/* * 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 "WebPageProxy.h" #import #import static const CGFloat dropdownTopMargin = 2; static const CGFloat dropdownRowHeight = 20; static const CGFloat dropdownShadowHeight = 5; static const CGFloat dropdownShadowBlurRadius = 3; static const size_t dropdownMaxSuggestions = 6; static NSString * const suggestionCellReuseIdentifier = @"WKDataListSuggestionCell"; @interface WKDataListSuggestionWindow : NSWindow @end @interface WKDataListSuggestionCell : NSView { RetainPtr _textField; BOOL _mouseIsOver; BOOL _active; } @property (nonatomic, assign) BOOL active; - (void)setText:(NSString *)text; @end @interface WKDataListSuggestionTable : NSTableView { RetainPtr _enclosingScrollView; Optional _activeRow; } - (id)initWithElementRect:(const WebCore::IntRect&)rect; - (void)setVisibleRect:(NSRect)rect; - (void)setActiveRow:(size_t)row; - (Optional)currentActiveRow; - (void)reload; @end @interface WKDataListSuggestionsView : NSObject { @private RetainPtr _table; WebKit::WebDataListSuggestionsDropdownMac* _dropdown; Vector _suggestions; NSView *_view; RetainPtr _enclosingWindow; } - (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::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([[WKDataListSuggestionsView 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 @end @implementation WKDataListSuggestionCell @synthesize active=_active; - (id)initWithFrame:(NSRect)frameRect { if (!(self = [super initWithFrame:frameRect])) return self; _textField = adoptNS([[NSTextField alloc] init]); [_textField setEditable:NO]; [_textField setBezeled:NO]; [_textField setBackgroundColor:[NSColor clearColor]]; [self addSubview:_textField.get()]; [self setIdentifier:@"WKDataListSuggestionCell"]; [self addTrackingRect:self.bounds owner:self userData:nil assumeInside:NO]; return self; } - (void)setText:(NSString *)text { [_textField setStringValue:text]; [_textField sizeToFit]; NSRect textFieldFrame = [_textField frame]; [_textField setFrame:NSMakeRect(0, (NSHeight(self.frame) - NSHeight(textFieldFrame)) / 2, NSWidth(textFieldFrame), NSHeight(textFieldFrame))]; _mouseIsOver = NO; self.active = NO; } - (void)setActive:(BOOL)shouldActivate { _active = shouldActivate; [self setNeedsDisplay:YES]; } - (void)drawRect:(NSRect)dirtyRect { if (self.active) { [[NSColor alternateSelectedControlColor] setFill]; [_textField setTextColor:[NSColor alternateSelectedControlTextColor]]; } else if (_mouseIsOver) { [[NSColor quaternaryLabelColor] setFill]; [_textField setTextColor:[NSColor textColor]]; } else { [[NSColor controlBackgroundColor] setFill]; [_textField setTextColor:[NSColor textColor]]; } NSRectFill(dirtyRect); [super drawRect:dirtyRect]; } - (void)mouseEntered:(NSEvent *)event { [super mouseEntered:event]; _mouseIsOver = YES; [self setNeedsDisplay:YES]; } - (void)mouseExited:(NSEvent *)event { [super mouseExited:event]; _mouseIsOver = NO; [self setNeedsDisplay:YES]; } - (BOOL)acceptsFirstResponder { return NO; } @end @implementation WKDataListSuggestionTable - (id)initWithElementRect:(const WebCore::IntRect&)rect { if (!(self = [super initWithFrame:NSMakeRect(0, 0, rect.width(), 0)])) return self; [self setIntercellSpacing:NSZeroSize]; [self setHeaderView:nil]; [self setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleNone]; auto column = adoptNS([[NSTableColumn alloc] init]); [column setWidth:rect.width()]; [self addTableColumn:column.get()]; _enclosingScrollView = adoptNS([[NSScrollView alloc] init]); [_enclosingScrollView setHasVerticalScroller:YES]; [_enclosingScrollView setVerticalScrollElasticity:NSScrollElasticityNone]; [_enclosingScrollView setHorizontalScrollElasticity:NSScrollElasticityNone]; [_enclosingScrollView setDocumentView:self]; auto dropShadow = adoptNS([[NSShadow alloc] init]); [dropShadow setShadowColor:[NSColor systemGrayColor]]; [dropShadow setShadowOffset:NSMakeSize(0, dropdownShadowHeight)]; [dropShadow setShadowBlurRadius:dropdownShadowBlurRadius]; [_enclosingScrollView setWantsLayer:YES]; [_enclosingScrollView setShadow:dropShadow.get()]; return self; } - (void)setVisibleRect:(NSRect)rect { [self setFrame:NSMakeRect(0, 0, NSWidth(rect) - dropdownShadowHeight * 2, 0)]; [_enclosingScrollView setFrame:NSMakeRect(dropdownShadowHeight, dropdownShadowHeight, NSWidth(rect) - dropdownShadowHeight * 2, NSHeight(rect) - dropdownShadowHeight)]; } - (Optional)currentActiveRow { return _activeRow; } - (void)setActiveRow:(size_t)row { if (_activeRow) { WKDataListSuggestionCell *oldCell = (WKDataListSuggestionCell *)[self viewAtColumn:0 row:_activeRow.value() makeIfNecessary:NO]; oldCell.active = NO; } [self scrollRowToVisible:row]; WKDataListSuggestionCell *newCell = (WKDataListSuggestionCell *)[self viewAtColumn:0 row:row makeIfNecessary:NO]; newCell.active = YES; _activeRow = row; } - (void)reload { _activeRow = WTF::nullopt; [self reloadData]; } - (BOOL)acceptsFirstResponder { return NO; } - (NSScrollView *)enclosingScrollView { return _enclosingScrollView.get(); } - (void)removeFromSuperviewWithoutNeedingDisplay { [super removeFromSuperviewWithoutNeedingDisplay]; [_enclosingScrollView removeFromSuperviewWithoutNeedingDisplay]; } @end @implementation WKDataListSuggestionsView - (id)initWithInformation:(WebCore::DataListSuggestionInformation&&)information inView:(NSView *)view { if (!(self = [super init])) return self; _view = view; _suggestions = WTFMove(information.suggestions); _table = adoptNS([[WKDataListSuggestionTable alloc] initWithElementRect:information.elementRect]); _enclosingWindow = adoptNS([[WKDataListSuggestionWindow alloc] initWithContentRect:NSZeroRect styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]); [_enclosingWindow setReleasedWhenClosed:NO]; [_enclosingWindow setFrame:[self dropdownRectForElementRect:information.elementRect] display:YES]; [_enclosingWindow setBackgroundColor:[NSColor clearColor]]; [_enclosingWindow setOpaque:NO]; [_table setVisibleRect:[_enclosingWindow frame]]; [_table setDelegate:self]; [_table setDataSource:self]; [_table setAction:@selector(selectedRow:)]; [_table setTarget:self]; return self; } - (String)currentSelectedString { Optional selectedRow = [_table currentActiveRow]; if (selectedRow && selectedRow.value() < _suggestions.size()) return _suggestions.at(selectedRow.value()); return String(); } - (void)updateWithInformation:(WebCore::DataListSuggestionInformation&&)information { _suggestions = WTFMove(information.suggestions); [_table reload]; [_enclosingWindow setFrame:[self dropdownRectForElementRect:information.elementRect] display:YES]; [_table setVisibleRect:[_enclosingWindow frame]]; } - (void)moveSelectionByDirection:(const String&)direction { size_t size = _suggestions.size(); Optional oldSelection = [_table currentActiveRow]; size_t newSelection; if (oldSelection) { size_t oldValue = oldSelection.value(); if (direction == "Up") newSelection = oldValue ? (oldValue - 1) : (size - 1); else newSelection = (oldValue + 1) % size; } else newSelection = (direction == "Up") ? (size - 1) : 0; [_table setActiveRow:newSelection]; } - (void)invalidate { [_table removeFromSuperviewWithoutNeedingDisplay]; [_table setDelegate:nil]; [_table setDataSource:nil]; [_table setTarget:nil]; _table = nil; [[_view window] removeChildWindow:_enclosingWindow.get()]; [_enclosingWindow close]; _enclosingWindow = nil; } - (NSRect)dropdownRectForElementRect:(const WebCore::IntRect&)rect { NSRect windowRect = [[_view window] convertRectToScreen:[_view convertRect:rect toView:nil]]; CGFloat height = std::min(_suggestions.size(), dropdownMaxSuggestions) * dropdownRowHeight; return NSMakeRect(NSMinX(windowRect) - dropdownShadowHeight, NSMinY(windowRect) - height - dropdownShadowHeight - dropdownTopMargin, rect.width() + dropdownShadowHeight * 2, height + dropdownShadowHeight); } - (void)showSuggestionsDropdown:(WebKit::WebDataListSuggestionsDropdownMac*)dropdown { _dropdown = dropdown; [[_enclosingWindow contentView] addSubview:[_table enclosingScrollView]]; [_table reload]; [[_view window] addChildWindow:_enclosingWindow.get() ordered:NSWindowAbove]; [[_table enclosingScrollView] flashScrollers]; } - (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; } - (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { WKDataListSuggestionCell *result = [tableView makeViewWithIdentifier:suggestionCellReuseIdentifier owner:self]; if (!result) { result = [[[WKDataListSuggestionCell alloc] initWithFrame:NSMakeRect(0, 0, tableView.frame.size.width, dropdownRowHeight)] autorelease]; [result setIdentifier:suggestionCellReuseIdentifier]; } [result setText:_suggestions.at(row)]; Optional currentActiveRow = [_table currentActiveRow]; if (currentActiveRow && static_cast(row) == currentActiveRow.value()) result.active = YES; return result; } @end #endif // ENABLE(DATALIST_ELEMENT) && USE(APPKIT)