|
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 |
|
41 using namespace WebCore; |
|
42 using 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 NSTableColumn *column = [[NSTableColumn alloc] init]; |
|
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 CGFloat maxWidth = 0; |
|
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<CGFloat>(400, 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::AlterationExtend |
|
176 direction:SelectionController::DirectionBackward 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); |
|
301 ASSERT(selectedRow < (int)[_completions count]); |
|
302 [self _insertMatch:[_completions objectAtIndex:selectedRow]]; |
|
303 } |
|
304 |
|
305 - (void)tableAction:(id)sender |
|
306 { |
|
307 [self _reflectSelection]; |
|
308 [self endRevertingChange:NO moveLeft:NO]; |
|
309 } |
|
310 |
|
311 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView |
|
312 { |
|
313 return [_completions count]; |
|
314 } |
|
315 |
|
316 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row |
|
317 { |
|
318 return [_completions objectAtIndex:row]; |
|
319 } |
|
320 |
|
321 - (void)tableViewSelectionDidChange:(NSNotification *)notification |
|
322 { |
|
323 [self _reflectSelection]; |
|
324 } |
|
325 |
|
326 @end |