1 /**
  2  * Copyright (C) 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 core, ops, odf, gui, runtime*/
 26 
 27 /**
 28  * @constructor
 29  * @implements {core.Destroyable}
 30  * @param {!ops.Session} session
 31  * @param {!gui.SessionConstraints} sessionConstraints
 32  * @param {!gui.SessionContext} sessionContext
 33  * @param {!string} inputMemberId
 34  * @param {!odf.ObjectNameGenerator} objectNameGenerator
 35  * @param {!boolean} directTextStylingEnabled
 36  * @param {!boolean} directParagraphStylingEnabled
 37  */
 38 gui.DirectFormattingController = function DirectFormattingController(
 39     session,
 40     sessionConstraints,
 41     sessionContext,
 42     inputMemberId,
 43     objectNameGenerator,
 44     directTextStylingEnabled,
 45     directParagraphStylingEnabled
 46     ) {
 47     "use strict";
 48 
 49     var self = this,
 50         odtDocument = session.getOdtDocument(),
 51         utils = new core.Utils(),
 52         odfUtils = new odf.OdfUtils(),
 53         eventNotifier = new core.EventNotifier([
 54             gui.DirectFormattingController.enabledChanged,
 55             gui.DirectFormattingController.textStylingChanged,
 56             gui.DirectFormattingController.paragraphStylingChanged
 57         ]),
 58         /**@const*/
 59         textns = odf.Namespaces.textns,
 60         /**@const*/
 61         FILTER_ACCEPT = core.PositionFilter.FilterResult.FILTER_ACCEPT,
 62         /**@type{Object}*/
 63         directCursorStyleProperties,
 64         // cached text settings
 65         /**@type{!gui.StyleSummary}*/
 66         lastSignalledStyleSummary,
 67         /**@type {!core.LazyProperty.<!{containsText: !boolean, appliedStyles: !Array.<!Object>, styleSummary: !gui.StyleSummary}>} */
 68         selectionInfoCache,
 69         /**@type {!{directTextStyling: !boolean, directParagraphStyling: !boolean}}*/
 70         enabledFeatures = {
 71             directTextStyling: false,
 72             directParagraphStyling: false
 73         };
 74 
 75     /**
 76      * Gets the current selection information style summary
 77      * @return {!gui.StyleSummary}
 78      */
 79     function getCachedStyleSummary() {
 80         return selectionInfoCache.value().styleSummary;
 81     }
 82     
 83     /**
 84      * Fetch all the character elements and text nodes in the specified range, or if the range is collapsed, the node just to
 85      * the left of the cursor.
 86      * @param {!Range} range
 87      * @return {!Array.<!Node>}
 88      */
 89     function getNodes(range) {
 90         var container, nodes;
 91 
 92         if (range.collapsed) {
 93             container = range.startContainer;
 94             // Attempt to find the node at the specified startOffset within the startContainer.
 95             // In the case where a range starts at (parent, 1), this will mean the
 96             // style information is retrieved for the child node at index 1.
 97 
 98             // Also, need to check the length is less than the number of child nodes, as a range is
 99             // legally able to start at (parent, parent.childNodes.length).
100             if (container.hasChildNodes() && range.startOffset < container.childNodes.length) {
101                 container = container.childNodes.item(range.startOffset);
102             }
103             nodes = [container];
104         } else {
105             nodes = odfUtils.getTextElements(range, true, false);
106         }
107 
108         return nodes;
109     }
110 
111     /**
112      * Get all styles currently applied to the selected range. If the range is collapsed,
113      * this will return the style the next inserted character will have
114      * @return {!{containsText: !boolean, appliedStyles: !Array.<!Object>, styleSummary: !gui.StyleSummary}}
115      */
116     function getSelectionInfo() {
117         var cursor = odtDocument.getCursor(inputMemberId),
118             range = cursor && cursor.getSelectedRange(),
119             nodes = [],
120             selectionStyles = [],
121             selectionContainsText = true;
122 
123         if (range) {
124             nodes = getNodes(range);
125             if (nodes.length === 0) {
126                 nodes = [range.startContainer, range.endContainer];
127                 selectionContainsText = false;
128             }
129             selectionStyles = odtDocument.getFormatting().getAppliedStyles(nodes);
130         }
131 
132         if (selectionStyles[0] !== undefined && directCursorStyleProperties) {
133             // direct cursor styles add to the style of the existing range, overriding where defined
134             selectionStyles[0] = utils.mergeObjects(selectionStyles[0],
135                 /**@type {!Object}*/(directCursorStyleProperties));
136         }
137 
138         return {
139             containsText: selectionContainsText,
140             appliedStyles: selectionStyles,
141             styleSummary: new gui.StyleSummary(selectionStyles)
142         };
143     }
144 
145     /**
146      * Create a map containing all the keys that have a different value
147      * in the new summary object.
148      * @param {!Object.<string,function():*>} oldSummary
149      * @param {!Object.<string,function():*>} newSummary
150      * @return {!Object.<!string, *>}
151      */
152     function createDiff(oldSummary, newSummary) {
153         var diffMap = {};
154         Object.keys(oldSummary).forEach(function (funcName) {
155             var oldValue = oldSummary[funcName](),
156                 newValue = newSummary[funcName]();
157 
158             if (oldValue !== newValue) {
159                 diffMap[funcName] = newValue;
160             }
161         });
162         return diffMap;
163     }
164 
165     /**
166      * @return {undefined}
167      */
168     function emitStylingChanges() {
169         var textStyleDiff,
170             paragraphStyleDiff,
171             newSelectionStylesSummary = getCachedStyleSummary();
172 
173         textStyleDiff = createDiff(lastSignalledStyleSummary.text, newSelectionStylesSummary.text);
174         paragraphStyleDiff = createDiff(lastSignalledStyleSummary.paragraph, newSelectionStylesSummary.paragraph);
175 
176         lastSignalledStyleSummary = newSelectionStylesSummary;
177 
178         if (Object.keys(textStyleDiff).length > 0) {
179             eventNotifier.emit(gui.DirectFormattingController.textStylingChanged, textStyleDiff);
180         }
181 
182         if (Object.keys(paragraphStyleDiff).length > 0) {
183             eventNotifier.emit(gui.DirectFormattingController.paragraphStylingChanged, paragraphStyleDiff);
184         }
185     }
186 
187     /**
188      * @return {undefined}
189      */
190     function updateEnabledState() {
191         var newEnabledFeatures = {
192             directTextStyling: true,
193             directParagraphStyling: true
194         };
195 
196         if (sessionConstraints.getState(gui.CommonConstraints.EDIT.REVIEW_MODE) === true) {
197             newEnabledFeatures.directTextStyling = newEnabledFeatures.directParagraphStyling = /**@type{!boolean}*/(sessionContext.isLocalCursorWithinOwnAnnotation());
198         }
199 
200         if (newEnabledFeatures.directTextStyling) {
201             newEnabledFeatures.directTextStyling = selectionInfoCache.value().containsText;
202         }
203 
204         if (!(newEnabledFeatures.directTextStyling === enabledFeatures.directTextStyling
205                 && newEnabledFeatures.directParagraphStyling === enabledFeatures.directParagraphStyling)) {
206             enabledFeatures = newEnabledFeatures;
207             eventNotifier.emit(gui.DirectFormattingController.enabledChanged, enabledFeatures);
208         }
209     }
210 
211     /**
212      * @return {!{directTextStyling: !boolean, directParagraphStyling: !boolean}}
213      */
214     this.enabledFeatures = function () {
215         return enabledFeatures;
216     };
217 
218     /**
219      * @param {!ops.OdtCursor|!string} cursorOrId
220      * @return {undefined}
221      */
222     function onCursorEvent(cursorOrId) {
223         var cursorMemberId = (typeof cursorOrId === "string")
224                                 ? cursorOrId : cursorOrId.getMemberId();
225         if (cursorMemberId === inputMemberId) {
226             selectionInfoCache.reset();
227             updateEnabledState();
228         }
229     }
230 
231     /**
232      * @return {undefined}
233      */
234     function onParagraphStyleModified() {
235         // TODO: check if the cursor (selection) is actually affected
236         selectionInfoCache.reset();
237     }
238 
239     /**
240      * @param {!{paragraphElement:Element}} args
241      * @return {undefined}
242      */
243     function onParagraphChanged(args) {
244         var cursor = odtDocument.getCursor(inputMemberId),
245             p = args.paragraphElement;
246 
247         if (cursor && odtDocument.getParagraphElement(cursor.getNode()) === p) {
248             selectionInfoCache.reset();
249         }
250     }
251 
252     /**
253      * @param {!function():boolean} predicate
254      * @param {!function(!boolean):undefined} toggleMethod
255      * @return {!boolean}
256      */
257     function toggle(predicate, toggleMethod) {
258         toggleMethod(!predicate());
259         return true;
260     }
261 
262     /**
263      * Apply the supplied text properties to the current range. If no range is selected,
264      * this styling will be applied to the next character entered.
265      * @param {!Object} textProperties
266      * @return {undefined}
267      */
268     function formatTextSelection(textProperties) {
269         if (!enabledFeatures.directTextStyling) {
270             return;
271         }
272 
273         var selection = odtDocument.getCursorSelection(inputMemberId),
274             op,
275             properties = {'style:text-properties' : textProperties};
276 
277         if (selection.length !== 0) {
278             op = new ops.OpApplyDirectStyling();
279             op.init({
280                 memberid: inputMemberId,
281                 position: selection.position,
282                 length: selection.length,
283                 setProperties: properties
284             });
285             session.enqueue([op]);
286         } else {
287             // Direct styling is additive. E.g., if the user selects bold and then italic, the intent is to produce
288             // bold & italic text
289             directCursorStyleProperties = utils.mergeObjects(directCursorStyleProperties || {}, properties);
290             selectionInfoCache.reset();
291         }
292     }
293     this.formatTextSelection = formatTextSelection;
294 
295     /**
296      * @param {!string} propertyName
297      * @param {!string} propertyValue
298      * @return {undefined}
299      */
300     function applyTextPropertyToSelection(propertyName, propertyValue) {
301         var textProperties = {};
302         textProperties[propertyName] = propertyValue;
303         formatTextSelection(textProperties);
304     }
305 
306     /**
307      * Generate an operation that would apply the current direct cursor styling to the specified
308      * position and length
309      * @param {!number} position
310      * @param {!number} length
311      * @param {!boolean} useCachedStyle
312      * @return {ops.Operation}
313      */
314     this.createCursorStyleOp = function (position, length, useCachedStyle) {
315         var styleOp = null,
316             /**@type{Object.<!string,!Object>}*/
317             properties = useCachedStyle ? selectionInfoCache.value().appliedStyles[0] : directCursorStyleProperties;
318 
319         if (properties && properties['style:text-properties']) {
320             styleOp = new ops.OpApplyDirectStyling();
321             styleOp.init({
322                 memberid: inputMemberId,
323                 position: position,
324                 length: length,
325                 setProperties: {'style:text-properties': properties['style:text-properties']}
326             });
327             directCursorStyleProperties = null;
328             selectionInfoCache.reset();
329         }
330         return styleOp;
331     };
332 
333     /**
334      * Listen for local operations and clear the local cursor styling if necessary
335      * @param {!ops.Operation} op
336      */
337     function clearCursorStyle(op) {
338         var spec = op.spec();
339         if (directCursorStyleProperties && spec.memberid === inputMemberId) {
340             if (spec.optype !== "SplitParagraph") {
341                 // Most operations by the local user should clear the current cursor style
342                 // SplitParagraph is an exception because at the time the split occurs, there has been no element
343                 // added to apply the style to. Even after a split, the cursor should still style the next inserted
344                 // character
345                 directCursorStyleProperties = null;
346                 selectionInfoCache.reset();
347             }
348         }
349     }
350 
351     /**
352      * @param {!boolean} checked
353      * @return {undefined}
354      */
355     function setBold(checked) {
356         var value = checked ? 'bold' : 'normal';
357         applyTextPropertyToSelection('fo:font-weight', value);
358     }
359     this.setBold = setBold;
360 
361     /**
362      * @param {!boolean} checked
363      * @return {undefined}
364      */
365     function setItalic(checked) {
366         var value = checked ? 'italic' : 'normal';
367         applyTextPropertyToSelection('fo:font-style', value);
368     }
369     this.setItalic = setItalic;
370 
371     /**
372      * @param {!boolean} checked
373      * @return {undefined}
374      */
375     function setHasUnderline(checked) {
376         var value = checked ? 'solid' : 'none';
377         applyTextPropertyToSelection('style:text-underline-style', value);
378     }
379     this.setHasUnderline = setHasUnderline;
380 
381     /**
382      * @param {!boolean} checked
383      * @return {undefined}
384      */
385     function setHasStrikethrough(checked) {
386         var value = checked ? 'solid' : 'none';
387         applyTextPropertyToSelection('style:text-line-through-style', value);
388     }
389     this.setHasStrikethrough = setHasStrikethrough;
390 
391     /**
392      * @param {!number} value
393      * @return {undefined}
394      */
395     function setFontSize(value) {
396         applyTextPropertyToSelection('fo:font-size', value + "pt");
397     }
398     this.setFontSize = setFontSize;
399 
400     /**
401      * @param {!string} value
402      * @return {undefined}
403      */
404     function setFontName(value) {
405         applyTextPropertyToSelection('style:font-name', value);
406     }
407     this.setFontName = setFontName;
408 
409     /**
410      * Get all styles currently applied to the selected range. If the range is collapsed,
411      * this will return the style the next inserted character will have.
412      * (Note, this is not used internally by WebODF, but is provided as a convenience method
413      * for external consumers)
414      * @return {!Array.<!Object>}
415      */
416     this.getAppliedStyles = function () {
417         return selectionInfoCache.value().appliedStyles;
418     };
419 
420     /**
421      * @return {!boolean}
422      */
423     this.toggleBold = toggle.bind(self, function () { return getCachedStyleSummary().isBold(); }, setBold);
424 
425     /**
426      * @return {!boolean}
427      */
428     this.toggleItalic = toggle.bind(self, function () { return getCachedStyleSummary().isItalic(); }, setItalic);
429 
430     /**
431      * @return {!boolean}
432      */
433     this.toggleUnderline = toggle.bind(self, function () { return getCachedStyleSummary().hasUnderline(); }, setHasUnderline);
434 
435     /**
436      * @return {!boolean}
437      */
438     this.toggleStrikethrough = toggle.bind(self, function () { return getCachedStyleSummary().hasStrikeThrough(); }, setHasStrikethrough);
439 
440     /**
441      * @return {!boolean}
442      */
443     this.isBold = function () {
444         return getCachedStyleSummary().isBold();
445     };
446 
447     /**
448      * @return {!boolean}
449      */
450     this.isItalic = function () {
451         return getCachedStyleSummary().isItalic();
452     };
453 
454     /**
455      * @return {!boolean}
456      */
457     this.hasUnderline = function () {
458         return getCachedStyleSummary().hasUnderline();
459     };
460 
461     /**
462      * @return {!boolean}
463      */
464     this.hasStrikeThrough = function () {
465         return getCachedStyleSummary().hasStrikeThrough();
466     };
467 
468     /**
469      * @return {number|undefined}
470      */
471     this.fontSize = function () {
472         return getCachedStyleSummary().fontSize();
473     };
474 
475     /**
476      * @return {string|undefined}
477      */
478     this.fontName = function () {
479         return getCachedStyleSummary().fontName();
480     };
481 
482     /**
483      * @return {!boolean}
484      */
485     this.isAlignedLeft = function () {
486         return getCachedStyleSummary().isAlignedLeft();
487     };
488 
489     /**
490      * @return {!boolean}
491      */
492     this.isAlignedCenter = function () {
493         return getCachedStyleSummary().isAlignedCenter();
494     };
495 
496     /**
497      * @return {!boolean}
498      */
499     this.isAlignedRight = function () {
500         return getCachedStyleSummary().isAlignedRight();
501     };
502 
503     /**
504      * @return {!boolean}
505      */
506     this.isAlignedJustified = function () {
507         return getCachedStyleSummary().isAlignedJustified();
508     };
509 
510     /**
511      * Round the step up to the next step
512      * @param {!number} step
513      * @return {!boolean}
514      */
515     function roundUp(step) {
516         return step === ops.OdtStepsTranslator.NEXT_STEP;
517     }
518 
519     /**
520      * @param {!Object.<string,string>} obj
521      * @param {string} key
522      * @return {string|undefined}
523      */
524     function getOwnProperty(obj, key) {
525         return obj.hasOwnProperty(key) ? obj[key] : undefined;
526     }
527 
528     /**
529      * @param {!function(!Object) : !Object} applyDirectStyling
530      * @return {undefined}
531      */
532     function applyParagraphDirectStyling(applyDirectStyling) {
533         if (!enabledFeatures.directParagraphStyling) {
534             return;
535         }
536 
537         var range = odtDocument.getCursor(inputMemberId).getSelectedRange(),
538             paragraphs = odfUtils.getParagraphElements(range),
539             formatting = odtDocument.getFormatting(),
540             operations = [],
541             derivedStyleNames = {},
542             /**@type{string|undefined}*/
543             defaultStyleName;
544 
545         paragraphs.forEach(function (paragraph) {
546             var paragraphStartPoint = odtDocument.convertDomPointToCursorStep(paragraph, 0, roundUp),
547                 paragraphStyleName = paragraph.getAttributeNS(odf.Namespaces.textns, "style-name"),
548                 /**@type{string|undefined}*/
549                 newParagraphStyleName,
550                 opAddStyle,
551                 opSetParagraphStyle,
552                 paragraphProperties;
553 
554             // Try and reuse an existing paragraph style if possible
555             if (paragraphStyleName) {
556                 newParagraphStyleName = getOwnProperty(derivedStyleNames, paragraphStyleName);
557             } else {
558                 newParagraphStyleName = defaultStyleName;
559             }
560 
561             if (!newParagraphStyleName) {
562                 newParagraphStyleName = objectNameGenerator.generateStyleName();
563                 if (paragraphStyleName) {
564                     derivedStyleNames[paragraphStyleName] = newParagraphStyleName;
565                     paragraphProperties = formatting.createDerivedStyleObject(paragraphStyleName, "paragraph", {});
566                 } else {
567                     defaultStyleName = newParagraphStyleName;
568                     paragraphProperties = {};
569                 }
570 
571                 // The assumption is that applyDirectStyling will return the same transform given the same
572                 // paragraph properties (e.g., there is nothing dependent on whether this is the 10th paragraph)
573                 paragraphProperties = applyDirectStyling(paragraphProperties);
574                 opAddStyle = new ops.OpAddStyle();
575                 opAddStyle.init({
576                     memberid: inputMemberId,
577                     styleName: newParagraphStyleName.toString(),
578                     styleFamily: 'paragraph',
579                     isAutomaticStyle: true,
580                     setProperties: paragraphProperties
581                 });
582                 operations.push(opAddStyle);
583             }
584 
585 
586             opSetParagraphStyle = new ops.OpSetParagraphStyle();
587             opSetParagraphStyle.init({
588                 memberid: inputMemberId,
589                 styleName: newParagraphStyleName.toString(),
590                 position: paragraphStartPoint
591             });
592 
593             operations.push(opSetParagraphStyle);
594         });
595         session.enqueue(operations);
596     }
597 
598     /**
599      * @param {!Object} styleOverrides
600      * @return {undefined}
601      */
602     function applySimpleParagraphDirectStyling(styleOverrides) {
603         applyParagraphDirectStyling(function (paragraphStyle) { return utils.mergeObjects(paragraphStyle, styleOverrides); });
604     }
605 
606     /**
607      * @param {!string} alignment
608      * @return {undefined}
609      */
610     function alignParagraph(alignment) {
611         applySimpleParagraphDirectStyling({"style:paragraph-properties" : {"fo:text-align" : alignment}});
612     }
613 
614     /**
615      * @return {!boolean}
616      */
617     this.alignParagraphLeft = function () {
618         alignParagraph('left');
619         return true;
620     };
621 
622     /**
623      * @return {!boolean}
624      */
625     this.alignParagraphCenter = function () {
626         alignParagraph('center');
627         return true;
628     };
629 
630     /**
631      * @return {!boolean}
632      */
633     this.alignParagraphRight = function () {
634         alignParagraph('right');
635         return true;
636     };
637 
638     /**
639      * @return {!boolean}
640      */
641     this.alignParagraphJustified = function () {
642         alignParagraph('justify');
643         return true;
644     };
645 
646     /**
647      * @param {!number} direction
648      * @param {!Object.<string,Object.<string,string>>} paragraphStyle
649      * @return {!Object}
650      */
651     function modifyParagraphIndent(direction, paragraphStyle) {
652         var tabStopDistance = odtDocument.getFormatting().getDefaultTabStopDistance(),
653             paragraphProperties = paragraphStyle["style:paragraph-properties"],
654             indentValue,
655             indent,
656             newIndent;
657         if (paragraphProperties) {
658             indentValue = paragraphProperties["fo:margin-left"];
659             indent = odfUtils.parseLength(indentValue);
660         }
661 
662         if (indent && indent.unit === tabStopDistance.unit) {
663             newIndent = (indent.value + (direction * tabStopDistance.value)) + indent.unit;
664         } else {
665             // TODO unit-conversion would allow indent to work irrespective of the paragraph's indent type
666             newIndent = (direction * tabStopDistance.value) + tabStopDistance.unit;
667         }
668 
669         return utils.mergeObjects(paragraphStyle, {"style:paragraph-properties" : {"fo:margin-left" : newIndent}});
670     }
671 
672     /**
673      * @return {!boolean}
674      */
675     this.indent = function () {
676         applyParagraphDirectStyling(modifyParagraphIndent.bind(null, 1));
677         return true;
678     };
679 
680     /**
681      * @return {!boolean}
682      */
683     this.outdent = function () {
684         applyParagraphDirectStyling(modifyParagraphIndent.bind(null, -1));
685         return true;
686     };
687 
688     /**
689      * Check if the selection is at the end of the last paragraph.
690      * @param {!Range} range
691      * @param {!Node} paragraphNode
692      * @return {boolean}
693      */
694     function isSelectionAtTheEndOfLastParagraph(range, paragraphNode) {
695         var iterator = gui.SelectionMover.createPositionIterator(paragraphNode),
696             rootConstrainedFilter = new core.PositionFilterChain();
697         rootConstrainedFilter.addFilter(odtDocument.getPositionFilter());
698         rootConstrainedFilter.addFilter(odtDocument.createRootFilter(inputMemberId));
699 
700         iterator.setUnfilteredPosition(/**@type{!Node}*/(range.endContainer), range.endOffset);
701         while (iterator.nextPosition()) {
702             if (rootConstrainedFilter.acceptPosition(iterator) === FILTER_ACCEPT) {
703                 return odtDocument.getParagraphElement(iterator.getCurrentNode()) !== paragraphNode;
704             }
705         }
706         return true;
707     }
708 
709     /**
710      * Returns true if the first text node in the selection has different text style from the first paragraph; otherwise false.
711      * @param {!Range} range
712      * @param {!Node} paragraphNode
713      * @return {!boolean}
714      */
715     function isTextStyleDifferentFromFirstParagraph(range, paragraphNode) {
716         var textNodes = getNodes(range),
717             textStyle = odtDocument.getFormatting().getAppliedStyles(textNodes)[0].styleProperties,
718             paragraphStyle = odtDocument.getFormatting().getAppliedStylesForElement(paragraphNode).styleProperties;
719         if (!textStyle || textStyle['style:family'] !== 'text' || !textStyle['style:text-properties']) {
720             return false;
721         }
722         if (!paragraphStyle || !paragraphStyle['style:text-properties']) {
723             return true;
724         }
725 
726         textStyle = /**@type{!Object.<string,string>}*/(textStyle['style:text-properties']);
727         paragraphStyle = /**@type{!Object.<string,string>}*/(paragraphStyle['style:text-properties']);
728         return !Object.keys(textStyle).every(function (key) {
729             return textStyle[key] === paragraphStyle[key];
730         });
731     }
732 
733     /**
734      * TODO: HACK, REMOVE
735      * Generates operations that would create and apply the current direct cursor
736      * styling to the paragraph at given position.
737      * @param {number} position
738      * @return {!Array.<!ops.Operation>}
739      */
740     this.createParagraphStyleOps = function (position) {
741         if (!enabledFeatures.directParagraphStyling) {
742             return [];
743         }
744 
745         var cursor = odtDocument.getCursor(inputMemberId),
746             range = cursor.getSelectedRange(),
747             operations = [], op,
748             startNode, endNode, paragraphNode,
749             properties, parentStyleName, styleName;
750 
751         if (cursor.hasForwardSelection()) {
752             startNode = cursor.getAnchorNode();
753             endNode = cursor.getNode();
754         } else {
755             startNode = cursor.getNode();
756             endNode = cursor.getAnchorNode();
757         }
758 
759         paragraphNode = /**@type{!Element}*/(odtDocument.getParagraphElement(endNode));
760         runtime.assert(Boolean(paragraphNode), "DirectFormattingController: Cursor outside paragraph");
761         if (!isSelectionAtTheEndOfLastParagraph(range, paragraphNode)) {
762             return operations;
763         }
764 
765         if (endNode !== startNode) {
766             paragraphNode = /**@type{!Element}*/(odtDocument.getParagraphElement(startNode));
767         }
768 
769         if (!directCursorStyleProperties && !isTextStyleDifferentFromFirstParagraph(range, paragraphNode)) {
770             return operations;
771         }
772 
773         properties = selectionInfoCache.value().appliedStyles[0];
774         if (!properties) {
775             return operations;
776         }
777 
778         parentStyleName = paragraphNode.getAttributeNS(textns, 'style-name');
779         if (parentStyleName) {
780             properties = {
781                 'style:text-properties': properties['style:text-properties']
782             };
783             properties = odtDocument.getFormatting().createDerivedStyleObject(parentStyleName, 'paragraph', properties);
784         }
785 
786         styleName = objectNameGenerator.generateStyleName();
787         op = new ops.OpAddStyle();
788         op.init({
789             memberid: inputMemberId,
790             styleName: styleName,
791             styleFamily: 'paragraph',
792             isAutomaticStyle: true,
793             setProperties: properties
794         });
795         operations.push(op);
796 
797         op = new ops.OpSetParagraphStyle();
798         op.init({
799             memberid: inputMemberId,
800             styleName: styleName,
801             position: position
802         });
803         operations.push(op);
804 
805         return operations;
806     };
807 
808     /**
809      * @param {!string} eventid
810      * @param {!Function} cb
811      * @return {undefined}
812      */
813     this.subscribe = function (eventid, cb) {
814         eventNotifier.subscribe(eventid, cb);
815     };
816 
817     /**
818      * @param {!string} eventid
819      * @param {!Function} cb
820      * @return {undefined}
821      */
822     this.unsubscribe = function (eventid, cb) {
823         eventNotifier.unsubscribe(eventid, cb);
824     };
825 
826     /**
827      * @param {!function(!Error=)} callback passing an error object in case of error
828      * @return {undefined}
829      */
830     this.destroy = function (callback) {
831         odtDocument.unsubscribe(ops.Document.signalCursorAdded, onCursorEvent);
832         odtDocument.unsubscribe(ops.Document.signalCursorRemoved, onCursorEvent);
833         odtDocument.unsubscribe(ops.Document.signalCursorMoved, onCursorEvent);
834         odtDocument.unsubscribe(ops.OdtDocument.signalParagraphStyleModified, onParagraphStyleModified);
835         odtDocument.unsubscribe(ops.OdtDocument.signalParagraphChanged, onParagraphChanged);
836         odtDocument.unsubscribe(ops.OdtDocument.signalOperationEnd, clearCursorStyle);
837         odtDocument.unsubscribe(ops.OdtDocument.signalProcessingBatchEnd, emitStylingChanges);
838         sessionConstraints.unsubscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState);
839         callback();
840     };
841 
842     /**
843      * @return {undefined}
844      */
845     /*jslint emptyblock: true*/
846     function emptyFunction() {
847     }
848     /*jslint emptyblock: false*/
849     /**
850      * @return {!boolean}
851      */
852     function emptyFalseReturningFunction() {
853         return false;
854     }
855 
856     function init() {
857         odtDocument.subscribe(ops.Document.signalCursorAdded, onCursorEvent);
858         odtDocument.subscribe(ops.Document.signalCursorRemoved, onCursorEvent);
859         odtDocument.subscribe(ops.Document.signalCursorMoved, onCursorEvent);
860         odtDocument.subscribe(ops.OdtDocument.signalParagraphStyleModified, onParagraphStyleModified);
861         odtDocument.subscribe(ops.OdtDocument.signalParagraphChanged, onParagraphChanged);
862         odtDocument.subscribe(ops.OdtDocument.signalOperationEnd, clearCursorStyle);
863         odtDocument.subscribe(ops.OdtDocument.signalProcessingBatchEnd, emitStylingChanges);
864 
865         sessionConstraints.subscribe(gui.CommonConstraints.EDIT.REVIEW_MODE, updateEnabledState);
866         selectionInfoCache = new core.LazyProperty(getSelectionInfo);
867         lastSignalledStyleSummary = getCachedStyleSummary();
868         updateEnabledState();
869 
870         if (!directTextStylingEnabled) {
871             self.formatTextSelection = emptyFunction;
872             self.setBold = emptyFunction;
873             self.setItalic = emptyFunction;
874             self.setHasUnderline = emptyFunction;
875             self.setHasStrikethrough = emptyFunction;
876             self.setFontSize = emptyFunction;
877             self.setFontName = emptyFunction;
878             self.toggleBold = emptyFalseReturningFunction;
879             self.toggleItalic = emptyFalseReturningFunction;
880             self.toggleUnderline = emptyFalseReturningFunction;
881             self.toggleStrikethrough = emptyFalseReturningFunction;
882         }
883 
884         if (!directParagraphStylingEnabled) {
885             self.alignParagraphCenter = emptyFunction;
886             self.alignParagraphJustified = emptyFunction;
887             self.alignParagraphLeft = emptyFunction;
888             self.alignParagraphRight = emptyFunction;
889             self.createParagraphStyleOps = function () { return []; };
890             self.indent = emptyFunction;
891             self.outdent = emptyFunction;
892         }
893     }
894 
895     init();
896 };
897 
898 /**@const*/gui.DirectFormattingController.enabledChanged = "enabled/changed";
899 /**@const*/gui.DirectFormattingController.textStylingChanged = "textStyling/changed";
900 /**@const*/gui.DirectFormattingController.paragraphStylingChanged = "paragraphStyling/changed";
901