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