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