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