1/** 2 * Copyright (c) 2017, CTI LOGIC 3 * Copyright (c) 2006-2017, JGraph Ltd 4 * Copyright (c) 2006-2017, Gaudenz Alder 5 * 6 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 * 10 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 11 * 12 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 13 * 14 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 15 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 16 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 17 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 18 */ 19 20function mxRuler(editorUi, unit, isVertical, isSecondery) 21{ 22 var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || 23 window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; 24 var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame; 25 26 var RULER_THICKNESS = this.RULER_THICKNESS; 27 var ruler = this; 28 this.unit = unit; 29 var style = (!Editor.isDarkMode()) ? { 30 bkgClr: '#ffffff', 31 outBkgClr: '#e8e9ed', 32 cornerClr: '#fbfbfb', 33 strokeClr: '#dadce0', 34 fontClr: '#BBBBBB', 35 guideClr: '#0000BB' 36 } : { 37 bkgClr: '#202020', 38 outBkgClr: Editor.darkColor, 39 cornerClr: Editor.darkColor, 40 strokeClr: '#505759', 41 fontClr: '#BBBBBB', 42 guideClr: '#0088cf' 43 }; 44 45 //create the container 46 var container = document.createElement('div'); 47 container.style.position = 'absolute'; 48 49 function resizeRulerContainer() 50 { 51 var diagCont = editorUi.diagramContainer; 52 53 container.style.top = (diagCont.offsetTop - RULER_THICKNESS) + 'px'; 54 container.style.left = (diagCont.offsetLeft - RULER_THICKNESS) + 'px'; 55 container.style.width = ((isVertical? 0 : diagCont.offsetWidth) + RULER_THICKNESS) + 'px'; 56 container.style.height = ((isVertical? diagCont.offsetHeight : 0) + RULER_THICKNESS) + 'px'; 57 }; 58 59 // Hook for dark mode changes 60 this.updateStyle = mxUtils.bind(this, function() 61 { 62 style = (!Editor.isDarkMode()) ? { 63 bkgClr: '#ffffff', 64 outBkgClr: '#e8e9ed', 65 cornerClr: '#fbfbfb', 66 strokeClr: '#dadce0', 67 fontClr: '#BBBBBB', 68 guideClr: '#0000BB' 69 } : { 70 bkgClr: '#202020', 71 outBkgClr: Editor.darkColor, 72 cornerClr: Editor.darkColor, 73 strokeClr: '#505759', 74 fontClr: '#BBBBBB', 75 guideClr: '#0088cf' 76 }; 77 78 container.style.background = style.bkgClr; 79 container.style[isVertical? 'borderRight' : 'borderBottom'] = '0.5px solid ' + style.strokeClr; 80 container.style.borderLeft = '0.5px solid ' + style.strokeClr; 81 }); 82 83 this.updateStyle(); 84 85 document.body.appendChild(container); 86 mxEvent.disableContextMenu(container); 87 88 this.editorUiRefresh = editorUi.refresh; 89 90 editorUi.refresh = function(minor) 91 { 92 ruler.editorUiRefresh.apply(editorUi, arguments); 93 94 resizeRulerContainer(); 95 }; 96 97 resizeRulerContainer(); 98 99 var canvas = document.createElement('canvas'); 100 //initial sizing which is corrected by the graph size event 101 canvas.width = container.offsetWidth; 102 canvas.height = container.offsetHeight; 103 container.style.overflow = 'hidden'; 104 canvas.style.position = 'relative'; 105 container.appendChild(canvas); 106 //Disable alpha to improve performance as we don't need it? 107 var ctx = canvas.getContext('2d'); 108 this.ui = editorUi; 109 var graph = editorUi.editor.graph; 110 this.graph = graph; 111 this.container = container; 112 this.canvas = canvas; 113 114 var drawLine = function (x1, y1, x2, y2, text) 115 { 116 //remove all fractions 117 x1 = Math.round(x1); y1 = Math.round(y1); x2 = Math.round(x2); y2 = Math.round(y2); 118 //adding the 0.5 is necessary to prevent anti-aliasing from making lines thicker! 119 ctx.beginPath(); 120 ctx.moveTo(x1 + 0.5, y1 + 0.5); 121 ctx.lineTo(x2 + 0.5, y2 + 0.5); 122 ctx.stroke(); 123 124 if (text) 125 { 126 if (isVertical) 127 { 128 ctx.save(); 129 ctx.translate(x1, y1); 130 ctx.rotate(-Math.PI / 2); 131 ctx.fillText(text, 0, 0); 132 ctx.restore(); 133 } 134 else 135 { 136 ctx.fillText(text, x1, y1); 137 } 138 } 139 }; 140 141 var drawRuler = function() 142 { 143 ctx.clearRect(0, 0, canvas.width, canvas.height); 144 145 ctx.beginPath(); 146 ctx.lineWidth = 0.7; 147 ctx.strokeStyle = style.strokeClr; 148 ctx.setLineDash([]); 149 ctx.font = '9px Arial'; 150 ctx.textAlign = 'center'; 151 152 var scale = graph.view.scale; 153 var bgPages = graph.view.getBackgroundPageBounds(); 154 var t = graph.view.translate; 155 var hasPageView = graph.pageVisible; 156 157 //The beginning of the ruler (zero) 158 var rStart = hasPageView? RULER_THICKNESS + (isVertical? bgPages.y - graph.container.scrollTop : bgPages.x - graph.container.scrollLeft) 159 : RULER_THICKNESS + (isVertical? t.y * scale - graph.container.scrollTop : t.x * scale - graph.container.scrollLeft); 160 161 //handle negative pages 162 var pageShift = 0; 163 164 if (hasPageView) 165 { 166 var layout = graph.getPageLayout(); 167 168 if (isVertical) 169 { 170 pageShift = layout.y * graph.pageFormat.height; 171 } 172 else 173 { 174 pageShift = layout.x * graph.pageFormat.width; 175 } 176 } 177 178 var tickStep, tickSize, len; 179 180 switch(ruler.unit) 181 { 182 case mxConstants.POINTS: 183 len = 10; 184 tickStep = 10; 185 tickSize = [3,5,5,5,5,10,5,5,5,5]; 186 break; 187 case mxConstants.MILLIMETERS: 188 len = 10; 189 tickStep = mxConstants.PIXELS_PER_MM; 190 tickSize = [5,3,3,3,3,6,3,3,3,3]; 191 break; 192 case mxConstants.METERS: 193 len = 20; 194 tickStep = mxConstants.PIXELS_PER_MM; 195 tickSize = [5,3,3,3,3,6,3,3,3,3,10,3,3,3,3,6,3,3,3,3]; 196 break; 197 case mxConstants.INCHES: 198 if (scale <=0.5 || scale >=4) 199 len = 8; 200 else 201 len = 16; 202 203 tickStep = mxConstants.PIXELS_PER_INCH / len; 204 tickSize = [5,3,5,3,7,3,5,3,7,3,5,3,7,3,5,3]; 205 break; 206 } 207 208 //Handle step size and change it with large/small scale 209 var step = tickStep; 210 211 if (scale >= 2) 212 { 213 step = tickStep / (Math.floor(scale / 2) * 2); 214 } 215 else if (scale <= 0.5) 216 { 217 step = tickStep * (Math.floor((1 / scale) / 2) * (ruler.unit == mxConstants.MILLIMETERS? 2 : 1)); 218 } 219 220 var lastTick = null; 221 222 //End of the ruler (pages end) 223 var rEnd = hasPageView? Math.min(rStart + (isVertical? bgPages.height: bgPages.width), isVertical? canvas.height : canvas.width) : (isVertical? canvas.height : canvas.width); 224 225 if (hasPageView) 226 { 227 //Clear the outside page part with a different color 228 ctx.fillStyle = style.outBkgClr; 229 230 if (isVertical) 231 { 232 var oh = rStart - RULER_THICKNESS; 233 234 if (oh > 0) 235 { 236 ctx.fillRect(0, RULER_THICKNESS, RULER_THICKNESS, oh); 237 } 238 239 if (rEnd < canvas.height) 240 { 241 ctx.fillRect(0, rEnd, RULER_THICKNESS, canvas.height); 242 } 243 } 244 else 245 { 246 var ow = rStart - RULER_THICKNESS; 247 248 if (ow > 0) 249 { 250 ctx.fillRect(RULER_THICKNESS, 0, ow, RULER_THICKNESS); 251 } 252 253 if (rEnd < canvas.width) 254 { 255 ctx.fillRect(rEnd, 0, canvas.width, RULER_THICKNESS); 256 } 257 } 258 } 259 260 //Draw ticks 261 ctx.fillStyle = style.fontClr; 262 263 for (var i = hasPageView? rStart : rStart % (step * scale); i <= rEnd; i += step * scale) 264 { 265 var current = Math.round((i - rStart) / scale / step); 266 267 if (i < RULER_THICKNESS || current == lastTick) //Prevent wasting time in drawing non-visible/duplicate lines 268 { 269 continue; 270 } 271 272 lastTick = current; 273 var text = null; 274 275 if (current % len == 0) 276 { 277 text = ruler.formatText(pageShift + current * step) + ''; 278 } 279 280 if (isVertical) 281 { 282 drawLine(RULER_THICKNESS - tickSize[Math.abs(current) % len], i, RULER_THICKNESS, i, text); 283 } 284 else 285 { 286 drawLine(i, RULER_THICKNESS - tickSize[Math.abs(current) % len], i, RULER_THICKNESS, text); 287 } 288 } 289 290 //Draw corner rect 291 ctx.lineWidth = 1; 292 drawLine(isVertical? 0 : RULER_THICKNESS, isVertical? RULER_THICKNESS : 0, RULER_THICKNESS, RULER_THICKNESS); 293 ctx.fillStyle = style.cornerClr; 294 ctx.fillRect(0, 0, RULER_THICKNESS, RULER_THICKNESS); 295 }; 296 297 var animationId = -1; 298 299 var listenersDrawRuler = function() 300 { 301 if (requestAnimationFrame != null) 302 { 303 if (cancelAnimationFrame != null) 304 { 305 cancelAnimationFrame(animationId); 306 } 307 308 animationId = requestAnimationFrame(drawRuler); 309 } 310 else 311 { 312 drawRuler(); 313 } 314 }; 315 316 var sizeListener = function() 317 { 318 var div = graph.container; 319 320 if (isVertical) 321 { 322 var newH = div.offsetHeight + RULER_THICKNESS; 323 324 if (canvas.height != newH) 325 { 326 canvas.height = newH; 327 container.style.height = newH + 'px'; 328 listenersDrawRuler(); 329 } 330 } 331 else 332 { 333 var newW = div.offsetWidth + RULER_THICKNESS; 334 335 if (canvas.width != newW) 336 { 337 canvas.width = newW; 338 container.style.width = newW + 'px'; 339 listenersDrawRuler(); 340 } 341 } 342 }; 343 344 this.drawRuler = listenersDrawRuler; 345 346 var efficientSizeListener = debounce(sizeListener, 10); 347 this.sizeListener = efficientSizeListener; 348 349 this.pageListener = function() 350 { 351 listenersDrawRuler(); 352 }; 353 354 var efficientScrollListener = debounce(function() 355 { 356 var newScroll = isVertical? graph.container.scrollTop : graph.container.scrollLeft; 357 358 if (ruler.lastScroll != newScroll) 359 { 360 ruler.lastScroll = newScroll; 361 listenersDrawRuler(); 362 } 363 }, 10); 364 365 this.scrollListener = efficientScrollListener; 366 this.unitListener = function(sender, evt) 367 { 368 ruler.setUnit(evt.getProperty('unit')); 369 }; 370 371 graph.addListener(mxEvent.SIZE, efficientSizeListener); 372 graph.container.addEventListener('scroll', efficientScrollListener); 373 graph.view.addListener('unitChanged', this.unitListener); 374 editorUi.addListener('pageViewChanged', this.pageListener); 375 editorUi.addListener('pageScaleChanged', this.pageListener); 376 editorUi.addListener('pageFormatChanged', this.pageListener); 377 378 function debounce(func, wait, immediate) 379 { 380 if (requestAnimationFrame != null) 381 { 382 return func; 383 } 384 385 var timeout; 386 return function() { 387 var context = this, args = arguments; 388 var later = function() { 389 timeout = null; 390 if (!immediate) func.apply(context, args); 391 }; 392 var callNow = immediate && !timeout; 393 clearTimeout(timeout); 394 timeout = setTimeout(later, wait); 395 if (callNow) func.apply(context, args); 396 }; 397 }; 398 399 this.setStyle = function(newStyle) 400 { 401 style = newStyle; 402 container.style.background = style.bkgClr; 403 drawRuler(); 404 } 405 406 // Showing guides on cell move 407 this.origGuideMove = mxGuide.prototype.move; 408 409 mxGuide.prototype.move = function (bounds, delta, gridEnabled, clone) 410 { 411 var ret = null; 412 413 // LATER: Fix repaint for width and height < 5 414 if ((isVertical && bounds.height > 4) || (!isVertical && bounds.width > 4)) 415 { 416 if (ruler.guidePart != null) 417 { 418 try 419 { 420 ctx.putImageData(ruler.guidePart.imgData1, ruler.guidePart.x1, ruler.guidePart.y1); 421 ctx.putImageData(ruler.guidePart.imgData2, ruler.guidePart.x2, ruler.guidePart.y2); 422 ctx.putImageData(ruler.guidePart.imgData3, ruler.guidePart.x3, ruler.guidePart.y3); 423 } 424 catch (e) 425 { 426 // ignore 427 } 428 } 429 430 ret = ruler.origGuideMove.apply(this, arguments); 431 432 try 433 { 434 var x1, y1, imgData1, x2, y2, imgData2, x3, y3, imgData3; 435 ctx.lineWidth = 0.5; 436 ctx.strokeStyle = style.guideClr; 437 ctx.setLineDash([2]); 438 439 if (isVertical) 440 { 441 y1 = bounds.y + ret.y + RULER_THICKNESS - this.graph.container.scrollTop; 442 x1 = 0; 443 y2 = y1 + bounds.height / 2; 444 x2 = RULER_THICKNESS / 2; 445 y3 = y1 + bounds.height; 446 x3 = 0; 447 imgData1 = ctx.getImageData(x1, y1 - 1, RULER_THICKNESS, 3); 448 drawLine(x1, y1, RULER_THICKNESS, y1); 449 y1--; 450 imgData2 = ctx.getImageData(x2, y2 - 1, RULER_THICKNESS, 3); 451 drawLine(x2, y2, RULER_THICKNESS, y2); 452 y2--; 453 imgData3 = ctx.getImageData(x3, y3 - 1, RULER_THICKNESS, 3); 454 drawLine(x3, y3, RULER_THICKNESS, y3); 455 y3--; 456 } 457 else 458 { 459 y1 = 0; 460 x1 = bounds.x + ret.x + RULER_THICKNESS - this.graph.container.scrollLeft; 461 y2 = RULER_THICKNESS / 2; 462 x2 = x1 + bounds.width / 2; 463 y3 = 0; 464 x3 = x1 + bounds.width; 465 imgData1 = ctx.getImageData(x1 - 1, y1, 3, RULER_THICKNESS); 466 drawLine(x1, y1, x1, RULER_THICKNESS); 467 x1--; 468 imgData2 = ctx.getImageData(x2 - 1, y2, 3, RULER_THICKNESS); 469 drawLine(x2, y2, x2, RULER_THICKNESS); 470 x2--; 471 imgData3 = ctx.getImageData(x3 - 1, y3, 3, RULER_THICKNESS); 472 drawLine(x3, y3, x3, RULER_THICKNESS); 473 x3--; 474 } 475 476 if (ruler.guidePart == null || ruler.guidePart.x1 != x1 || ruler.guidePart.y1 != y1) 477 { 478 ruler.guidePart = { 479 imgData1: imgData1, 480 x1: x1, 481 y1: y1, 482 imgData2: imgData2, 483 x2: x2, 484 y2: y2, 485 imgData3: imgData3, 486 x3: x3, 487 y3: y3 488 } 489 } 490 } 491 catch (e) 492 { 493 // ignore 494 } 495 } 496 else 497 { 498 ret = ruler.origGuideMove.apply(this, arguments); 499 } 500 501 return ret; 502 } 503 504 this.origGuideDestroy = mxGuide.prototype.destroy; 505 506 mxGuide.prototype.destroy = function() 507 { 508 var ret = ruler.origGuideDestroy.apply(this, arguments); 509 510 if (ruler.guidePart != null) 511 { 512 try 513 { 514 ctx.putImageData(ruler.guidePart.imgData1, ruler.guidePart.x1, ruler.guidePart.y1); 515 ctx.putImageData(ruler.guidePart.imgData2, ruler.guidePart.x2, ruler.guidePart.y2); 516 ctx.putImageData(ruler.guidePart.imgData3, ruler.guidePart.x3, ruler.guidePart.y3); 517 ruler.guidePart = null; 518 } 519 catch (e) 520 { 521 // ignore 522 } 523 } 524 525 return ret; 526 }; 527}; 528 529mxRuler.prototype.RULER_THICKNESS = 14; 530mxRuler.prototype.unit = mxConstants.POINTS; 531 532mxRuler.prototype.setUnit = function(unit) 533{ 534 this.unit = unit; 535 this.drawRuler(); 536}; 537 538mxRuler.prototype.formatText = function(pixels) 539{ 540 switch(this.unit) 541 { 542 case mxConstants.POINTS: 543 return Math.round(pixels); 544 case mxConstants.MILLIMETERS: 545 return (pixels / mxConstants.PIXELS_PER_MM).toFixed(1); 546 case mxConstants.METERS: 547 return (pixels / (mxConstants.PIXELS_PER_MM * 1000)).toFixed(4); 548 case mxConstants.INCHES: 549 return (pixels / mxConstants.PIXELS_PER_INCH).toFixed(2); 550 } 551}; 552 553mxRuler.prototype.destroy = function() 554{ 555 this.ui.refresh = this.editorUiRefresh; 556 mxGuide.prototype.move = this.origGuideMove; 557 mxGuide.prototype.destroy = this.origGuideDestroy; 558 this.graph.removeListener(this.sizeListener); 559 this.graph.container.removeEventListener('scroll', this.scrollListener); 560 this.graph.view.removeListener('unitChanged', this.unitListener); 561 this.ui.removeListener('pageViewChanged', this.pageListener); 562 this.ui.removeListener('pageScaleChanged', this.pageListener); 563 this.ui.removeListener('pageFormatChanged', this.pageListener); 564 565 if (this.container != null) 566 { 567 this.container.parentNode.removeChild(this.container); 568 } 569}; 570 571function mxDualRuler(editorUi, unit) 572{ 573 var rulerOffset = new mxPoint(mxRuler.prototype.RULER_THICKNESS, mxRuler.prototype.RULER_THICKNESS); 574 this.editorUiGetDiagContOffset = editorUi.getDiagramContainerOffset; 575 576 editorUi.getDiagramContainerOffset = function() 577 { 578 return rulerOffset; 579 }; 580 581 this.editorUiRefresh = editorUi.refresh; 582 this.ui = editorUi; 583 this.origGuideMove = mxGuide.prototype.move; 584 this.origGuideDestroy = mxGuide.prototype.destroy; 585 586 this.vRuler = new mxRuler(editorUi, unit, true); 587 this.hRuler = new mxRuler(editorUi, unit, false, true); 588 589 // Adds units context menu 590 var installMenu = mxUtils.bind(this, function(node) 591 { 592 var menuWasVisible = false; 593 594 mxEvent.addGestureListeners(node, mxUtils.bind(this, function(evt) 595 { 596 menuWasVisible = editorUi.currentMenu != null; 597 mxEvent.consume(evt); 598 }), null, mxUtils.bind(this, function(evt) 599 { 600 if (editorUi.editor.graph.isEnabled() && !editorUi.editor.graph.isMouseDown && 601 (mxEvent.isTouchEvent(evt) || mxEvent.isPopupTrigger(evt))) 602 { 603 editorUi.editor.graph.popupMenuHandler.hideMenu(); 604 editorUi.hideCurrentMenu(); 605 606 if (!mxEvent.isTouchEvent(evt) || !menuWasVisible) 607 { 608 var menu = new mxPopupMenu(mxUtils.bind(this, function(menu, parent) 609 { 610 editorUi.menus.addMenuItems(menu, ['points', 'inches', 'millimeters', 'meters'], parent); 611 })); 612 613 menu.div.className += ' geMenubarMenu'; 614 menu.smartSeparators = true; 615 menu.showDisabled = true; 616 menu.autoExpand = true; 617 618 // Disables autoexpand and destroys menu when hidden 619 menu.hideMenu = mxUtils.bind(this, function() 620 { 621 mxPopupMenu.prototype.hideMenu.apply(menu, arguments); 622 editorUi.resetCurrentMenu(); 623 menu.destroy(); 624 }); 625 626 var x = mxEvent.getClientX(evt); 627 var y = mxEvent.getClientY(evt); 628 menu.popup(x, y, null, evt); 629 editorUi.setCurrentMenu(menu, node); 630 } 631 632 mxEvent.consume(evt); 633 } 634 })); 635 }); 636 637 installMenu(this.hRuler.container); 638 installMenu(this.vRuler.container); 639 640 this.vRuler.drawRuler(); 641 this.hRuler.drawRuler(); 642}; 643 644mxDualRuler.prototype.updateStyle = function() 645{ 646 this.vRuler.updateStyle(); 647 this.hRuler.updateStyle(); 648 this.vRuler.drawRuler(); 649 this.hRuler.drawRuler(); 650}; 651 652mxDualRuler.prototype.setUnit = function(unit) 653{ 654 this.vRuler.setUnit(unit); 655 this.hRuler.setUnit(unit); 656}; 657 658mxDualRuler.prototype.setStyle = function(newStyle) 659{ 660 this.vRuler.setStyle(newStyle); 661 this.hRuler.setStyle(newStyle); 662} 663 664mxDualRuler.prototype.destroy = function() 665{ 666 this.vRuler.destroy(); 667 this.hRuler.destroy(); 668 this.ui.refresh = this.editorUiRefresh; 669 mxGuide.prototype.move = this.origGuideMove; 670 mxGuide.prototype.destroy = this.origGuideDestroy; 671 this.ui.getDiagramContainerOffset = this.editorUiGetDiagContOffset; 672};