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