| /* |
| * 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> |
| |
| static const CGFloat dropdownTopMargin = 3; |
| static const CGFloat dropdownVerticalPadding = 4; |
| static const CGFloat dropdownRowHeight = 20; |
| static const size_t dropdownMaxSuggestions = 6; |
| static NSString * const suggestionCellReuseIdentifier = @"WKDataListSuggestionView"; |
| |
| @interface WKDataListSuggestionWindow : NSWindow |
| @end |
| |
| @interface WKDataListSuggestionView : NSTableCellView |
| @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> _textField; |
| } |
| |
| - (id)initWithFrame:(NSRect)frameRect |
| { |
| if (!(self = [super initWithFrame:frameRect])) |
| return self; |
| |
| _textField = adoptNS([[NSTextField alloc] initWithFrame:frameRect]); |
| self.textField = _textField.get(); |
| |
| [self addSubview:self.textField]; |
| |
| self.identifier = suggestionCellReuseIdentifier; |
| self.textField.editable = NO; |
| self.textField.bezeled = NO; |
| self.textField.font = [NSFont menuFontOfSize:0]; |
| self.textField.drawsBackground = NO; |
| |
| return self; |
| } |
| |
| - (void)setText:(NSString *)text |
| { |
| self.textField.stringValue = text; |
| } |
| |
| - (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle |
| { |
| [super setBackgroundStyle:backgroundStyle]; |
| self.textField.textColor = backgroundStyle == NSBackgroundStyleLight ? [NSColor textColor] : [NSColor alternateSelectedControlTextColor]; |
| } |
| |
| - (BOOL)acceptsFirstResponder |
| { |
| return NO; |
| } |
| |
| @end |
| |
| @implementation WKDataListSuggestionTableRowView |
| |
| - (void)drawSelectionInRect:(NSRect)dirtyRect |
| { |
| [self setEmphasized:YES]; |
| [super drawSelectionInRect:dirtyRect]; |
| } |
| |
| @end |
| |
| @implementation WKDataListSuggestionTableView { |
| RetainPtr<NSScrollView> _enclosingScrollView; |
| } |
| |
| - (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()]; |
| |
| _enclosingScrollView = adoptNS([[NSScrollView alloc] init]); |
| [_enclosingScrollView setHasVerticalScroller:YES]; |
| [_enclosingScrollView setVerticalScrollElasticity:NSScrollElasticityAllowed]; |
| [_enclosingScrollView setHorizontalScrollElasticity:NSScrollElasticityNone]; |
| [_enclosingScrollView setDocumentView:self]; |
| [_enclosingScrollView setDrawsBackground:NO]; |
| [[_enclosingScrollView contentView] setAutomaticallyAdjustsContentInsets:NO]; |
| [[_enclosingScrollView contentView] setContentInsets:NSEdgeInsetsMake(dropdownVerticalPadding, 0, dropdownVerticalPadding, 0)]; |
| [[_enclosingScrollView contentView] scrollToPoint:NSMakePoint(0, -dropdownVerticalPadding)]; |
| |
| return self; |
| } |
| |
| - (void)layout |
| { |
| [_enclosingScrollView setFrame:[_enclosingScrollView superview].bounds]; |
| } |
| |
| - (void)reload |
| { |
| [self reloadData]; |
| } |
| |
| - (BOOL)acceptsFirstResponder |
| { |
| return NO; |
| } |
| |
| - (NSScrollView *)enclosingScrollView |
| { |
| return _enclosingScrollView.get(); |
| } |
| |
| - (void)removeFromSuperviewWithoutNeedingDisplay |
| { |
| [super removeFromSuperviewWithoutNeedingDisplay]; |
| [_enclosingScrollView removeFromSuperviewWithoutNeedingDisplay]; |
| } |
| |
| @end |
| |
| @implementation WKDataListSuggestionsController { |
| WebKit::WebDataListSuggestionsDropdownMac* _dropdown; |
| Vector<String> _suggestions; |
| NSView *_presentingView; |
| |
| RetainPtr<WKDataListSuggestionWindow> _enclosingWindow; |
| RetainPtr<WKDataListSuggestionTableView> _table; |
| } |
| |
| - (id)initWithInformation:(WebCore::DataListSuggestionInformation&&)information inView:(NSView *)presentingView |
| { |
| if (!(self = [super init])) |
| return self; |
| |
| _presentingView = presentingView; |
| _suggestions = WTFMove(information.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]; |
| |
| [_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); |
| |
| return String(); |
| } |
| |
| - (void)updateWithInformation:(WebCore::DataListSuggestionInformation&&)information |
| { |
| _suggestions = WTFMove(information.suggestions); |
| [_table reload]; |
| |
| [_enclosingWindow setFrame:[self dropdownRectForElementRect:information.elementRect] display:YES]; |
| } |
| |
| - (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]; |
| |
| // Notify accessibility clients of new selection. |
| NSString *currentSelectedString = [self currentSelectedString]; |
| [self notifyAccessibilityClients:currentSelectedString]; |
| } |
| |
| - (void)invalidate |
| { |
| [_table removeFromSuperviewWithoutNeedingDisplay]; |
| |
| [_table setDelegate:nil]; |
| [_table setDataSource:nil]; |
| [_table setTarget:nil]; |
| |
| _table = 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]]; |
| auto visibleSuggestionCount = std::min(_suggestions.size(), dropdownMaxSuggestions); |
| CGFloat height = visibleSuggestionCount * (dropdownRowHeight + [_table intercellSpacing].height) + (dropdownVerticalPadding * 2); |
| return NSMakeRect(NSMinX(windowRect), NSMinY(windowRect) - height - dropdownTopMargin, rect.width(), height); |
| } |
| |
| - (void)showSuggestionsDropdown:(WebKit::WebDataListSuggestionsDropdownMac*)dropdown |
| { |
| _dropdown = dropdown; |
| [[_enclosingWindow contentView] addSubview:[_table enclosingScrollView]]; |
| [_table reload]; |
| [[_presentingView window] addChildWindow:_enclosingWindow.get() ordered:NSWindowAbove]; |
| [[_table enclosingScrollView] 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 |
| { |
| _dropdown->didSelectOption(_suggestions.at([sender selectedRow])); |
| } |
| |
| - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView |
| { |
| return _suggestions.size(); |
| } |
| |
| - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row |
| { |
| return dropdownRowHeight; |
| } |
| |
| - (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] initWithFrame:NSMakeRect(0, 0, tableView.frame.size.width, dropdownRowHeight)] autorelease]; |
| [result setIdentifier:suggestionCellReuseIdentifier]; |
| } |
| |
| result.text = _suggestions.at(row); |
| |
| return result; |
| } |
| |
| @end |
| |
| #endif // ENABLE(DATALIST_ELEMENT) && USE(APPKIT) |