| /* |
| * Copyright (C) 2005, 2006, 2007, 2008, 2009 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. |
| * 3. Neither the name of Apple Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE 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 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 "WebTextCompletionController.h" |
| |
| #import "DOMNodeInternal.h" |
| #import "DOMRangeInternal.h" |
| #import "WebFrameInternal.h" |
| #import "WebHTMLViewInternal.h" |
| #import "WebView.h" |
| #import <WebCore/Frame.h> |
| #import <WebCore/SimpleRange.h> |
| |
| @interface NSWindow (WebNSWindowDetails) |
| - (void)_setForceActiveControls:(BOOL)flag; |
| @end |
| |
| using namespace WebCore; |
| |
| // This class handles the complete: operation. |
| // It counts on its host view to call endRevertingChange: whenever the current completion needs to be aborted. |
| |
| // The class is in one of two modes: Popup window showing, or not. |
| // It is shown when a completion yields more than one match. |
| // If a completion yields one or zero matches, it is not shown, and there is no state carried across to the next completion. |
| |
| @implementation WebTextCompletionController |
| |
| - (id)initWithWebView:(WebView *)view HTMLView:(WebHTMLView *)htmlView |
| { |
| self = [super init]; |
| if (!self) |
| return nil; |
| _view = view; |
| _htmlView = htmlView; |
| return self; |
| } |
| |
| - (void)dealloc |
| { |
| [_popupWindow release]; |
| [_completions release]; |
| [_originalString release]; |
| |
| [super dealloc]; |
| } |
| |
| - (void)_insertMatch:(NSString *)match |
| { |
| // FIXME: 3769654 - We should preserve case of string being inserted, even in prefix (but then also be |
| // able to revert that). Mimic NSText. |
| WebFrame *frame = [_htmlView _frame]; |
| NSString *newText = [match substringFromIndex:prefixLength]; |
| [frame _replaceSelectionWithText:newText selectReplacement:YES smartReplace:NO]; |
| } |
| |
| // mostly lifted from NSTextView_KeyBinding.m |
| - (void)_buildUI |
| { |
| NSRect scrollFrame = NSMakeRect(0, 0, 100, 100); |
| NSRect tableFrame = NSZeroRect; |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| tableFrame.size = [NSScrollView contentSizeForFrameSize:scrollFrame.size hasHorizontalScroller:NO hasVerticalScroller:YES borderType:NSNoBorder]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| auto column = adoptNS([[NSTableColumn alloc] init]); |
| [column setWidth:tableFrame.size.width]; |
| [column setEditable:NO]; |
| |
| _tableView = [[NSTableView alloc] initWithFrame:tableFrame]; |
| [_tableView setAutoresizingMask:NSViewWidthSizable]; |
| [_tableView addTableColumn:column.get()]; |
| [_tableView setGridStyleMask:NSTableViewGridNone]; |
| [_tableView setCornerView:nil]; |
| [_tableView setHeaderView:nil]; |
| [_tableView setColumnAutoresizingStyle:NSTableViewUniformColumnAutoresizingStyle]; |
| [_tableView setDelegate:self]; |
| [_tableView setDataSource:self]; |
| [_tableView setTarget:self]; |
| [_tableView setDoubleAction:@selector(tableAction:)]; |
| |
| auto scrollView = adoptNS([[NSScrollView alloc] initWithFrame:scrollFrame]); |
| [scrollView setBorderType:NSNoBorder]; |
| [scrollView setHasVerticalScroller:YES]; |
| [scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; |
| [scrollView setDocumentView:_tableView]; |
| [_tableView release]; |
| |
| _popupWindow = [[NSWindow alloc] initWithContentRect:scrollFrame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]; |
| [_popupWindow setAlphaValue:0.88f]; |
| [_popupWindow setContentView:scrollView.get()]; |
| [_popupWindow setHasShadow:YES]; |
| [_popupWindow _setForceActiveControls:YES]; |
| [_popupWindow setReleasedWhenClosed:NO]; |
| } |
| |
| // mostly lifted from NSTextView_KeyBinding.m |
| - (void)_placePopupWindow:(NSPoint)topLeft |
| { |
| NSUInteger numberToShow = [_completions count]; |
| if (numberToShow > 20) |
| numberToShow = 20; |
| |
| NSRect windowFrame; |
| NSPoint wordStart = topLeft; |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| windowFrame.origin = [[_view window] convertBaseToScreen:[_htmlView convertPoint:wordStart toView:nil]]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| windowFrame.size.height = numberToShow * [_tableView rowHeight] + (numberToShow + 1) * [_tableView intercellSpacing].height; |
| windowFrame.origin.y -= windowFrame.size.height; |
| NSDictionary *attributes = @{ NSFontAttributeName: [NSFont systemFontOfSize:12.0f] }; |
| CGFloat maxWidth = 0; |
| int maxIndex = -1; |
| for (NSUInteger i = 0; i < numberToShow; i++) { |
| float width = ceilf([[_completions objectAtIndex:i] sizeWithAttributes:attributes].width); |
| if (width > maxWidth) { |
| maxWidth = width; |
| maxIndex = i; |
| } |
| } |
| windowFrame.size.width = 100; |
| if (maxIndex >= 0) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| maxWidth = ceilf([NSScrollView frameSizeForContentSize:NSMakeSize(maxWidth, 100.0f) hasHorizontalScroller:NO hasVerticalScroller:YES borderType:NSNoBorder].width); |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| maxWidth = ceilf([NSWindow frameRectForContentRect:NSMakeRect(0.0f, 0.0f, maxWidth, 100.0f) styleMask:NSWindowStyleMaskBorderless].size.width); |
| maxWidth += 5.0f; |
| windowFrame.size.width = std::max(maxWidth, windowFrame.size.width); |
| } |
| [_popupWindow setFrame:windowFrame display:NO]; |
| |
| [_tableView reloadData]; |
| [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; |
| [_tableView scrollRowToVisible:0]; |
| [self _reflectSelection]; |
| [_popupWindow setLevel:NSPopUpMenuWindowLevel]; |
| [_popupWindow orderFront:nil]; |
| [[_view window] addChildWindow:_popupWindow ordered:NSWindowAbove]; |
| } |
| |
| - (void)doCompletion |
| { |
| if (!_popupWindow) { |
| NSSpellChecker *checker = [NSSpellChecker sharedSpellChecker]; |
| if (!checker) { |
| LOG_ERROR("No NSSpellChecker"); |
| return; |
| } |
| |
| // Get preceeding word stem |
| WebFrame *frame = [_htmlView _frame]; |
| DOMRange *selection = kit(core(frame)->selection().selection().toNormalizedRange()); |
| DOMRange *wholeWord = [frame _rangeByAlteringCurrentSelection:FrameSelection::AlterationExtend |
| direction:SelectionDirection::Backward granularity:TextGranularity::WordGranularity]; |
| DOMRange *prefix = [wholeWord cloneRange]; |
| [prefix setEnd:[selection startContainer] offset:[selection startOffset]]; |
| |
| // Reject some NOP cases |
| if ([prefix collapsed]) { |
| NSBeep(); |
| return; |
| } |
| NSString *prefixStr = [frame _stringForRange:prefix]; |
| NSString *trimmedPrefix = [prefixStr stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; |
| if ([trimmedPrefix length] == 0) { |
| NSBeep(); |
| return; |
| } |
| prefixLength = [prefixStr length]; |
| |
| // Lookup matches |
| [_completions release]; |
| _completions = [checker completionsForPartialWordRange:NSMakeRange(0, [prefixStr length]) inString:prefixStr language:nil inSpellDocumentWithTag:[_view spellCheckerDocumentTag]]; |
| [_completions retain]; |
| |
| if (!_completions || [_completions count] == 0) { |
| NSBeep(); |
| } else if ([_completions count] == 1) { |
| [self _insertMatch:[_completions objectAtIndex:0]]; |
| } else { |
| ASSERT(!_originalString); // this should only be set IFF we have a popup window |
| _originalString = [[frame _stringForRange:selection] retain]; |
| [self _buildUI]; |
| NSRect wordRect = [frame _caretRectAtPosition:Position(core([wholeWord startContainer]), [wholeWord startOffset], Position::PositionIsOffsetInAnchor) affinity:NSSelectionAffinityDownstream]; |
| // +1 to be under the word, not the caret |
| // FIXME - 3769652 - Wrong positioning for right to left languages. We should line up the upper |
| // right corner with the caret instead of upper left, and the +1 would be a -1. |
| NSPoint wordLowerLeft = { NSMinX(wordRect)+1, NSMaxY(wordRect) }; |
| [self _placePopupWindow:wordLowerLeft]; |
| } |
| } else { |
| [self endRevertingChange:YES moveLeft:NO]; |
| } |
| } |
| |
| - (void)endRevertingChange:(BOOL)revertChange moveLeft:(BOOL)goLeft |
| { |
| if (_popupWindow) { |
| // tear down UI |
| [[_view window] removeChildWindow:_popupWindow]; |
| [_popupWindow orderOut:self]; |
| // Must autorelease because event tracking code may be on the stack touching UI |
| [_popupWindow autorelease]; |
| _popupWindow = nil; |
| |
| if (revertChange) { |
| WebFrame *frame = [_htmlView _frame]; |
| [frame _replaceSelectionWithText:_originalString selectReplacement:YES smartReplace:NO]; |
| } else if ([_htmlView _hasSelection]) { |
| if (goLeft) |
| [_htmlView moveBackward:nil]; |
| else |
| [_htmlView moveForward:nil]; |
| } |
| [_originalString release]; |
| _originalString = nil; |
| } |
| // else there is no state to abort if the window was not up |
| } |
| |
| - (BOOL)popupWindowIsOpen |
| { |
| return _popupWindow != nil; |
| } |
| |
| // WebHTMLView gives us a crack at key events it sees. Return whether we consumed the event. |
| // The features for the various keys mimic NSTextView. |
| - (BOOL)filterKeyDown:(NSEvent *)event |
| { |
| if (!_popupWindow) |
| return NO; |
| NSString *string = [event charactersIgnoringModifiers]; |
| if (![string length]) |
| return NO; |
| unichar c = [string characterAtIndex:0]; |
| if (c == NSUpArrowFunctionKey) { |
| int selectedRow = [_tableView selectedRow]; |
| if (0 < selectedRow) { |
| [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectedRow - 1] byExtendingSelection:NO]; |
| [_tableView scrollRowToVisible:selectedRow - 1]; |
| } |
| return YES; |
| } |
| if (c == NSDownArrowFunctionKey) { |
| int selectedRow = [_tableView selectedRow]; |
| if (selectedRow < (int)[_completions count] - 1) { |
| [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectedRow + 1] byExtendingSelection:NO]; |
| [_tableView scrollRowToVisible:selectedRow + 1]; |
| } |
| return YES; |
| } |
| if (c == NSRightArrowFunctionKey || c == '\n' || c == '\r' || c == '\t') { |
| // FIXME: What about backtab? |
| [self endRevertingChange:NO moveLeft:NO]; |
| return YES; |
| } |
| if (c == NSLeftArrowFunctionKey) { |
| [self endRevertingChange:NO moveLeft:YES]; |
| return YES; |
| } |
| if (c == 0x1B || c == NSF5FunctionKey) { |
| // FIXME: F5? |
| [self endRevertingChange:YES moveLeft:NO]; |
| return YES; |
| } |
| if (c == ' ' || (c >= 0x21 && c <= 0x2F) || (c >= 0x3A && c <= 0x40) || (c >= 0x5B && c <= 0x60) || (c >= 0x7B && c <= 0x7D)) { |
| // FIXME: Is the above list of keys really definitive? |
| // Originally this code called ispunct; aren't there other punctuation keys on international keyboards? |
| [self endRevertingChange:NO moveLeft:NO]; |
| return NO; // let the char get inserted |
| } |
| return NO; |
| } |
| |
| - (void)_reflectSelection |
| { |
| int selectedRow = [_tableView selectedRow]; |
| ASSERT(selectedRow >= 0); |
| ASSERT(selectedRow < (int)[_completions count]); |
| [self _insertMatch:[_completions objectAtIndex:selectedRow]]; |
| } |
| |
| - (void)tableAction:(id)sender |
| { |
| [self _reflectSelection]; |
| [self endRevertingChange:NO moveLeft:NO]; |
| } |
| |
| - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView |
| { |
| return [_completions count]; |
| } |
| |
| - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row |
| { |
| return [_completions objectAtIndex:row]; |
| } |
| |
| - (void)tableViewSelectionDidChange:(NSNotification *)notification |
| { |
| [self _reflectSelection]; |
| } |
| |
| @end |