1 /**
  2  * Copyright (C) 2012-2013 KO GmbH <copyright@kogmbh.com>
  3  *
  4  * @licstart
  5  * This file is part of WebODF.
  6  *
  7  * WebODF is free software: you can redistribute it and/or modify it
  8  * under the terms of the GNU Affero General Public License (GNU AGPL)
  9  * as published by the Free Software Foundation, either version 3 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * WebODF is distributed in the hope that it will be useful, but
 13  * WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU Affero General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Affero General Public License
 18  * along with WebODF.  If not, see <http://www.gnu.org/licenses/>.
 19  * @licend
 20  *
 21  * @source: http://www.webodf.org/
 22  * @source: https://github.com/kogmbh/WebODF/
 23  */
 24 
 25 /*global runtime, core, gui, ops*/
 26 
 27 
 28 /**
 29  * The caret manager is responsible for creating a caret as UI representation
 30  * of a member's cursor.
 31  * If the caret is for the local member, then the manager will control the
 32  * caret's current focus, and ensure the caret stays visible after every local
 33  * operation.
 34  * @constructor
 35  * @implements {core.Destroyable}
 36  * @param {!gui.SessionController} sessionController
 37  * @param {!gui.Viewport} viewport
 38  */
 39 gui.CaretManager = function CaretManager(sessionController, viewport) {
 40     "use strict";
 41     var /**@type{!Object.<string,!gui.Caret>}*/
 42         carets = {},
 43         window = runtime.getWindow(),
 44         odtDocument = sessionController.getSession().getOdtDocument(),
 45         eventManager = sessionController.getEventManager();
 46 
 47     /**
 48      * @param {!string} memberId
 49      * @return {?gui.Caret}
 50      */
 51     function getCaret(memberId) {
 52         return carets.hasOwnProperty(memberId) ? carets[memberId] : null;
 53     }
 54 
 55     /**
 56      * Get the horizontal offset of the local caret from the
 57      * left edge of the screen (in pixels).
 58      * @return {!number|undefined}
 59      */
 60     function getLocalCaretXOffsetPx() {
 61         var localCaret = getCaret(sessionController.getInputMemberId()),
 62             lastRect;
 63         if (localCaret) {
 64             lastRect = localCaret.getBoundingClientRect();
 65         }
 66         // usually the rect is 1px width, so rect.left ~= rect.right.
 67         // Right is used because during IME composition the caret width includes
 68         // the chars being composed. The caret is *always* flush against the right side
 69         // of the it's BCR.
 70         return lastRect ? lastRect.right : undefined;
 71     }
 72 
 73     /**
 74      * @return {!Array.<!gui.Caret>}
 75      */
 76     function getCarets() {
 77         return Object.keys(carets).map(function (memberid) {
 78             return carets[memberid];
 79         });
 80     }
 81 
 82     /**
 83      * @param {!string} memberId
 84      * @return {undefined}
 85      */
 86     function removeCaret(memberId) {
 87         var caret = carets[memberId];
 88         if (caret) {
 89             // Remove the caret before destroying it in case the destroy function causes new window/webodf events to be
 90             // triggered. This ensures the caret can't receive any new events once destroy has been invoked
 91             delete carets[memberId];
 92             if (memberId === sessionController.getInputMemberId()) {
 93                 odtDocument.unsubscribe(ops.OdtDocument.signalProcessingBatchEnd, caret.ensureVisible);
 94                 odtDocument.unsubscribe(ops.Document.signalCursorMoved, caret.refreshCursorBlinking);
 95 
 96                 eventManager.unsubscribe("compositionupdate", caret.handleUpdate);
 97                 eventManager.unsubscribe("compositionend", caret.handleUpdate);
 98                 eventManager.unsubscribe("focus", caret.setFocus);
 99                 eventManager.unsubscribe("blur", caret.removeFocus);
100 
101                 window.removeEventListener("focus", caret.show, false);
102                 window.removeEventListener("blur", caret.hide, false);
103             } else {
104                 odtDocument.unsubscribe(ops.OdtDocument.signalProcessingBatchEnd, caret.handleUpdate);
105             }
106             /*jslint emptyblock:true*/
107             caret.destroy(function() {});
108             /*jslint emptyblock:false*/
109         }
110     }
111 
112     /**
113      * @param {!ops.OdtCursor} cursor
114      * @param {!boolean} caretAvatarInitiallyVisible  Set to false to hide the associated avatar
115      * @param {!boolean} blinkOnRangeSelect  Specify that the caret should blink if a non-collapsed range is selected
116      * @return {!gui.Caret}
117      */
118     this.registerCursor = function (cursor, caretAvatarInitiallyVisible, blinkOnRangeSelect) {
119         var memberid = cursor.getMemberId(),
120             caret = new gui.Caret(cursor, viewport, caretAvatarInitiallyVisible, blinkOnRangeSelect);
121 
122         carets[memberid] = caret;
123 
124         // if local input member, then let controller listen on caret span
125         if (memberid === sessionController.getInputMemberId()) {
126             runtime.log("Starting to track input on new cursor of " + memberid);
127             odtDocument.subscribe(ops.OdtDocument.signalProcessingBatchEnd, caret.ensureVisible);
128             odtDocument.subscribe(ops.Document.signalCursorMoved, caret.refreshCursorBlinking);
129 
130             eventManager.subscribe("compositionupdate", caret.handleUpdate);
131             eventManager.subscribe("compositionend", caret.handleUpdate);
132             eventManager.subscribe("focus", caret.setFocus);
133             eventManager.subscribe("blur", caret.removeFocus);
134 
135             window.addEventListener("focus", caret.show, false);
136             window.addEventListener("blur", caret.hide, false);
137 
138             // Add event trap as an overlay element to the caret
139             caret.setOverlayElement(eventManager.getEventTrap());
140         } else {
141             odtDocument.subscribe(ops.OdtDocument.signalProcessingBatchEnd, caret.handleUpdate);
142         }
143 
144         return caret;
145     };
146 
147     /**
148      * @param {!string} memberId
149      * @return {?gui.Caret}
150      */
151     this.getCaret = getCaret;
152 
153     /**
154      * @return {!Array.<!gui.Caret>}
155      */
156     this.getCarets = getCarets;
157 
158     /**
159      * @param {!function(!Error=)} callback, passing an error object in case of error
160      * @return {undefined}
161      */
162     this.destroy = function (callback) {
163         var caretCleanup = getCarets().map(function(caret) { return caret.destroy; });
164 
165         sessionController.getSelectionController().setCaretXPositionLocator(null);
166         odtDocument.unsubscribe(ops.Document.signalCursorRemoved, removeCaret);
167         carets = {};
168         core.Async.destroyAll(caretCleanup, callback);
169     };
170 
171     function init() {
172         sessionController.getSelectionController().setCaretXPositionLocator(getLocalCaretXOffsetPx);
173         odtDocument.subscribe(ops.Document.signalCursorRemoved, removeCaret);
174     }
175 
176     init();
177 };
178