| /* |
| * 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 <WebCore/IntRect.h> |
| #import <pal/spi/cocoa/NSColorSPI.h> |
| |
| 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<NSTextField> _textField; |
| BOOL _mouseIsOver; |
| BOOL _active; |
| } |
| |
| @property (nonatomic, assign) BOOL active; |
| |
| - (void)setText:(NSString *)text; |
| @end |
| |
| @interface WKDataListSuggestionTable : NSTableView { |
| RetainPtr<NSScrollView> _enclosingScrollView; |
| Optional<size_t> _activeRow; |
| } |
| |
| - (id)initWithElementRect:(const WebCore::IntRect&)rect; |
| - (void)setVisibleRect:(NSRect)rect; |
| - (void)setActiveRow:(size_t)row; |
| - (Optional<size_t>)currentActiveRow; |
| - (void)reload; |
| @end |
| |
| @interface WKDataListSuggestionsView : NSObject<NSTableViewDataSource, NSTableViewDelegate> { |
| @private |
| RetainPtr<WKDataListSuggestionTable> _table; |
| WebKit::WebDataListSuggestionsDropdownMac* _dropdown; |
| Vector<String> _suggestions; |
| NSView *_view; |
| RetainPtr<NSWindow> _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> 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<size_t>)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<size_t> 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<size_t> 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<size_t> currentActiveRow = [_table currentActiveRow]; |
| if (currentActiveRow && static_cast<size_t>(row) == currentActiveRow.value()) |
| result.active = YES; |
| |
| return result; |
| } |
| |
| @end |
| |
| #endif // ENABLE(DATALIST_ELEMENT) && USE(APPKIT) |