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