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