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