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; 42using 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 // Added cast to work around problem with multiple Foundation initWithIdentifier: methods with different parameter types. 88 NSTableColumn *column = [(NSTableColumn *)[NSTableColumn alloc] initWithIdentifier:[NSNumber numberWithInt:0]]; 89 [column setWidth:tableFrame.size.width]; 90 [column setEditable:NO]; 91 92 _tableView = [[NSTableView alloc] initWithFrame:tableFrame]; 93 [_tableView setAutoresizingMask:NSViewWidthSizable]; 94 [_tableView addTableColumn:column]; 95 [column release]; 96 [_tableView setGridStyleMask:NSTableViewGridNone]; 97 [_tableView setCornerView:nil]; 98 [_tableView setHeaderView:nil]; 99 [_tableView setColumnAutoresizingStyle:NSTableViewUniformColumnAutoresizingStyle]; 100 [_tableView setDelegate:self]; 101 [_tableView setDataSource:self]; 102 [_tableView setTarget:self]; 103 [_tableView setDoubleAction:@selector(tableAction:)]; 104 105 NSScrollView *scrollView = [[NSScrollView alloc] initWithFrame:scrollFrame]; 106 [scrollView setBorderType:NSNoBorder]; 107 [scrollView setHasVerticalScroller:YES]; 108 [scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; 109 [scrollView setDocumentView:_tableView]; 110 [_tableView release]; 111 112 _popupWindow = [[NSWindow alloc] initWithContentRect:scrollFrame styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; 113 [_popupWindow setAlphaValue:0.88f]; 114 [_popupWindow setContentView:scrollView]; 115 [scrollView release]; 116 [_popupWindow setHasShadow:YES]; 117 [_popupWindow setOneShot:YES]; 118 [_popupWindow _setForceActiveControls:YES]; 119 [_popupWindow setReleasedWhenClosed:NO]; 120} 121 122// mostly lifted from NSTextView_KeyBinding.m 123- (void)_placePopupWindow:(NSPoint)topLeft 124{ 125 int numberToShow = [_completions count]; 126 if (numberToShow > 20) 127 numberToShow = 20; 128 129 NSRect windowFrame; 130 NSPoint wordStart = topLeft; 131 windowFrame.origin = [[_view window] convertBaseToScreen:[_htmlView convertPoint:wordStart toView:nil]]; 132 windowFrame.size.height = numberToShow * [_tableView rowHeight] + (numberToShow + 1) * [_tableView intercellSpacing].height; 133 windowFrame.origin.y -= windowFrame.size.height; 134 NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSFont systemFontOfSize:12.0f], NSFontAttributeName, nil]; 135 CGFloat maxWidth = 0; 136 int maxIndex = -1; 137 int i; 138 for (i = 0; i < numberToShow; i++) { 139 float width = ceilf([[_completions objectAtIndex:i] sizeWithAttributes:attributes].width); 140 if (width > maxWidth) { 141 maxWidth = width; 142 maxIndex = i; 143 } 144 } 145 windowFrame.size.width = 100; 146 if (maxIndex >= 0) { 147 maxWidth = ceilf([NSScrollView frameSizeForContentSize:NSMakeSize(maxWidth, 100.0f) hasHorizontalScroller:NO hasVerticalScroller:YES borderType:NSNoBorder].width); 148 maxWidth = ceilf([NSWindow frameRectForContentRect:NSMakeRect(0.0f, 0.0f, maxWidth, 100.0f) styleMask:NSBorderlessWindowMask].size.width); 149 maxWidth += 5.0f; 150 windowFrame.size.width = max(maxWidth, windowFrame.size.width); 151 maxWidth = min<CGFloat>(400, windowFrame.size.width); 152 } 153 [_popupWindow setFrame:windowFrame display:NO]; 154 155 [_tableView reloadData]; 156 [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO]; 157 [_tableView scrollRowToVisible:0]; 158 [self _reflectSelection]; 159 [_popupWindow setLevel:NSPopUpMenuWindowLevel]; 160 [_popupWindow orderFront:nil]; 161 [[_view window] addChildWindow:_popupWindow ordered:NSWindowAbove]; 162} 163 164- (void)doCompletion 165{ 166 if (!_popupWindow) { 167 NSSpellChecker *checker = [NSSpellChecker sharedSpellChecker]; 168 if (!checker) { 169 LOG_ERROR("No NSSpellChecker"); 170 return; 171 } 172 173 // Get preceeding word stem 174 WebFrame *frame = [_htmlView _frame]; 175 DOMRange *selection = kit(core(frame)->selection()->toNormalizedRange().get()); 176 DOMRange *wholeWord = [frame _rangeByAlteringCurrentSelection:SelectionController::EXTEND 177 direction:SelectionController::BACKWARD granularity:WordGranularity]; 178 DOMRange *prefix = [wholeWord cloneRange]; 179 [prefix setEnd:[selection startContainer] offset:[selection startOffset]]; 180 181 // Reject some NOP cases 182 if ([prefix collapsed]) { 183 NSBeep(); 184 return; 185 } 186 NSString *prefixStr = [frame _stringForRange:prefix]; 187 NSString *trimmedPrefix = [prefixStr stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 188 if ([trimmedPrefix length] == 0) { 189 NSBeep(); 190 return; 191 } 192 prefixLength = [prefixStr length]; 193 194 // Lookup matches 195 [_completions release]; 196 _completions = [checker completionsForPartialWordRange:NSMakeRange(0, [prefixStr length]) inString:prefixStr language:nil inSpellDocumentWithTag:[_view spellCheckerDocumentTag]]; 197 [_completions retain]; 198 199 if (!_completions || [_completions count] == 0) { 200 NSBeep(); 201 } else if ([_completions count] == 1) { 202 [self _insertMatch:[_completions objectAtIndex:0]]; 203 } else { 204 ASSERT(!_originalString); // this should only be set IFF we have a popup window 205 _originalString = [[frame _stringForRange:selection] retain]; 206 [self _buildUI]; 207 NSRect wordRect = [frame _caretRectAtNode:[wholeWord startContainer] offset:[wholeWord startOffset] affinity:NSSelectionAffinityDownstream]; 208 // +1 to be under the word, not the caret 209 // FIXME - 3769652 - Wrong positioning for right to left languages. We should line up the upper 210 // right corner with the caret instead of upper left, and the +1 would be a -1. 211 NSPoint wordLowerLeft = { NSMinX(wordRect)+1, NSMaxY(wordRect) }; 212 [self _placePopupWindow:wordLowerLeft]; 213 } 214 } else { 215 [self endRevertingChange:YES moveLeft:NO]; 216 } 217} 218 219- (void)endRevertingChange:(BOOL)revertChange moveLeft:(BOOL)goLeft 220{ 221 if (_popupWindow) { 222 // tear down UI 223 [[_view window] removeChildWindow:_popupWindow]; 224 [_popupWindow orderOut:self]; 225 // Must autorelease because event tracking code may be on the stack touching UI 226 [_popupWindow autorelease]; 227 _popupWindow = nil; 228 229 if (revertChange) { 230 WebFrame *frame = [_htmlView _frame]; 231 [frame _replaceSelectionWithText:_originalString selectReplacement:YES smartReplace:NO]; 232 } else if ([_htmlView _hasSelection]) { 233 if (goLeft) 234 [_htmlView moveBackward:nil]; 235 else 236 [_htmlView moveForward:nil]; 237 } 238 [_originalString release]; 239 _originalString = nil; 240 } 241 // else there is no state to abort if the window was not up 242} 243 244- (BOOL)popupWindowIsOpen 245{ 246 return _popupWindow != nil; 247} 248 249// WebHTMLView gives us a crack at key events it sees. Return whether we consumed the event. 250// The features for the various keys mimic NSTextView. 251- (BOOL)filterKeyDown:(NSEvent *)event 252{ 253 if (!_popupWindow) 254 return NO; 255 NSString *string = [event charactersIgnoringModifiers]; 256 if (![string length]) 257 return NO; 258 unichar c = [string characterAtIndex:0]; 259 if (c == NSUpArrowFunctionKey) { 260 int selectedRow = [_tableView selectedRow]; 261 if (0 < selectedRow) { 262 [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectedRow - 1] byExtendingSelection:NO]; 263 [_tableView scrollRowToVisible:selectedRow - 1]; 264 } 265 return YES; 266 } 267 if (c == NSDownArrowFunctionKey) { 268 int selectedRow = [_tableView selectedRow]; 269 if (selectedRow < (int)[_completions count] - 1) { 270 [_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:selectedRow + 1] byExtendingSelection:NO]; 271 [_tableView scrollRowToVisible:selectedRow + 1]; 272 } 273 return YES; 274 } 275 if (c == NSRightArrowFunctionKey || c == '\n' || c == '\r' || c == '\t') { 276 // FIXME: What about backtab? 277 [self endRevertingChange:NO moveLeft:NO]; 278 return YES; 279 } 280 if (c == NSLeftArrowFunctionKey) { 281 [self endRevertingChange:NO moveLeft:YES]; 282 return YES; 283 } 284 if (c == 0x1B || c == NSF5FunctionKey) { 285 // FIXME: F5? 286 [self endRevertingChange:YES moveLeft:NO]; 287 return YES; 288 } 289 if (c == ' ' || c >= 0x21 && c <= 0x2F || c >= 0x3A && c <= 0x40 || c >= 0x5B && c <= 0x60 || c >= 0x7B && c <= 0x7D) { 290 // FIXME: Is the above list of keys really definitive? 291 // Originally this code called ispunct; aren't there other punctuation keys on international keyboards? 292 [self endRevertingChange:NO moveLeft:NO]; 293 return NO; // let the char get inserted 294 } 295 return NO; 296} 297 298- (void)_reflectSelection 299{ 300 int selectedRow = [_tableView selectedRow]; 301 ASSERT(selectedRow >= 0); 302 ASSERT(selectedRow < (int)[_completions count]); 303 [self _insertMatch:[_completions objectAtIndex:selectedRow]]; 304} 305 306- (void)tableAction:(id)sender 307{ 308 [self _reflectSelection]; 309 [self endRevertingChange:NO moveLeft:NO]; 310} 311 312- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView 313{ 314 return [_completions count]; 315} 316 317- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row 318{ 319 return [_completions objectAtIndex:row]; 320} 321 322- (void)tableViewSelectionDidChange:(NSNotification *)notification 323{ 324 [self _reflectSelection]; 325} 326 327@end 328