browser/metro/base/content/contenthandlers/SelectionHandler.js

changeset 2
7e26c7da4463
equal deleted inserted replaced
-1:000000000000 0:11e896dc40a4
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 let Ci = Components.interfaces;
6 let Cc = Components.classes;
7
8 dump("### SelectionHandler.js loaded\n");
9
10 var SelectionHandler = {
11 init: function init() {
12 this.type = kContentSelector;
13 this.snap = true;
14 this.lastYPos = this.lastXPos = null;
15 addMessageListener("Browser:SelectionStart", this);
16 addMessageListener("Browser:SelectionAttach", this);
17 addMessageListener("Browser:SelectionEnd", this);
18 addMessageListener("Browser:SelectionMoveStart", this);
19 addMessageListener("Browser:SelectionMove", this);
20 addMessageListener("Browser:SelectionMoveEnd", this);
21 addMessageListener("Browser:SelectionUpdate", this);
22 addMessageListener("Browser:SelectionClose", this);
23 addMessageListener("Browser:SelectionCopy", this);
24 addMessageListener("Browser:SelectionDebug", this);
25 addMessageListener("Browser:CaretAttach", this);
26 addMessageListener("Browser:CaretMove", this);
27 addMessageListener("Browser:CaretUpdate", this);
28 addMessageListener("Browser:SelectionSwitchMode", this);
29 addMessageListener("Browser:RepositionInfoRequest", this);
30 addMessageListener("Browser:SelectionHandlerPing", this);
31 addMessageListener("Browser:ResetLastPos", this);
32 },
33
34 shutdown: function shutdown() {
35 removeMessageListener("Browser:SelectionStart", this);
36 removeMessageListener("Browser:SelectionAttach", this);
37 removeMessageListener("Browser:SelectionEnd", this);
38 removeMessageListener("Browser:SelectionMoveStart", this);
39 removeMessageListener("Browser:SelectionMove", this);
40 removeMessageListener("Browser:SelectionMoveEnd", this);
41 removeMessageListener("Browser:SelectionUpdate", this);
42 removeMessageListener("Browser:SelectionClose", this);
43 removeMessageListener("Browser:SelectionCopy", this);
44 removeMessageListener("Browser:SelectionDebug", this);
45 removeMessageListener("Browser:CaretAttach", this);
46 removeMessageListener("Browser:CaretMove", this);
47 removeMessageListener("Browser:CaretUpdate", this);
48 removeMessageListener("Browser:SelectionSwitchMode", this);
49 removeMessageListener("Browser:RepositionInfoRequest", this);
50 removeMessageListener("Browser:SelectionHandlerPing", this);
51 removeMessageListener("Browser:ResetLastPos", this);
52 },
53
54 sendAsync: function sendAsync(aMsg, aJson) {
55 sendAsyncMessage(aMsg, aJson);
56 },
57
58 /*************************************************
59 * Browser event handlers
60 */
61
62 /*
63 * Selection start event handler
64 */
65 _onSelectionStart: function _onSelectionStart(aJson) {
66 // Init content window information
67 if (!this._initTargetInfo(aJson.xPos, aJson.yPos)) {
68 this._onFail("failed to get target information");
69 return;
70 }
71
72 // for context menu select command, which doesn't trigger
73 // form input focus changes.
74 if (aJson.setFocus && this._targetIsEditable) {
75 this._targetElement.focus();
76 }
77
78 // Clear any existing selection from the document
79 let selection = this._contentWindow.getSelection();
80 selection.removeAllRanges();
81
82 // Set our initial selection, aX and aY should be in client coordinates.
83 let framePoint = this._clientPointToFramePoint({ xPos: aJson.xPos, yPos: aJson.yPos });
84 if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos,
85 Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
86 this._onFail("failed to set selection at point");
87 return;
88 }
89
90 // Update the position of our selection monocles
91 this._updateSelectionUI("start", true, true);
92 },
93
94 _onSelectionAttach: function _onSelectionAttach(aX, aY) {
95 // Init content window information
96 if (!this._initTargetInfo(aX, aY)) {
97 this._onFail("failed to get frame offset");
98 return;
99 }
100
101 // Update the position of our selection monocles
102 this._updateSelectionUI("start", true, true);
103 },
104
105 /*
106 * Switch selection modes. Currently we only support switching
107 * from "caret" to "selection".
108 */
109 _onSwitchMode: function _onSwitchMode(aMode, aMarker, aX, aY) {
110 if (aMode != "selection") {
111 this._onFail("unsupported mode switch");
112 return;
113 }
114
115 // Sanity check to be sure we are initialized
116 if (!this._targetElement) {
117 this._onFail("not initialized");
118 return;
119 }
120
121 // Only use selectAtPoint for editable content and avoid that for inputs,
122 // as we can expand caret to selection manually more precisely. We can use
123 // selectAtPoint for inputs too though, but only once bug 881938 is fully
124 // resolved.
125 if(Util.isEditableContent(this._targetElement)) {
126 // Similar to _onSelectionStart - we need to create initial selection
127 // but without the initialization bits.
128 let framePoint = this._clientPointToFramePoint({ xPos: aX, yPos: aY });
129 if (!this._domWinUtils.selectAtPoint(framePoint.xPos, framePoint.yPos,
130 Ci.nsIDOMWindowUtils.SELECT_CHARACTER)) {
131 this._onFail("failed to set selection at point");
132 return;
133 }
134 } else if (this._targetElement.selectionStart == 0 || aMarker == "end") {
135 // Expand caret forward or backward depending on direction
136 this._targetElement.selectionEnd++;
137 } else {
138 this._targetElement.selectionStart--;
139 }
140
141 // We bail if things get out of sync here implying we missed a message.
142 this._selectionMoveActive = true;
143
144 // Update the position of the selection marker that is *not*
145 // being dragged.
146 this._updateSelectionUI("update", aMarker == "end", aMarker == "start");
147 },
148
149 /*
150 * Selection monocle start move event handler
151 */
152 _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) {
153 if (!this._contentWindow) {
154 this._onFail("_onSelectionMoveStart was called without proper view set up");
155 return;
156 }
157
158 if (this._selectionMoveActive) {
159 this._onFail("mouse is already down on drag start?");
160 return;
161 }
162
163 // We bail if things get out of sync here implying we missed a message.
164 this._selectionMoveActive = true;
165
166 if (this._targetIsEditable) {
167 // If we're coming out of an out-of-bounds scroll, the node the user is
168 // trying to drag may be hidden (the monocle will be pegged to the edge
169 // of the edit). Make sure the node the user wants to move is visible
170 // and has focus.
171 this._updateInputFocus(aMsg.change);
172 }
173
174 // Update the position of our selection monocles
175 this._updateSelectionUI("update", true, true);
176 },
177
178 /*
179 * Selection monocle move event handler
180 */
181 _onSelectionMove: function _onSelectionMove(aMsg) {
182 if (!this._contentWindow) {
183 this._onFail("_onSelectionMove was called without proper view set up");
184 return;
185 }
186
187 if (!this._selectionMoveActive) {
188 this._onFail("mouse isn't down for drag move?");
189 return;
190 }
191
192 this._handleSelectionPoint(aMsg, false);
193 },
194
195 /*
196 * Selection monocle move finished event handler
197 */
198 _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) {
199 if (!this._contentWindow) {
200 this._onFail("_onSelectionMove was called without proper view set up");
201 return;
202 }
203
204 if (!this._selectionMoveActive) {
205 this._onFail("mouse isn't down for drag move?");
206 return;
207 }
208
209 this._handleSelectionPoint(aMsg, true);
210 this._selectionMoveActive = false;
211
212 // _handleSelectionPoint may set a scroll timer, so this must
213 // be reset after the last call.
214 this._clearTimers();
215
216 // Update the position of our selection monocles
217 this._updateSelectionUI("end", true, true);
218 },
219
220 /*
221 * _onCaretAttach - called by SelectionHelperUI when the user taps in a
222 * form input. Initializes SelectionHandler, updates the location of the
223 * caret, and messages back with current monocle position information.
224 *
225 * @param aX, aY tap location in client coordinates.
226 */
227 _onCaretAttach: function _onCaretAttach(aX, aY) {
228 // Init content window information
229 if (!this._initTargetInfo(aX, aY)) {
230 this._onFail("failed to get target information");
231 return;
232 }
233
234 // This should never happen, but we check to make sure
235 if (!this._targetIsEditable) {
236 this._onFail("Coordiates didn't find a text input element.");
237 return;
238 }
239
240 // Locate and sanity check the caret position
241 let selection = this._getSelection();
242 if (!selection || !selection.isCollapsed) {
243 this._onFail("No selection or selection is not collapsed.");
244 return;
245 }
246
247 // Update the position of our selection monocles
248 this._updateSelectionUI("caret", false, false, true);
249 },
250
251 /*
252 * Selection copy event handler
253 *
254 * Check to see if the incoming click was on our selection rect.
255 * if it was, copy to the clipboard. Incoming coordinates are
256 * content values.
257 */
258 _onSelectionCopy: function _onSelectionCopy(aMsg) {
259 let tap = {
260 xPos: aMsg.xPos,
261 yPos: aMsg.yPos,
262 };
263
264 let tapInSelection = (tap.xPos > this._cache.selection.left &&
265 tap.xPos < this._cache.selection.right) &&
266 (tap.yPos > this._cache.selection.top &&
267 tap.yPos < this._cache.selection.bottom);
268 // Util.dumpLn(tapInSelection,
269 // tap.xPos, tap.yPos, "|", this._cache.selection.left,
270 // this._cache.selection.right, this._cache.selection.top,
271 // this._cache.selection.bottom);
272 let success = false;
273 let selectedText = this._getSelectedText();
274 if (tapInSelection && selectedText.length) {
275 let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
276 .getService(Ci.nsIClipboardHelper);
277 clipboard.copyString(selectedText, this._contentWindow.document);
278 success = true;
279 }
280 sendSyncMessage("Content:SelectionCopied", { succeeded: success });
281 },
282
283 /*
284 * Selection close event handler
285 *
286 * @param aClearSelection requests that selection be cleared.
287 */
288 _onSelectionClose: function _onSelectionClose(aClearSelection) {
289 if (aClearSelection) {
290 this._clearSelection();
291 }
292 this.closeSelection();
293 },
294
295 /*
296 * Called any time SelectionHelperUI would like us to
297 * recalculate the selection bounds.
298 */
299 _onSelectionUpdate: function _onSelectionUpdate(aMsg) {
300 if (!this._contentWindow) {
301 this._onFail("_onSelectionUpdate was called without proper view set up");
302 return;
303 }
304
305 if (aMsg && aMsg.isInitiatedByAPZC) {
306 let {offset: offset} = Content.getCurrentWindowAndOffset(
307 this._targetCoordinates.x, this._targetCoordinates.y);
308 this._contentOffset = offset;
309 }
310
311 // Update the position of our selection monocles
312 this._updateSelectionUI("update", true, true);
313 },
314
315 /*
316 * Called if for any reason we fail during the selection
317 * process. Cancels the selection.
318 */
319 _onFail: function _onFail(aDbgMessage) {
320 if (aDbgMessage && aDbgMessage.length > 0)
321 Util.dumpLn(aDbgMessage);
322 this.sendAsync("Content:SelectionFail");
323 this._clearSelection();
324 this.closeSelection();
325 },
326
327 /*
328 * _repositionInfoRequest - fired at us by ContentAreaObserver when the
329 * soft keyboard is being displayed. CAO wants to make a decision about
330 * whether the browser deck needs repositioning.
331 */
332 _repositionInfoRequest: function _repositionInfoRequest(aJsonMsg) {
333 let result = this._calcNewContentPosition(aJsonMsg.viewHeight);
334
335 // no repositioning needed
336 if (result == 0) {
337 this.sendAsync("Content:RepositionInfoResponse", { reposition: false });
338 return;
339 }
340
341 this.sendAsync("Content:RepositionInfoResponse", {
342 reposition: true,
343 raiseContent: result,
344 });
345 },
346
347 _onPing: function _onPing(aId) {
348 this.sendAsync("Content:SelectionHandlerPong", { id: aId });
349 },
350
351 onClickCoords: function (xPos, yPos) {
352 this.lastXPos = xPos;
353 this.lastYPos = yPos;
354 },
355
356 /*************************************************
357 * Selection helpers
358 */
359
360 /*
361 * _clearSelection
362 *
363 * Clear existing selection if it exists and reset our internla state.
364 */
365 _clearSelection: function _clearSelection() {
366 this._clearTimers();
367 if (this._contentWindow) {
368 let selection = this._getSelection();
369 if (selection)
370 selection.removeAllRanges();
371 } else {
372 let selection = content.getSelection();
373 if (selection)
374 selection.removeAllRanges();
375 }
376 },
377
378 /*
379 * closeSelection
380 *
381 * Shuts SelectionHandler down.
382 */
383 closeSelection: function closeSelection() {
384 this._clearTimers();
385 this._cache = null;
386 this._contentWindow = null;
387 this._targetElement = null;
388 this._selectionMoveActive = false;
389 this._contentOffset = null;
390 this._domWinUtils = null;
391 this._targetIsEditable = false;
392 this._targetCoordinates = null;
393 sendSyncMessage("Content:HandlerShutdown", {});
394 },
395
396 /*
397 * Find content within frames - cache the target nsIDOMWindow,
398 * client coordinate offset, target element, and dom utils interface.
399 */
400 _initTargetInfo: function _initTargetInfo(aX, aY) {
401 // getCurrentWindowAndOffset takes client coordinates
402 let { element: element,
403 contentWindow: contentWindow,
404 offset: offset,
405 utils: utils } =
406 Content.getCurrentWindowAndOffset(aX, aY);
407 if (!contentWindow) {
408 return false;
409 }
410 this._targetElement = element;
411 this._contentWindow = contentWindow;
412 this._contentOffset = offset;
413 this._domWinUtils = utils;
414 this._targetIsEditable = Util.isEditable(this._targetElement);
415 this._targetCoordinates = {
416 x: aX,
417 y: aY
418 };
419
420 return true;
421 },
422
423 /*
424 * _calcNewContentPosition - calculates the distance the browser should be
425 * raised to move the focused form input out of the way of the soft
426 * keyboard.
427 *
428 * @param aNewViewHeight the new content view height
429 * @return 0 if no positioning is required or a positive val equal to the
430 * distance content should be raised to center the target element.
431 */
432 _calcNewContentPosition: function _calcNewContentPosition(aNewViewHeight) {
433 // We have no target element but the keyboard is up
434 // so lets not cover content that is below the keyboard
435 if (!this._cache || !this._cache.element) {
436 if (this.lastYPos != null && this.lastYPos > aNewViewHeight) {
437 return Services.metro.keyboardHeight;
438 }
439 return 0;
440 }
441
442 let position = Util.centerElementInView(aNewViewHeight, this._cache.element);
443 if (position !== undefined) {
444 return position;
445 }
446
447 // Special case: we are dealing with an input that is taller than the
448 // desired height of content. We need to center on the caret location.
449 let rect =
450 this._domWinUtils.sendQueryContentEvent(
451 this._domWinUtils.QUERY_CARET_RECT,
452 this._targetElement.selectionEnd,
453 0, 0, 0,
454 this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
455 if (!rect || !rect.succeeded) {
456 Util.dumpLn("no caret was present, unexpected.");
457 return 0;
458 }
459
460 // Note sendQueryContentEvent with QUERY_CARET_RECT is really buggy. If it
461 // can't find the exact location of the caret position it will "guess".
462 // Sometimes this can put the result in unexpected locations.
463 let caretLocation = Math.max(Math.min(Math.round(rect.top + (rect.height * .5)),
464 viewBottom), 0);
465
466 // Caret is above the bottom of the new view bounds, no need to shift.
467 if (caretLocation <= aNewViewHeight) {
468 return 0;
469 }
470
471 // distance from the top of the keyboard down to the caret location
472 return caretLocation - aNewViewHeight;
473 },
474
475 /*************************************************
476 * Events
477 */
478
479 /*
480 * Scroll + selection advancement timer when the monocle is
481 * outside the bounds of an input control.
482 */
483 scrollTimerCallback: function scrollTimerCallback() {
484 let result = SelectionHandler.updateTextEditSelection();
485 // Update monocle position and speed if we've dragged off to one side
486 if (result.trigger) {
487 SelectionHandler._updateSelectionUI("update", result.start, result.end);
488 }
489 },
490
491 receiveMessage: function sh_receiveMessage(aMessage) {
492 if (this._debugEvents && aMessage.name != "Browser:SelectionMove") {
493 Util.dumpLn("SelectionHandler:", aMessage.name);
494 }
495 let json = aMessage.json;
496 switch (aMessage.name) {
497 case "Browser:SelectionStart":
498 this._onSelectionStart(json);
499 break;
500
501 case "Browser:SelectionAttach":
502 this._onSelectionAttach(json.xPos, json.yPos);
503 break;
504
505 case "Browser:CaretAttach":
506 this._onCaretAttach(json.xPos, json.yPos);
507 break;
508
509 case "Browser:CaretMove":
510 this._onCaretMove(json.caret.xPos, json.caret.yPos);
511 break;
512
513 case "Browser:CaretUpdate":
514 this._onCaretPositionUpdate(json.caret.xPos, json.caret.yPos);
515 break;
516
517 case "Browser:SelectionSwitchMode":
518 this._onSwitchMode(json.newMode, json.change, json.xPos, json.yPos);
519 break;
520
521 case "Browser:SelectionClose":
522 this._onSelectionClose(json.clearSelection);
523 break;
524
525 case "Browser:SelectionMoveStart":
526 this._onSelectionMoveStart(json);
527 break;
528
529 case "Browser:SelectionMove":
530 this._onSelectionMove(json);
531 break;
532
533 case "Browser:SelectionMoveEnd":
534 this._onSelectionMoveEnd(json);
535 break;
536
537 case "Browser:SelectionCopy":
538 this._onSelectionCopy(json);
539 break;
540
541 case "Browser:SelectionDebug":
542 this._onSelectionDebug(json);
543 break;
544
545 case "Browser:SelectionUpdate":
546 this._onSelectionUpdate(json);
547 break;
548
549 case "Browser:RepositionInfoRequest":
550 // This message is sent simultaneously with a tap event.
551 // Wait a bit to make sure we have the most up-to-date tap co-ordinates
552 // before a call to _calcNewContentPosition() which accesses them.
553 content.setTimeout (function () {
554 SelectionHandler._repositionInfoRequest(json);
555 }, 50);
556 break;
557
558 case "Browser:SelectionHandlerPing":
559 this._onPing(json.id);
560 break;
561
562 case "Browser:ResetLastPos":
563 this.onClickCoords(json.xPos, json.yPos);
564 break;
565 }
566 },
567
568 /*************************************************
569 * Utilities
570 */
571
572 _getDocShell: function _getDocShell(aWindow) {
573 if (aWindow == null)
574 return null;
575 return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
576 .getInterface(Ci.nsIWebNavigation)
577 .QueryInterface(Ci.nsIDocShell);
578 },
579
580 _getSelectedText: function _getSelectedText() {
581 let selection = this._getSelection();
582 if (selection)
583 return selection.toString();
584 return "";
585 },
586
587 _getSelection: function _getSelection() {
588 if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
589 return this._targetElement
590 .QueryInterface(Ci.nsIDOMNSEditableElement)
591 .editor.selection;
592 } else if (this._contentWindow)
593 return this._contentWindow.getSelection();
594 return null;
595 },
596
597 _getSelectController: function _getSelectController() {
598 if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
599 return this._targetElement
600 .QueryInterface(Ci.nsIDOMNSEditableElement)
601 .editor.selectionController;
602 } else {
603 let docShell = this._getDocShell(this._contentWindow);
604 if (docShell == null)
605 return null;
606 return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
607 .getInterface(Ci.nsISelectionDisplay)
608 .QueryInterface(Ci.nsISelectionController);
609 }
610 },
611 };
612 this.SelectionHandler = SelectionHandler;
613
614 SelectionHandler.__proto__ = new SelectionPrototype();
615 SelectionHandler.init();
616

mercurial