| /* |
| * Copyright (C) 2018 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 "WebDataListSuggestionsDropdownMac.h" |
| |
| #if ENABLE(DATALIST_ELEMENT) && USE(APPKIT) |
| |
| #import "AppKitSPI.h" |
| #import "WebPageProxy.h" |
| #import <WebCore/IntRect.h> |
| #import <WebCore/LocalizedStrings.h> |
| #import <pal/spi/cocoa/NSColorSPI.h> |
| |
| constexpr CGFloat dropdownTopMargin = 3; |
| constexpr CGFloat dropdownVerticalPadding = 4; |
| constexpr CGFloat dropdownRowHeightWithoutLabel = 20; |
| constexpr CGFloat dropdownRowHeightWithLabel = 40; |
| constexpr CGFloat maximumTotalHeightForDropdownCells = 120; |
| static NSString * const suggestionCellReuseIdentifier = @"WKDataListSuggestionView"; |
| |
| @interface WKDataListSuggestionWindow : NSWindow |
| @end |
| |
| @interface WKDataListSuggestionView : NSTableCellView |
| @property (nonatomic) BOOL shouldShowBottomDivider; |
| @end |
| |
| @interface WKDataListSuggestionTableRowView : NSTableRowView |
| @end |
| |
| @interface WKDataListSuggestionTableView : NSTableView |
| @end |
| |
| @interface WKDataListSuggestionsController : NSObject<NSTableViewDataSource, NSTableViewDelegate> |
| |
| - (id)initWithInformation:(WebCore::DataListSuggestionInformation&&)information inView:(NSView *)view; |
| - (void)showSuggestionsDropdown:(WebKit::WebDataListSuggestionsDropdownMac&)dropdown; |
| - (void)updateWithInformation:(WebCore::DataListSuggestionInformation&&)information; |
| - (void)moveSelectionByDirection:(const String&)direction; |
| - (void)invalidate; |
| |
| - (String)currentSelectedString; |
| |
| @end |
| |
| namespace WebKit { |
| |
| Ref<WebDataListSuggestionsDropdownMac> WebDataListSuggestionsDropdownMac::create(WebPageProxy& page, NSView *view) |
| { |
| return adoptRef(*new WebDataListSuggestionsDropdownMac(page, view)); |
| } |
| |
| WebDataListSuggestionsDropdownMac::~WebDataListSuggestionsDropdownMac() { } |
| |
| WebDataListSuggestionsDropdownMac::WebDataListSuggestionsDropdownMac(WebPageProxy& page, NSView *view) |
| : WebDataListSuggestionsDropdown(page) |
| , m_view(view) |
| { |
| } |
| |
| void WebDataListSuggestionsDropdownMac::show(WebCore::DataListSuggestionInformation&& information) |
| { |
| if (m_dropdownUI) { |
| [m_dropdownUI updateWithInformation:WTFMove(information)]; |
| return; |
| } |
| |
| m_dropdownUI = adoptNS([[WKDataListSuggestionsController alloc] initWithInformation:WTFMove(information) inView:m_view]); |
| [m_dropdownUI showSuggestionsDropdown:*this]; |
| } |
| |
| void WebDataListSuggestionsDropdownMac::didSelectOption(const String& selectedOption) |
| { |
| if (!m_page) |
| return; |
| |
| m_page->didSelectOption(selectedOption); |
| close(); |
| } |
| |
| void WebDataListSuggestionsDropdownMac::selectOption() |
| { |
| if (!m_page) |
| return; |
| |
| String selectedOption = [m_dropdownUI currentSelectedString]; |
| if (!selectedOption.isNull()) |
| m_page->didSelectOption(selectedOption); |
| |
| close(); |
| } |
| |
| void WebDataListSuggestionsDropdownMac::handleKeydownWithIdentifier(const String& key) |
| { |
| if (key == "Enter") |
| selectOption(); |
| else if (key == "Up" || key == "Down") |
| [m_dropdownUI moveSelectionByDirection:key]; |
| } |
| |
| void WebDataListSuggestionsDropdownMac::close() |
| { |
| [m_dropdownUI invalidate]; |
| m_dropdownUI = nil; |
| WebDataListSuggestionsDropdown::close(); |
| } |
| |
| } // namespace WebKit |
| |
| @implementation WKDataListSuggestionWindow { |
| RetainPtr<NSVisualEffectView> _backdropView; |
| } |
| |
| - (id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)styleMask backing:(NSBackingStoreType)backingStoreType defer:(BOOL)defer |
| { |
| self = [super initWithContentRect:contentRect styleMask:styleMask backing:backingStoreType defer:defer]; |
| if (!self) |
| return nil; |
| |
| self.hasShadow = YES; |
| |
| _backdropView = [[NSVisualEffectView alloc] initWithFrame:contentRect]; |
| [_backdropView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; |
| [_backdropView setMaterial:NSVisualEffectMaterialMenu]; |
| [_backdropView setState:NSVisualEffectStateActive]; |
| [_backdropView setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; |
| |
| [self setContentView:_backdropView.get()]; |
| |
| return self; |
| } |
| |
| - (BOOL)canBecomeKeyWindow |
| { |
| return NO; |
| } |
| |
| - (BOOL)hasKeyAppearance |
| { |
| return YES; |
| } |
| |
| - (NSWindowShadowOptions)shadowOptions |
| { |
| return NSWindowShadowSecondaryWindow; |
| } |
| |
| @end |
| |
| @implementation WKDataListSuggestionView { |
| RetainPtr<NSTextField> _valueField; |
| RetainPtr<NSTextField> _labelField; |
| RetainPtr<NSView> _bottomDivider; |
| } |
| |
| - (id)initWithFrame:(NSRect)frameRect |
| { |
| if (!(self = [super initWithFrame:frameRect])) |
| return self; |
| |
| _valueField = adoptNS([[NSTextField alloc] init]); |
| _labelField = adoptNS([[NSTextField alloc] init]); |
| _bottomDivider = adoptNS([[NSView alloc] init]); |
| [_bottomDivider setWantsLayer:YES]; |
| [_bottomDivider setHidden:YES]; |
| [_bottomDivider layer].backgroundColor = NSColor.separatorColor.CGColor; |
| [self addSubview:_bottomDivider.get()]; |
| |
| auto setUpTextField = [&](NSTextField *textField) { |
| textField.editable = NO; |
| textField.bezeled = NO; |
| textField.font = [NSFont menuFontOfSize:0]; |
| textField.drawsBackground = NO; |
| [self addSubview:textField]; |
| }; |
| |
| setUpTextField(_valueField.get()); |
| setUpTextField(_labelField.get()); |
| self.identifier = suggestionCellReuseIdentifier; |
| |
| return self; |
| } |
| |
| - (void)layout |
| { |
| [super layout]; |
| |
| auto bounds = self.bounds; |
| auto width = bounds.size.width; |
| if (![_labelField isHidden]) { |
| auto halfOfHeight = bounds.size.height / 2; |
| [_valueField setFrame:NSMakeRect(0, halfOfHeight, width, halfOfHeight)]; |
| [_labelField setFrame:NSMakeRect(0, 0, width, halfOfHeight)]; |
| } else |
| [_valueField setFrame:bounds]; |
| |
| if (![_bottomDivider isHidden]) |
| [_bottomDivider setFrame:NSMakeRect(0, 0, width, 0.5)]; |
| } |
| |
| - (void)setValue:(NSString *)value label:(NSString *)label |
| { |
| if ([[_valueField stringValue] isEqualToString:value] && [[_labelField stringValue] isEqualToString:label]) |
| return; |
| |
| [_valueField setStringValue:value]; |
| [_labelField setStringValue:label]; |
| [_labelField setHidden:!label.length]; |
| self.needsLayout = YES; |
| } |
| |
| - (void)setShouldShowBottomDivider:(BOOL)showBottomDivider |
| { |
| if ([_bottomDivider isHidden] == !showBottomDivider) |
| return; |
| |
| [_bottomDivider setHidden:!showBottomDivider]; |
| self.needsLayout = YES; |
| } |
| |
| - (BOOL)shouldShowBottomDivider |
| { |
| return [_bottomDivider isHidden]; |
| } |
| |
| - (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle |
| { |
| [super setBackgroundStyle:backgroundStyle]; |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| [_valueField setTextColor:backgroundStyle == NSBackgroundStyleLight ? NSColor.textColor : NSColor.alternateSelectedControlTextColor]; |
| [_labelField setTextColor:NSColor.secondaryLabelColor]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| } |
| |
| - (BOOL)acceptsFirstResponder |
| { |
| return NO; |
| } |
| |
| @end |
| |
| @implementation WKDataListSuggestionTableRowView |
| |
| - (void)drawSelectionInRect:(NSRect)dirtyRect |
| { |
| [self setEmphasized:YES]; |
| [super drawSelectionInRect:dirtyRect]; |
| } |
| |
| @end |
| |
| @implementation WKDataListSuggestionTableView |
| |
| - (id)initWithElementRect:(const WebCore::IntRect&)rect |
| { |
| if (!(self = [super initWithFrame:NSMakeRect(0, 0, rect.width(), 0)])) |
| return self; |
| |
| [self setHeaderView:nil]; |
| [self setBackgroundColor:[NSColor clearColor]]; |
| [self setIntercellSpacing:NSMakeSize(0, self.intercellSpacing.height)]; |
| |
| auto column = adoptNS([[NSTableColumn alloc] init]); |
| [column setWidth:rect.width()]; |
| [self addTableColumn:column.get()]; |
| |
| return self; |
| } |
| |
| - (void)reload |
| { |
| [self reloadData]; |
| } |
| |
| - (BOOL)acceptsFirstResponder |
| { |
| return NO; |
| } |
| |
| @end |
| |
| static BOOL shouldShowDividersBetweenCells(const Vector<WebCore::DataListSuggestion>& suggestions) |
| { |
| return notFound != suggestions.findMatching([](auto& suggestion) { |
| return !suggestion.label.isEmpty(); |
| }); |
| } |
| |
| @implementation WKDataListSuggestionsController { |
| WeakPtr<WebKit::WebDataListSuggestionsDropdownMac> _dropdown; |
| Vector<WebCore::DataListSuggestion> _suggestions; |
| NSView *_presentingView; |
| |
| RetainPtr<NSScrollView> _scrollView; |
| RetainPtr<WKDataListSuggestionWindow> _enclosingWindow; |
| RetainPtr<WKDataListSuggestionTableView> _table; |
| BOOL _showDividersBetweenCells; |
| } |
| |
| - (id)initWithInformation:(WebCore::DataListSuggestionInformation&&)information inView:(NSView *)presentingView |
| { |
| if (!(self = [super init])) |
| return self; |
| |
| _presentingView = presentingView; |
| _suggestions = WTFMove(information.suggestions); |
| _showDividersBetweenCells = shouldShowDividersBetweenCells(_suggestions); |
| _table = adoptNS([[WKDataListSuggestionTableView alloc] initWithElementRect:information.elementRect]); |
| |
| _enclosingWindow = adoptNS([[WKDataListSuggestionWindow alloc] initWithContentRect:NSZeroRect styleMask:(NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView) backing:NSBackingStoreBuffered defer:NO]); |
| [_enclosingWindow setReleasedWhenClosed:NO]; |
| [_enclosingWindow setFrame:[self dropdownRectForElementRect:information.elementRect] display:YES]; |
| [_enclosingWindow setTitleVisibility:NSWindowTitleHidden]; |
| [_enclosingWindow setTitlebarAppearsTransparent:YES]; |
| [_enclosingWindow setMovable:NO]; |
| [_enclosingWindow setBackgroundColor:[NSColor clearColor]]; |
| [_enclosingWindow setOpaque:NO]; |
| |
| _scrollView = adoptNS([[NSScrollView alloc] initWithFrame:[_enclosingWindow contentView].bounds]); |
| [_scrollView setHasVerticalScroller:YES]; |
| [_scrollView setVerticalScrollElasticity:NSScrollElasticityAllowed]; |
| [_scrollView setHorizontalScrollElasticity:NSScrollElasticityNone]; |
| [_scrollView setDocumentView:_table.get()]; |
| [_scrollView setDrawsBackground:NO]; |
| [[_scrollView contentView] setAutomaticallyAdjustsContentInsets:NO]; |
| [[_scrollView contentView] setContentInsets:NSEdgeInsetsMake(dropdownVerticalPadding, 0, dropdownVerticalPadding, 0)]; |
| [[_scrollView contentView] scrollToPoint:NSMakePoint(0, -dropdownVerticalPadding)]; |
| |
| [_table setDelegate:self]; |
| [_table setDataSource:self]; |
| [_table setAction:@selector(selectedRow:)]; |
| [_table setTarget:self]; |
| |
| return self; |
| } |
| |
| - (String)currentSelectedString |
| { |
| NSInteger selectedRow = [_table selectedRow]; |
| |
| if (selectedRow >= 0 && static_cast<size_t>(selectedRow) < _suggestions.size()) |
| return _suggestions.at(selectedRow).value; |
| |
| return String(); |
| } |
| |
| - (void)updateWithInformation:(WebCore::DataListSuggestionInformation&&)information |
| { |
| _suggestions = WTFMove(information.suggestions); |
| _showDividersBetweenCells = shouldShowDividersBetweenCells(_suggestions); |
| [_table reload]; |
| |
| [_enclosingWindow setFrame:[self dropdownRectForElementRect:information.elementRect] display:YES]; |
| [_scrollView setFrame:[_enclosingWindow contentView].bounds]; |
| } |
| |
| - (void)notifyAccessibilityClients:(NSString *)info |
| { |
| NSDictionary<NSAccessibilityNotificationUserInfoKey, id> *userInfo = [NSDictionary dictionaryWithObjectsAndKeys: |
| NSAccessibilityPriorityKey, @(NSAccessibilityPriorityHigh), |
| NSAccessibilityAnnouncementKey, info, nil]; |
| NSAccessibilityPostNotificationWithUserInfo(NSApp, NSAccessibilityAnnouncementRequestedNotification, userInfo); |
| } |
| |
| - (void)moveSelectionByDirection:(const String&)direction |
| { |
| size_t size = _suggestions.size(); |
| NSInteger oldSelection = [_table selectedRow]; |
| |
| size_t newSelection; |
| if (oldSelection != -1) { |
| if (direction == "Up") |
| newSelection = oldSelection ? (oldSelection - 1) : (size - 1); |
| else |
| newSelection = (oldSelection + 1) % size; |
| } else |
| newSelection = (direction == "Up") ? (size - 1) : 0; |
| |
| [_table selectRowIndexes:[NSIndexSet indexSetWithIndex:newSelection] byExtendingSelection:NO]; |
| [_table scrollRowToVisible:newSelection]; |
| |
| // Notify accessibility clients of new selection. |
| NSString *currentSelectedString = [self currentSelectedString]; |
| [self notifyAccessibilityClients:currentSelectedString]; |
| } |
| |
| - (void)invalidate |
| { |
| [_table removeFromSuperviewWithoutNeedingDisplay]; |
| [_scrollView removeFromSuperviewWithoutNeedingDisplay]; |
| |
| [_table setDelegate:nil]; |
| [_table setDataSource:nil]; |
| [_table setTarget:nil]; |
| |
| _table = nil; |
| _scrollView = nil; |
| |
| [[_presentingView window] removeChildWindow:_enclosingWindow.get()]; |
| [_enclosingWindow close]; |
| _enclosingWindow = nil; |
| |
| // Notify accessibility clients that datalist went away. |
| NSString *info = WEB_UI_STRING("Suggestions list hidden.", "Accessibility announcement for the data list suggestions dropdown going away."); |
| [self notifyAccessibilityClients:info]; |
| } |
| |
| - (NSRect)dropdownRectForElementRect:(const WebCore::IntRect&)rect |
| { |
| NSRect windowRect = [[_presentingView window] convertRectToScreen:[_presentingView convertRect:rect toView:nil]]; |
| |
| CGFloat totalCellHeight = 0; |
| for (auto& suggestion : _suggestions) |
| totalCellHeight += suggestion.label.isEmpty() ? dropdownRowHeightWithoutLabel : dropdownRowHeightWithLabel; |
| |
| CGFloat totalIntercellSpacingAndPadding = dropdownVerticalPadding * 2; |
| if (_suggestions.size() > 1) |
| totalIntercellSpacingAndPadding += (_suggestions.size() - 1) * [_table intercellSpacing].height; |
| |
| CGFloat height = totalIntercellSpacingAndPadding + std::min(totalCellHeight, maximumTotalHeightForDropdownCells); |
| return NSMakeRect(NSMinX(windowRect), NSMinY(windowRect) - height - dropdownTopMargin, rect.width(), height); |
| } |
| |
| - (void)showSuggestionsDropdown:(WebKit::WebDataListSuggestionsDropdownMac&)dropdown |
| { |
| _dropdown = makeWeakPtr(dropdown); |
| [[_enclosingWindow contentView] addSubview:_scrollView.get()]; |
| [_table reload]; |
| [[_presentingView window] addChildWindow:_enclosingWindow.get() ordered:NSWindowAbove]; |
| [_scrollView flashScrollers]; |
| |
| // Notify accessibility clients of datalist becoming visible. |
| NSString *currentSelectedString = [self currentSelectedString]; |
| NSString *info = [NSString stringWithFormat:WEB_UI_STRING("Suggestions list visible, %@", "Accessibility announcement that the suggestions list became visible. The format argument is for the first option in the list."), currentSelectedString]; |
| [self notifyAccessibilityClients:info]; |
| } |
| |
| - (void)selectedRow:(NSTableView *)sender |
| { |
| auto selectedString = self.currentSelectedString; |
| if (!selectedString) |
| return; |
| |
| _dropdown->didSelectOption(selectedString); |
| } |
| |
| - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView |
| { |
| return _suggestions.size(); |
| } |
| |
| - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row |
| { |
| auto suggestionIndex = static_cast<size_t>(row); |
| if (suggestionIndex >= _suggestions.size()) { |
| ASSERT_NOT_REACHED(); |
| return 0; |
| } |
| |
| return _suggestions.at(suggestionIndex).label.isEmpty() ? dropdownRowHeightWithoutLabel : dropdownRowHeightWithLabel; |
| } |
| |
| - (NSTableRowView *)tableView:(NSTableView *)tableView rowViewForRow:(NSInteger)row |
| { |
| return [[[WKDataListSuggestionTableRowView alloc] init] autorelease]; |
| } |
| |
| - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row |
| { |
| WKDataListSuggestionView *result = [tableView makeViewWithIdentifier:suggestionCellReuseIdentifier owner:self]; |
| |
| if (!result) { |
| result = [[[WKDataListSuggestionView alloc] init] autorelease]; |
| [result setIdentifier:suggestionCellReuseIdentifier]; |
| } |
| |
| auto& suggestion = _suggestions.at(row); |
| result.shouldShowBottomDivider = _showDividersBetweenCells && row < static_cast<NSInteger>(_suggestions.size() - 1); |
| [result setValue:suggestion.value label:suggestion.label]; |
| |
| return result; |
| } |
| |
| @end |
| |
| #endif // ENABLE(DATALIST_ELEMENT) && USE(APPKIT) |