• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
41using namespace WebCore;
42
43// This class handles the complete: operation.
44// It counts on its host view to call endRevertingChange: whenever the current completion needs to be aborted.
45
46// The class is in one of two modes: Popup window showing, or not.
47// It is shown when a completion yields more than one match.
48// If a completion yields one or zero matches, it is not shown, and there is no state carried across to the next completion.
49
50@implementation WebTextCompletionController
51
52- (id)initWithWebView:(WebView *)view HTMLView:(WebHTMLView *)htmlView
53{
54    self = [super init];
55    if (!self)
56        return nil;
57    _view = view;
58    _htmlView = htmlView;
59    return self;
60}
61
62- (void)dealloc
63{
64    [_popupWindow release];
65    [_completions release];
66    [_originalString release];
67
68    [super dealloc];
69}
70
71- (void)_insertMatch:(NSString *)match
72{
73    // FIXME: 3769654 - We should preserve case of string being inserted, even in prefix (but then also be
74    // able to revert that).  Mimic NSText.
75    WebFrame *frame = [_htmlView _frame];
76    NSString *newText = [match substringFromIndex:prefixLength];
77    [frame _replaceSelectionWithText:newText selectReplacement:YES smartReplace:NO];
78}
79
80// mostly lifted from NSTextView_KeyBinding.m
81- (void)_buildUI
82{
83    NSRect scrollFrame = NSMakeRect(0, 0, 100, 100);
84    NSRect tableFrame = NSZeroRect;
85    tableFrame.size = [NSScrollView contentSizeForFrameSize:scrollFrame.size hasHorizontalScroller:NO hasVerticalScroller:YES borderType:NSNoBorder];
86    // Added cast to work around problem with multiple Foundation initWithIdentifier: methods with different parameter types.
87    NSTableColumn *column = [(NSTableColumn *)[NSTableColumn alloc] initWithIdentifier:[NSNumber numberWithInt:0]];
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    float maxWidth = 0.0f;
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(400.0f, 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::EXTEND
176            direction:SelectionController::BACKWARD 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 && selectedRow < (int)[_completions count]);
301    [self _insertMatch:[_completions objectAtIndex:selectedRow]];
302}
303
304- (void)tableAction:(id)sender
305{
306    [self _reflectSelection];
307    [self endRevertingChange:NO moveLeft:NO];
308}
309
310- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
311{
312    return [_completions count];
313}
314
315- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
316{
317    return [_completions objectAtIndex:row];
318}
319
320- (void)tableViewSelectionDidChange:(NSNotification *)notification
321{
322    [self _reflectSelection];
323}
324
325@end
326