WebKit/mac/WebView/WebTextCompletionController.mm
changeset 0 4f2f89ce4247
equal deleted inserted replaced
-1:000000000000 0:4f2f89ce4247
       
     1 /*
       
     2  * Copyright (C) 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights reserved.
       
     3  *
       
     4  * Redistribution and use in source and binary forms, with or without
       
     5  * modification, are permitted provided that the following conditions
       
     6  * are met:
       
     7  *
       
     8  * 1.  Redistributions of source code must retain the above copyright
       
     9  *     notice, this list of conditions and the following disclaimer. 
       
    10  * 2.  Redistributions in binary form must reproduce the above copyright
       
    11  *     notice, this list of conditions and the following disclaimer in the
       
    12  *     documentation and/or other materials provided with the distribution. 
       
    13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
       
    14  *     its contributors may be used to endorse or promote products derived
       
    15  *     from this software without specific prior written permission. 
       
    16  *
       
    17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
       
    18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
    19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
       
    20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
       
    21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
       
    22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
       
    23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
       
    24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
       
    25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
       
    26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
       
    27  */
       
    28 
       
    29 #import "WebTextCompletionController.h"
       
    30 
       
    31 #import "DOMRangeInternal.h"
       
    32 #import "WebFrameInternal.h"
       
    33 #import "WebHTMLViewInternal.h"
       
    34 #import "WebTypesInternal.h"
       
    35 #import <WebCore/Frame.h>
       
    36 
       
    37 @interface NSWindow (WebNSWindowDetails)
       
    38 - (void)_setForceActiveControls:(BOOL)flag;
       
    39 @end
       
    40 
       
    41 using namespace WebCore;
       
    42 using namespace std;
       
    43 
       
    44 // This class handles the complete: operation.
       
    45 // It counts on its host view to call endRevertingChange: whenever the current completion needs to be aborted.
       
    46 
       
    47 // The class is in one of two modes: Popup window showing, or not.
       
    48 // It is shown when a completion yields more than one match.
       
    49 // If a completion yields one or zero matches, it is not shown, and there is no state carried across to the next completion.
       
    50 
       
    51 @implementation WebTextCompletionController
       
    52 
       
    53 - (id)initWithWebView:(WebView *)view HTMLView:(WebHTMLView *)htmlView
       
    54 {
       
    55     self = [super init];
       
    56     if (!self)
       
    57         return nil;
       
    58     _view = view;
       
    59     _htmlView = htmlView;
       
    60     return self;
       
    61 }
       
    62 
       
    63 - (void)dealloc
       
    64 {
       
    65     [_popupWindow release];
       
    66     [_completions release];
       
    67     [_originalString release];
       
    68     
       
    69     [super dealloc];
       
    70 }
       
    71 
       
    72 - (void)_insertMatch:(NSString *)match
       
    73 {
       
    74     // FIXME: 3769654 - We should preserve case of string being inserted, even in prefix (but then also be
       
    75     // able to revert that).  Mimic NSText.
       
    76     WebFrame *frame = [_htmlView _frame];
       
    77     NSString *newText = [match substringFromIndex:prefixLength];
       
    78     [frame _replaceSelectionWithText:newText selectReplacement:YES smartReplace:NO];
       
    79 }
       
    80 
       
    81 // mostly lifted from NSTextView_KeyBinding.m
       
    82 - (void)_buildUI
       
    83 {
       
    84     NSRect scrollFrame = NSMakeRect(0, 0, 100, 100);
       
    85     NSRect tableFrame = NSZeroRect;    
       
    86     tableFrame.size = [NSScrollView contentSizeForFrameSize:scrollFrame.size hasHorizontalScroller:NO hasVerticalScroller:YES borderType:NSNoBorder];
       
    87     NSTableColumn *column = [[NSTableColumn alloc] init];
       
    88     [column setWidth:tableFrame.size.width];
       
    89     [column setEditable:NO];
       
    90     
       
    91     _tableView = [[NSTableView alloc] initWithFrame:tableFrame];
       
    92     [_tableView setAutoresizingMask:NSViewWidthSizable];
       
    93     [_tableView addTableColumn:column];
       
    94     [column release];
       
    95     [_tableView setGridStyleMask:NSTableViewGridNone];
       
    96     [_tableView setCornerView:nil];
       
    97     [_tableView setHeaderView:nil];
       
    98     [_tableView setColumnAutoresizingStyle:NSTableViewUniformColumnAutoresizingStyle];
       
    99     [_tableView setDelegate:self];
       
   100     [_tableView setDataSource:self];
       
   101     [_tableView setTarget:self];
       
   102     [_tableView setDoubleAction:@selector(tableAction:)];
       
   103     
       
   104     NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:scrollFrame];
       
   105     [scrollView setBorderType:NSNoBorder];
       
   106     [scrollView setHasVerticalScroller:YES];
       
   107     [scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
       
   108     [scrollView setDocumentView:_tableView];
       
   109     [_tableView release];
       
   110     
       
   111     _popupWindow = [[NSWindow alloc] initWithContentRect:scrollFrame styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
       
   112     [_popupWindow setAlphaValue:0.88f];
       
   113     [_popupWindow setContentView:scrollView];
       
   114     [scrollView release];
       
   115     [_popupWindow setHasShadow:YES];
       
   116     [_popupWindow setOneShot:YES];
       
   117     [_popupWindow _setForceActiveControls:YES];
       
   118     [_popupWindow setReleasedWhenClosed:NO];
       
   119 }
       
   120 
       
   121 // mostly lifted from NSTextView_KeyBinding.m
       
   122 - (void)_placePopupWindow:(NSPoint)topLeft
       
   123 {
       
   124     int numberToShow = [_completions count];
       
   125     if (numberToShow > 20)
       
   126         numberToShow = 20;
       
   127 
       
   128     NSRect windowFrame;
       
   129     NSPoint wordStart = topLeft;
       
   130     windowFrame.origin = [[_view window] convertBaseToScreen:[_htmlView convertPoint:wordStart toView:nil]];
       
   131     windowFrame.size.height = numberToShow * [_tableView rowHeight] + (numberToShow + 1) * [_tableView intercellSpacing].height;
       
   132     windowFrame.origin.y -= windowFrame.size.height;
       
   133     NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSFont systemFontOfSize:12.0f], NSFontAttributeName, nil];
       
   134     CGFloat maxWidth = 0;
       
   135     int maxIndex = -1;
       
   136     int i;
       
   137     for (i = 0; i < numberToShow; i++) {
       
   138         float width = ceilf([[_completions objectAtIndex:i] sizeWithAttributes:attributes].width);
       
   139         if (width > maxWidth) {
       
   140             maxWidth = width;
       
   141             maxIndex = i;
       
   142         }
       
   143     }
       
   144     windowFrame.size.width = 100;
       
   145     if (maxIndex >= 0) {
       
   146         maxWidth = ceilf([NSScrollView frameSizeForContentSize:NSMakeSize(maxWidth, 100.0f) hasHorizontalScroller:NO hasVerticalScroller:YES borderType:NSNoBorder].width);
       
   147         maxWidth = ceilf([NSWindow frameRectForContentRect:NSMakeRect(0.0f, 0.0f, maxWidth, 100.0f) styleMask:NSBorderlessWindowMask].size.width);
       
   148         maxWidth += 5.0f;
       
   149         windowFrame.size.width = max(maxWidth, windowFrame.size.width);
       
   150         maxWidth = min<CGFloat>(400, windowFrame.size.width);
       
   151     }
       
   152     [_popupWindow setFrame:windowFrame display:NO];
       
   153     
       
   154     [_tableView reloadData];
       
   155     [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
       
   156     [_tableView scrollRowToVisible:0];
       
   157     [self _reflectSelection];
       
   158     [_popupWindow setLevel:NSPopUpMenuWindowLevel];
       
   159     [_popupWindow orderFront:nil];    
       
   160     [[_view window] addChildWindow:_popupWindow ordered:NSWindowAbove];
       
   161 }
       
   162 
       
   163 - (void)doCompletion
       
   164 {
       
   165     if (!_popupWindow) {
       
   166         NSSpellChecker *checker = [NSSpellChecker sharedSpellChecker];
       
   167         if (!checker) {
       
   168             LOG_ERROR("No NSSpellChecker");
       
   169             return;
       
   170         }
       
   171 
       
   172         // Get preceeding word stem
       
   173         WebFrame *frame = [_htmlView _frame];
       
   174         DOMRange *selection = kit(core(frame)->selection()->toNormalizedRange().get());
       
   175         DOMRange *wholeWord = [frame _rangeByAlteringCurrentSelection:SelectionController::AlterationExtend
       
   176             direction:SelectionController::DirectionBackward granularity:WordGranularity];
       
   177         DOMRange *prefix = [wholeWord cloneRange];
       
   178         [prefix setEnd:[selection startContainer] offset:[selection startOffset]];
       
   179 
       
   180         // Reject some NOP cases
       
   181         if ([prefix collapsed]) {
       
   182             NSBeep();
       
   183             return;
       
   184         }
       
   185         NSString *prefixStr = [frame _stringForRange:prefix];
       
   186         NSString *trimmedPrefix = [prefixStr stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
       
   187         if ([trimmedPrefix length] == 0) {
       
   188             NSBeep();
       
   189             return;
       
   190         }
       
   191         prefixLength = [prefixStr length];
       
   192 
       
   193         // Lookup matches
       
   194         [_completions release];
       
   195         _completions = [checker completionsForPartialWordRange:NSMakeRange(0, [prefixStr length]) inString:prefixStr language:nil inSpellDocumentWithTag:[_view spellCheckerDocumentTag]];
       
   196         [_completions retain];
       
   197     
       
   198         if (!_completions || [_completions count] == 0) {
       
   199             NSBeep();
       
   200         } else if ([_completions count] == 1) {
       
   201             [self _insertMatch:[_completions objectAtIndex:0]];
       
   202         } else {
       
   203             ASSERT(!_originalString);       // this should only be set IFF we have a popup window
       
   204             _originalString = [[frame _stringForRange:selection] retain];
       
   205             [self _buildUI];
       
   206             NSRect wordRect = [frame _caretRectAtNode:[wholeWord startContainer] offset:[wholeWord startOffset] affinity:NSSelectionAffinityDownstream];
       
   207             // +1 to be under the word, not the caret
       
   208             // FIXME - 3769652 - Wrong positioning for right to left languages.  We should line up the upper
       
   209             // right corner with the caret instead of upper left, and the +1 would be a -1.
       
   210             NSPoint wordLowerLeft = { NSMinX(wordRect)+1, NSMaxY(wordRect) };
       
   211             [self _placePopupWindow:wordLowerLeft];
       
   212         }
       
   213     } else {
       
   214         [self endRevertingChange:YES moveLeft:NO];
       
   215     }
       
   216 }
       
   217 
       
   218 - (void)endRevertingChange:(BOOL)revertChange moveLeft:(BOOL)goLeft
       
   219 {
       
   220     if (_popupWindow) {
       
   221         // tear down UI
       
   222         [[_view window] removeChildWindow:_popupWindow];
       
   223         [_popupWindow orderOut:self];
       
   224         // Must autorelease because event tracking code may be on the stack touching UI
       
   225         [_popupWindow autorelease];
       
   226         _popupWindow = nil;
       
   227 
       
   228         if (revertChange) {
       
   229             WebFrame *frame = [_htmlView _frame];
       
   230             [frame _replaceSelectionWithText:_originalString selectReplacement:YES smartReplace:NO];
       
   231         } else if ([_htmlView _hasSelection]) {
       
   232             if (goLeft)
       
   233                 [_htmlView moveBackward:nil];
       
   234             else
       
   235                 [_htmlView moveForward:nil];
       
   236         }
       
   237         [_originalString release];
       
   238         _originalString = nil;
       
   239     }
       
   240     // else there is no state to abort if the window was not up
       
   241 }
       
   242 
       
   243 - (BOOL)popupWindowIsOpen
       
   244 {
       
   245     return _popupWindow != nil;
       
   246 }
       
   247 
       
   248 // WebHTMLView gives us a crack at key events it sees. Return whether we consumed the event.
       
   249 // The features for the various keys mimic NSTextView.
       
   250 - (BOOL)filterKeyDown:(NSEvent *)event
       
   251 {
       
   252     if (!_popupWindow)
       
   253         return NO;
       
   254     NSString *string = [event charactersIgnoringModifiers];
       
   255     if (![string length])
       
   256         return NO;
       
   257     unichar c = [string characterAtIndex:0];
       
   258     if (c == NSUpArrowFunctionKey) {
       
   259         int selectedRow = [_tableView selectedRow];
       
   260         if (0 < selectedRow) {
       
   261             [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectedRow - 1] byExtendingSelection:NO];
       
   262             [_tableView scrollRowToVisible:selectedRow - 1];
       
   263         }
       
   264         return YES;
       
   265     }
       
   266     if (c == NSDownArrowFunctionKey) {
       
   267         int selectedRow = [_tableView selectedRow];
       
   268         if (selectedRow < (int)[_completions count] - 1) {
       
   269             [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectedRow + 1] byExtendingSelection:NO];
       
   270             [_tableView scrollRowToVisible:selectedRow + 1];
       
   271         }
       
   272         return YES;
       
   273     }
       
   274     if (c == NSRightArrowFunctionKey || c == '\n' || c == '\r' || c == '\t') {
       
   275         // FIXME: What about backtab?
       
   276         [self endRevertingChange:NO moveLeft:NO];
       
   277         return YES;
       
   278     }
       
   279     if (c == NSLeftArrowFunctionKey) {
       
   280         [self endRevertingChange:NO moveLeft:YES];
       
   281         return YES;
       
   282     }
       
   283     if (c == 0x1B || c == NSF5FunctionKey) {
       
   284         // FIXME: F5?
       
   285         [self endRevertingChange:YES moveLeft:NO];
       
   286         return YES;
       
   287     }
       
   288     if (c == ' ' || c >= 0x21 && c <= 0x2F || c >= 0x3A && c <= 0x40 || c >= 0x5B && c <= 0x60 || c >= 0x7B && c <= 0x7D) {
       
   289         // FIXME: Is the above list of keys really definitive?
       
   290         // Originally this code called ispunct; aren't there other punctuation keys on international keyboards?
       
   291         [self endRevertingChange:NO moveLeft:NO];
       
   292         return NO; // let the char get inserted
       
   293     }
       
   294     return NO;
       
   295 }
       
   296 
       
   297 - (void)_reflectSelection
       
   298 {
       
   299     int selectedRow = [_tableView selectedRow];
       
   300     ASSERT(selectedRow >= 0);
       
   301     ASSERT(selectedRow < (int)[_completions count]);
       
   302     [self _insertMatch:[_completions objectAtIndex:selectedRow]];
       
   303 }
       
   304 
       
   305 - (void)tableAction:(id)sender
       
   306 {
       
   307     [self _reflectSelection];
       
   308     [self endRevertingChange:NO moveLeft:NO];
       
   309 }
       
   310 
       
   311 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
       
   312 {
       
   313     return [_completions count];
       
   314 }
       
   315 
       
   316 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
       
   317 {
       
   318     return [_completions objectAtIndex:row];
       
   319 }
       
   320 
       
   321 - (void)tableViewSelectionDidChange:(NSNotification *)notification
       
   322 {
       
   323     [self _reflectSelection];
       
   324 }
       
   325 
       
   326 @end