1 /* See license.txt for terms of usage */ 2 3 FBL.ns(function() { with (FBL) { 4 5 6 7 /* Defines the API for SourceBoxDecorator and provides the default implementation. 8 * Decorators are passed the source box on construction, called to create the HTML, 9 * and called whenever the user scrolls the view. 10 */ 11 Firebug.SourceBoxDecorator = function(sourceBox){} 12 13 Firebug.SourceBoxDecorator.sourceBoxCounter = 0; 14 15 Firebug.SourceBoxDecorator.prototype = 16 { 17 onSourceBoxCreation: function(sourceBox) 18 { 19 // allow panel-document unique ids to be generated for lines. 20 sourceBox.uniqueId = ++Firebug.SourceBoxDecorator.sourceBoxCounter; 21 }, 22 /* called on a delay after the view port is updated, eg vertical scroll 23 * The sourceBox will contain lines from firstRenderedLine to lastRenderedLine 24 * The user will be able to see sourceBox.firstViewableLine to sourceBox.lastViewableLine 25 */ 26 decorate: function(sourceBox, sourceFile) 27 { 28 return; 29 }, 30 31 /* called once as each line is being rendered. 32 * @param lineNo integer 1-maxLineNumbers 33 */ 34 getUserVisibleLineNumber: function(sourceBox, lineNo) 35 { 36 return lineNo; 37 }, 38 39 /* call once as each line is being rendered. 40 * @param lineNo integer 1-maxLineNumbers 41 */ 42 getLineHTML: function(sourceBox, lineNo) 43 { 44 var html = escapeForSourceLine(sourceBox.lines[lineNo-1]); 45 46 // If the pref says so, replace tabs by corresponding number of spaces. 47 if (Firebug.replaceTabs > 0) 48 { 49 var space = new Array(Firebug.replaceTabs + 1).join(" "); 50 html = html.replace(/\t/g, space); 51 } 52 53 return html; 54 }, 55 56 /* 57 * @return a string unique to the sourcebox and line number, valid in getElementById() 58 */ 59 getLineId: function(sourceBox, lineNo) 60 { 61 return 'sb' + sourceBox.uniqueId + '-L' + lineNo; 62 }, 63 } 64 65 66 /* 67 * @panel Firebug.SourceBoxPanel: Intermediate level class for showing lines of source, eg Script Panel 68 * Implements a 'viewport' to render only the lines the user is viewing or has recently viewed. 69 * Scroll events or scrollToLine calls are converted to viewableRange line number range. 70 * The range of lines is rendered, skipping any that have already been rendered. Then if the 71 * new line range overlaps the old line range, done; else delete the old range. 72 * That way the lines kept contiguous. 73 * The rendering details are delegated to SourceBoxDecorator; each source line may be expanded into 74 * more rendered lines. 75 */ 76 77 Firebug.SourceBoxPanel = function() {}; 78 /* @lends */ 79 Firebug.SourceBoxPanel = extend( extend(Firebug.MeasureBox, Firebug.ActivablePanel), 80 { 81 82 initialize: function(context, doc) 83 { 84 this.onResize = bind(this.resizer, this); 85 86 this.sourceBoxes = {}; 87 this.decorator = this.getDecorator(); 88 Firebug.Panel.initialize.apply(this, arguments); 89 }, 90 91 initializeNode: function(panelNode) 92 { 93 this.resizeEventTarget = Firebug.chrome.$('fbContentBox'); 94 this.resizeEventTarget.addEventListener("resize", this.onResize, true); 95 }, 96 97 reattach: function(doc) 98 { 99 var oldEventTarget = this.resizeEventTarget; 100 oldEventTarget.removeEventListener("resize", this.onResize, true); 101 Firebug.Panel.reattach.apply(this, arguments); 102 this.resizeEventTarget = Firebug.chrome.$('fbContentBox'); 103 this.resizeEventTarget.addEventListener("resize", this.onResize, true); 104 }, 105 106 destroyNode: function() 107 { 108 Firebug.Panel.destroyNode.apply(this, arguments); 109 this.resizeEventTarget.removeEventListener("resize", this.onResize, true); 110 }, 111 112 // ************************************** 113 /* Panel extension point. 114 * Called just before box is shown 115 */ 116 updateSourceBox: function(sourceBox) 117 { 118 119 }, 120 121 /* Panel extension point. Called on panel initialization 122 * @return Must implement SourceBoxDecorator API. 123 */ 124 getDecorator: function() 125 { 126 return new Firebug.SourceBoxDecorator(); 127 }, 128 129 /* Panel extension point 130 * @return string eg "js" or "css" 131 */ 132 getSourceType: function() 133 { 134 throw "Need to override in extender"; 135 }, 136 137 // ************************************** 138 disablePanel: function(module) 139 { 140 this.sourceBoxes = {}; // clear so we start fresh if enabled 141 Firebug.ActivablePanel.disablePanel.apply(this, arguments); 142 }, 143 144 getSourceLinesFrom: function(selection) 145 { 146 // https://developer.mozilla.org/en/DOM/Selection 147 if (selection.isCollapsed) 148 return ""; 149 150 var anchorSourceRow = getAncestorByClass(selection.anchorNode, "sourceRow"); 151 var focusSourceRow = getAncestorByClass(selection.focusNode, "sourceRow"); 152 if (anchorSourceRow == focusSourceRow) 153 { 154 return selection.toString();// trivial case 155 } 156 var buf = this.getSourceLine(anchorSourceRow, selection.anchorOffset); 157 158 var currentSourceRow = anchorSourceRow.nextSibling; 159 while(currentSourceRow && (currentSourceRow != focusSourceRow) && hasClass(currentSourceRow, "sourceRow")) 160 { 161 buf += this.getSourceLine(currentSourceRow); 162 currentSourceRow = currentSourceRow.nextSibling; 163 } 164 buf += this.getSourceLine(focusSourceRow, 0, selection.focusOffset); 165 return buf; 166 }, 167 168 getSourceLine: function(sourceRow, beginOffset, endOffset) 169 { 170 var source = getChildByClass(sourceRow, "sourceRowText").textContent; 171 if (endOffset) 172 source = source.substring(beginOffset, endOffset); 173 else if (beginOffset) 174 source = source.substring(beginOffset); 175 else 176 source = source; 177 178 return source; 179 }, 180 181 // **************************************************************************************** 182 183 getSourceBoxBySourceFile: function(sourceFile) 184 { 185 if (sourceFile.href) 186 { 187 var sourceBox = this.getSourceBoxByURL(sourceFile.href); 188 if (sourceBox && sourceBox.repObject == sourceFile) 189 return sourceBox; 190 else 191 return null; // cause a new one to be created 192 } 193 }, 194 195 getSourceBoxByURL: function(url) 196 { 197 return url ? this.sourceBoxes[url] : null; 198 }, 199 200 renameSourceBox: function(oldURL, newURL) 201 { 202 var sourceBox = this.sourceBoxes[oldURL]; 203 if (sourceBox) 204 { 205 delete this.sourceBoxes[oldURL]; 206 this.sourceBoxes[newURL] = sourceBox; 207 } 208 }, 209 210 showSourceFile: function(sourceFile) 211 { 212 var sourceBox = this.getSourceBoxBySourceFile(sourceFile); 213 if (FBTrace.DBG_SOURCEFILES) 214 FBTrace.sysout("firebug.showSourceFile: "+sourceFile, sourceBox); 215 if (!sourceBox) 216 sourceBox = this.createSourceBox(sourceFile); 217 218 this.showSourceBox(sourceBox); 219 }, 220 221 showSourceBox: function(sourceBox) 222 { 223 if (this.selectedSourceBox) 224 collapse(this.selectedSourceBox, true); 225 226 this.selectedSourceBox = sourceBox; 227 delete this.currentSearch; 228 229 if (sourceBox) 230 { 231 this.reView(sourceBox); 232 this.updateSourceBox(sourceBox); 233 collapse(sourceBox, false); 234 } 235 }, 236 237 /* Private, do not call outside of this object 238 * A sourceBox is a div with additional operations and state. 239 * @param sourceFile there is at most one sourceBox for each sourceFile 240 */ 241 createSourceBox: function(sourceFile) // decorator(sourceFile, sourceBox) 242 { 243 var sourceBox = this.initializeSourceBox(sourceFile); 244 245 sourceBox.decorator = this.decorator; 246 247 // Framework connection 248 sourceBox.decorator.onSourceBoxCreation(sourceBox); 249 250 this.sourceBoxes[sourceFile.href] = sourceBox; 251 252 if (FBTrace.DBG_SOURCEFILES) 253 FBTrace.sysout("firebug.createSourceBox with "+sourceBox.maximumLineNumber+" lines for "+sourceFile+(sourceFile.href?" sourceBoxes":" anon "), sourceBox); 254 255 this.panelNode.appendChild(sourceBox); 256 this.setSourceBoxLineSizes(sourceBox); 257 258 return sourceBox; 259 }, 260 261 initializeSourceBox: function(sourceFile) 262 { 263 var sourceBox = this.document.createElement("div"); 264 setClass(sourceBox, "sourceBox"); 265 collapse(sourceBox, true); 266 267 var lines = sourceFile.loadScriptLines(this.context); 268 if (!lines) 269 { 270 lines = ["Failed to load source for sourceFile "+sourceFile]; 271 } 272 273 sourceBox.lines = lines; 274 sourceBox.repObject = sourceFile; 275 276 sourceBox.maximumLineNumber = lines.length; 277 sourceBox.maxLineNoChars = (sourceBox.maximumLineNumber + "").length; 278 279 sourceBox.getLineNode = function(lineNo) 280 { 281 // XXXjjb this method is supposed to return null if the lineNo is not in the viewport 282 return $(this.decorator.getLineId(this, lineNo), this.ownerDocument); 283 }; 284 285 var paddedSource = 286 "<div class='topSourcePadding'>" + 287 "<div class='sourceRow'><div class='sourceLine'></div><div class='sourceRowText'></div></div>"+ 288 "</div>"+ 289 "<div class='sourceViewport'></div>"+ 290 "<div class='bottomSourcePadding'>"+ 291 "<div class='sourceRow'><div class='sourceLine'></div><div class='sourceRowText'></div></div>"+ 292 "</div>"; 293 294 appendInnerHTML(sourceBox, paddedSource); 295 296 sourceBox.viewport = getChildByClass(sourceBox, 'sourceViewport'); 297 return sourceBox; 298 }, 299 300 setSourceBoxLineSizes: function(sourceBox) 301 { 302 var view = sourceBox.viewport; 303 304 var lineNoCharsSpacer = ""; 305 for (var i = 0; i < sourceBox.maxLineNoChars; i++) 306 lineNoCharsSpacer += "0"; 307 308 this.startMeasuring(view); 309 var size = this.measureText(lineNoCharsSpacer); 310 this.stopMeasuring(); 311 312 sourceBox.lineHeight = size.height + 1; 313 sourceBox.lineNoWidth = size.width; 314 315 var view = sourceBox.viewport; // TODO some cleaner way 316 view.previousSibling.firstChild.firstChild.style.width = sourceBox.lineNoWidth + "px"; 317 view.nextSibling.firstChild.firstChild.style.width = sourceBox.lineNoWidth +"px"; 318 319 if (FBTrace.DBG_SOURCEFILES) 320 { 321 FBTrace.sysout("setSourceBoxLineSizes size", size); 322 FBTrace.sysout("firebug.setSourceBoxLineSizes, sourceBox.scrollTop "+sourceBox.scrollTop+ " sourceBox.lineHeight: "+sourceBox.lineHeight+" sourceBox.lineNoWidth:"+sourceBox.lineNoWidth+"\n"); 323 } 324 }, 325 326 /* 327 * @return SourceLink to currently selected source file 328 */ 329 getSourceLink: function(lineNo) 330 { 331 if (!this.selectedSourceBox) 332 return; 333 if (!lineNo) 334 lineNo = this.getCentralLine(this.selectedSourceBox); 335 return new SourceLink(this.selectedSourceBox.repObject.href, lineNo, this.getSourceType()); 336 }, 337 338 /* Select sourcebox with href, scroll lineNo into center, highlight lineNo with highlighter given 339 * @param href a URL, null means the selected sourcefile 340 * @param lineNo integer 1-maximumLineNumber 341 * @param highlighter callback, a function(sourceBox). sourceBox.centralLine will be lineNo 342 */ 343 scrollToLine: function(href, lineNo, highlighter) 344 { 345 if (FBTrace.DBG_SOURCEFILES) FBTrace.sysout("SourceBoxPanel.scrollToLine: "+lineNo+"@"+href+"\n"); 346 347 if (this.context.scrollTimeout) 348 { 349 this.context.clearTimeout(this.contextscrollTimeout); 350 delete this.context.scrollTimeout 351 } 352 353 if (href) 354 { 355 if (!this.selectedSourceBox || this.selectedSourceBox.repObject.href != href) 356 { 357 var sourceFile = this.context.sourceFileMap[href]; 358 if (!sourceFile) 359 { 360 if(FBTrace.DBG_SOURCEFILES) 361 FBTrace.sysout("scrollToLine FAILS, no sourceFile for href "+href, this.context.sourceFileMap); 362 return; 363 } 364 this.navigate(sourceFile); 365 } 366 } 367 368 this.context.scrollTimeout = this.context.setTimeout(bindFixed(function() 369 { 370 if (!this.selectedSourceBox) 371 { 372 if (FBTrace.DBG_SOURCEFILES) 373 FBTrace.sysout("SourceBoxPanel.scrollTimeout no selectedSourceBox"); 374 return; 375 } 376 377 this.selectedSourceBox.targetedLine = lineNo; 378 379 // At this time we know which sourcebox is selected but the viewport is not selected. 380 // We need to scroll, let the scroll handler set the viewport, then highlight any lines visible. 381 var skipScrolling = false; 382 if (this.selectedSourceBox.firstViewableLine && this.selectedSourceBox.lastViewableLine) 383 { 384 var linesFromTop = lineNo - this.selectedSourceBox.firstViewableLine; 385 var linesFromBot = this.selectedSourceBox.lastViewableLine - lineNo; 386 skipScrolling = (linesFromTop > 3 && linesFromBot > 3); 387 if (FBTrace.DBG_SOURCEFILES) FBTrace.sysout("SourceBoxPanel.scrollTimeout: skipScrolling: "+skipScrolling+" fromTop:"+linesFromTop+" fromBot:"+linesFromBot); 388 } 389 else // the selectedSourceBox has not been built 390 { 391 if (FBTrace.DBG_SOURCEFILES) 392 FBTrace.sysout("SourceBoxPanel.scrollTimeout, no viewable lines", this.selectedSourceBox); 393 } 394 395 if (highlighter) 396 this.selectedSourceBox.highlighter = highlighter; 397 398 if (!skipScrolling) 399 { 400 var viewRange = this.getViewRangeFromTargetLine(this.selectedSourceBox, lineNo); 401 this.selectedSourceBox.newScrollTop = this.getScrollTopFromViewRange(this.selectedSourceBox, viewRange); 402 if (FBTrace.DBG_SOURCEFILES) FBTrace.sysout("SourceBoxPanel.scrollTimeout: newScrollTop "+this.selectedSourceBox.newScrollTop+" for "+this.selectedSourceBox.repObject.href); 403 this.selectedSourceBox.scrollTop = this.selectedSourceBox.newScrollTop; // *may* cause scrolling 404 if (FBTrace.DBG_SOURCEFILES) FBTrace.sysout("SourceBoxPanel.scrollTimeout: scrollTo "+lineNo+" scrollTop:"+this.selectedSourceBox.scrollTop+ " lineHeight: "+this.selectedSourceBox.lineHeight); 405 } 406 407 if (this.selectedSourceBox.highlighter) 408 this.applyDecorator(this.selectedSourceBox); // may need to highlight even if we don't scroll 409 410 }, this)); 411 }, 412 413 /* 414 * @return a highlighter function(sourceBox) that puts a class on the line for a time slice 415 */ 416 jumpHighlightFactory: function(lineNo, context) 417 { 418 return function jumpHighlightIfInView(sourceBox) 419 { 420 var lineNode = sourceBox.getLineNode(lineNo); 421 if (lineNode) 422 { 423 setClassTimed(lineNode, "jumpHighlight", context); 424 if (FBTrace.DBG_SOURCEFILES) 425 FBTrace.sysout("jumpHighlightFactory on line "+lineNo+" lineNode:"+lineNode.innerHTML+"\n"); 426 } 427 else 428 { 429 if (FBTrace.DBG_SOURCEFILES) 430 FBTrace.sysout("jumpHighlightFactory no node at line "+lineNo, sourceBox); 431 } 432 433 return false; // not sticky 434 } 435 }, 436 437 /* 438 * resize and scroll event handler 439 */ 440 resizer: function(event) 441 { 442 // The resize target is Firebug as a whole. But most of the UI needs no special code for resize. 443 // But our SourceBoxPanel has viewport that will change size. 444 if (this.selectedSourceBox && this.visible) 445 { 446 if (FBTrace.DBG_SOURCEFILES) 447 FBTrace.sysout("resizer event: "+event.type, event); 448 449 this.reView(this.selectedSourceBox); 450 } 451 }, 452 453 reView: function(sourceBox, clearCache) // called for all scroll events, including any time sourcebox.scrollTop is set 454 { 455 if (sourceBox.targetedLine) 456 { 457 sourceBox.targetLineNumber = sourceBox.targetedLine; 458 var viewRange = this.getViewRangeFromTargetLine(sourceBox, sourceBox.targetedLine); 459 delete sourceBox.targetedLine; 460 } 461 else 462 { 463 var viewRange = this.getViewRangeFromScrollTop(sourceBox, sourceBox.scrollTop); 464 } 465 466 if (clearCache) 467 { 468 this.clearSourceBox(sourceBox); 469 } 470 else if (sourceBox.scrollTop === sourceBox.lastScrollTop && sourceBox.clientHeight === sourceBox.lastClientHeight) 471 { 472 if (sourceBox.firstRenderedLine <= viewRange.firstLine && sourceBox.lastRenderedLine >= viewRange.lastLine) 473 { 474 if (FBTrace.DBG_SOURCEFILES) 475 FBTrace.sysout("reView skipping sourceBox "+sourceBox.scrollTop+"=scrollTop="+sourceBox.lastScrollTop+", "+ sourceBox.clientHeight+"=clientHeight="+sourceBox.lastClientHeight, sourceBox); 476 // skip work if nothing changes. 477 return; 478 } 479 } 480 481 dispatch([Firebug.A11yModel], "onBeforeViewportChange", [this]); // XXXjjb TODO where should this be? 482 this.buildViewAround(sourceBox, viewRange); 483 484 if (Firebug.uiListeners.length > 0) 485 { 486 var link = new SourceLink(sourceBox.repObject.href, sourceBox.centralLine, this.getSourceType()); 487 dispatch(Firebug.uiListeners, "onViewportChange", [link]); 488 } 489 490 sourceBox.lastScrollTop = sourceBox.scrollTop; 491 sourceBox.lastClientHeight = sourceBox.clientHeight; 492 }, 493 494 buildViewAround: function(sourceBox, viewRange) 495 { 496 try 497 { 498 this.updateViewportCache(sourceBox, viewRange); 499 } 500 catch(exc) 501 { 502 if(FBTrace.DBG_ERRORS) 503 FBTrace.sysout("buildViewAround updateViewportCache FAILS "+exc, exc); 504 } 505 506 this.setViewportPadding(sourceBox, viewRange); 507 508 sourceBox.centralLine = Math.floor( (viewRange.lastLine - viewRange.firstLine)/2 ); 509 510 this.applyDecorator(sourceBox); 511 512 return; 513 }, 514 515 updateViewportCache: function(sourceBox, viewRange) 516 { 517 var cacheHit = this.insertedLinesOverlapCache(sourceBox, viewRange); 518 519 if (!cacheHit) 520 { 521 this.clearSourceBox(sourceBox); // no overlap, remove old range 522 sourceBox.firstRenderedLine = viewRange.firstLine; // reset cached range 523 sourceBox.lastRenderedLine = viewRange.lastLine; 524 } 525 else // cache overlap, expand range of cache 526 { 527 sourceBox.firstRenderedLine = Math.min(viewRange.firstLine, sourceBox.firstRenderedLine); 528 sourceBox.lastRenderedLine = Math.max(viewRange.lastLine, sourceBox.lastRenderedLine); 529 } 530 sourceBox.firstViewableLine = viewRange.firstLine; // todo actually check that these are viewable 531 sourceBox.lastViewableLine = viewRange.lastLine; 532 sourceBox.numberOfRenderedLines = sourceBox.lastRenderedLine - sourceBox.firstRenderedLine + 1; 533 534 if (FBTrace.DBG_SOURCEFILES) 535 FBTrace.sysout("buildViewAround viewRange: "+viewRange.firstLine+"-"+viewRange.lastLine+" rendered: "+sourceBox.firstRenderedLine+"-"+sourceBox.lastRenderedLine, sourceBox); 536 }, 537 538 /* 539 * Add lines from viewRange, but do not adjust first/lastRenderedLine. 540 * @return true if viewRange overlaps first/lastRenderedLine 541 */ 542 insertedLinesOverlapCache: function(sourceBox, viewRange) 543 { 544 var topCacheLine = null; 545 var cacheHit = false; 546 for (var line = viewRange.firstLine; line <= viewRange.lastLine; line++) 547 { 548 if (line >= sourceBox.firstRenderedLine && line <= sourceBox.lastRenderedLine ) 549 { 550 cacheHit = true; 551 continue; 552 } 553 554 var lineHTML = this.getSourceLineHTML(sourceBox, line); 555 556 var ref = null; 557 if (line < sourceBox.firstRenderedLine) // prepend if we are above the cache 558 { 559 if (!topCacheLine) 560 topCacheLine = sourceBox.getLineNode(sourceBox.firstRenderedLine); 561 ref = topCacheLine; 562 } 563 564 var newElement = appendInnerHTML(sourceBox.viewport, lineHTML, ref); 565 } 566 return cacheHit; 567 }, 568 569 clearSourceBox: function(sourceBox) 570 { 571 if (sourceBox.firstRenderedLine) 572 { 573 var topMostCachedElement = sourceBox.getLineNode(sourceBox.firstRenderedLine); // eg 1 574 var totalCached = sourceBox.lastRenderedLine - sourceBox.firstRenderedLine + 1; // eg 20 - 1 + 1 = 19 575 if (topMostCachedElement && totalCached) 576 this.removeLines(sourceBox, topMostCachedElement, totalCached); 577 } 578 sourceBox.lastRenderedLine = 0; 579 sourceBox.firstRenderedLine = 0; 580 sourceBox.numberOfRenderedLines = 0; 581 }, 582 583 getSourceLineHTML: function(sourceBox, i) 584 { 585 var lineNo = sourceBox.decorator.getUserVisibleLineNumber(sourceBox, i); 586 var lineHTML = sourceBox.decorator.getLineHTML(sourceBox, i); 587 var lineId = sourceBox.decorator.getLineId(sourceBox, i); // decorator lines may not have ids 588 589 var lineNoText = this.getTextForLineNo(lineNo, sourceBox.maxLineNoChars); 590 591 var theHTML = 592 '<div ' 593 + (lineId ? ('id="' + lineId + '"') : "") 594 + ' class="sourceRow" role="presentation"><a class="' 595 + 'sourceLine' + '" role="presentation">' 596 + lineNoText 597 + '</a><span class="sourceRowText" role="presentation">' 598 + lineHTML 599 + '</span></div>'; 600 601 return theHTML; 602 }, 603 604 getTextForLineNo: function(lineNo, maxLineNoChars) 605 { 606 // Make sure all line numbers are the same width (with a fixed-width font) 607 var lineNoText = lineNo + ""; 608 while (lineNoText.length < maxLineNoChars) 609 lineNoText = " " + lineNoText; 610 611 return lineNoText; 612 }, 613 614 removeLines: function(sourceBox, firstRemoval, totalRemovals) 615 { 616 for(var i = 1; i <= totalRemovals; i++) 617 { 618 var nextSourceLine = firstRemoval; 619 firstRemoval = firstRemoval.nextSibling; 620 sourceBox.viewport.removeChild(nextSourceLine); 621 } 622 }, 623 624 getCentralLine: function(sourceBox) 625 { 626 return sourceBox.centralLine; 627 }, 628 629 getViewRangeFromTargetLine: function(sourceBox, targetLineNumber) 630 { 631 var viewRange = {firstLine: 1, centralLine: targetLineNumber, lastLine: 1}; 632 633 var averageLineHeight = this.getAverageLineHeight(sourceBox); 634 var panelHeight = this.panelNode.clientHeight; 635 var linesPerViewport = Math.round((panelHeight / averageLineHeight) + 1); 636 637 viewRange.firstLine = Math.round(targetLineNumber - linesPerViewport / 2); 638 639 if (viewRange.firstLine <= 0) 640 viewRange.firstLine = 1; 641 642 viewRange.lastLine = viewRange.firstLine + linesPerViewport; 643 644 if (viewRange.lastLine > sourceBox.maximumLineNumber) 645 viewRange.lastLine = sourceBox.maximumLineNumber; 646 647 return viewRange; 648 }, 649 650 /* 651 * Use the average height of source lines in the cache to estimate where the scroll bar points based on scrollTop 652 */ 653 getViewRangeFromScrollTop: function(sourceBox, scrollTop) 654 { 655 var viewRange = {}; 656 var averageLineHeight = this.getAverageLineHeight(sourceBox); 657 viewRange.firstLine = Math.floor(scrollTop / averageLineHeight + 1); 658 659 var panelHeight = this.panelNode.clientHeight; 660 var viewableLines = Math.ceil((panelHeight / averageLineHeight) + 1); 661 viewRange.lastLine = viewRange.firstLine + viewableLines; 662 if (viewRange.lastLine > sourceBox.maximumLineNumber) 663 viewRange.lastLine = sourceBox.maximumLineNumber; 664 665 viewRange.centralLine = Math.floor((viewRange.lastLine - viewRange.firstLine)/2); 666 667 if (FBTrace.DBG_SOURCEFILES) 668 { 669 FBTrace.sysout("getViewRangeFromScrollTop scrollTop:"+scrollTop+" viewRange: "+viewRange.firstLine+"-"+viewRange.lastLine); 670 if (!this.noRecurse) 671 { 672 this.noRecurse = true; 673 var testScrollTop = this.getScrollTopFromViewRange(sourceBox, viewRange); 674 delete this.noRecurse; 675 FBTrace.sysout("getViewRangeFromScrollTop "+((scrollTop==testScrollTop)?"checks":(scrollTop+"=!scrollTop!="+testScrollTop))); 676 } 677 } 678 679 return viewRange; 680 }, 681 682 /* 683 * inverse of the getViewRangeFromScrollTop. 684 * If the viewRange was set by targetLineNumber, then this value become the new scroll top 685 * else the value will be the same as the scrollbar's given value of scrollTop. 686 */ 687 getScrollTopFromViewRange: function(sourceBox, viewRange) 688 { 689 var averageLineHeight = this.getAverageLineHeight(sourceBox); 690 var scrollTop = Math.floor(averageLineHeight * (viewRange.firstLine - 1)); 691 692 if (FBTrace.DBG_SOURCEFILES) 693 { 694 FBTrace.sysout("getScrollTopFromViewRange viewRange:"+viewRange.firstLine+"-"+viewRange.lastLine+" averageLineHeight: "+averageLineHeight+" scrollTop "+scrollTop); 695 if (!this.noRecurse) 696 { 697 this.noRecurse = true; 698 var testViewRange = this.getViewRangeFromScrollTop(sourceBox, scrollTop); 699 delete this.noRecurse; 700 var vrStr = viewRange.firstLine+"-"+viewRange.lastLine; 701 var tvrStr = testViewRange.firstLine+"-"+testViewRange.lastLine; 702 FBTrace.sysout("getScrollTopFromCenterLine "+((viewRange==testViewRange)? "checks" : vrStr+"=!viewRange!="+tvrStr)); 703 } 704 } 705 706 return scrollTop; 707 }, 708 709 /* 710 * The virtual sourceBox height is the averageLineHeight * max lines 711 * @return float 712 */ 713 getAverageLineHeight: function(sourceBox) 714 { 715 var averageLineHeight = sourceBox.lineHeight; // fall back to single line height 716 717 var renderedViewportHeight = sourceBox.viewport.clientHeight; 718 var numberOfRenderedLines = sourceBox.numberOfRenderedLines; 719 if (renderedViewportHeight && numberOfRenderedLines) 720 averageLineHeight = renderedViewportHeight / numberOfRenderedLines; 721 722 return averageLineHeight; 723 }, 724 725 /* 726 * The virtual sourceBox = topPadding + sourceBox.viewport + bottomPadding 727 * The viewport grows as more lines are added to the cache 728 * The virtual sourceBox height is estimated from the average height lines in the viewport cache 729 */ 730 getTotalPadding: function(sourceBox) 731 { 732 var numberOfRenderedLines = sourceBox.numberOfRenderedLines; 733 if (!numberOfRenderedLines) 734 return 0; 735 736 var max = sourceBox.maximumLineNumber; 737 var averageLineHeight = this.getAverageLineHeight(sourceBox); 738 // total box will be the average line height times total lines 739 var virtualSourceBoxHeight = Math.floor(max * averageLineHeight); 740 if (virtualSourceBoxHeight < sourceBox.clientHeight) 741 { 742 var scrollBarHeight = sourceBox.offsetHeight - sourceBox.clientHeight; 743 // the total - view-taken-up - scrollbar 744 var totalPadding = sourceBox.clientHeight - sourceBox.viewport.clientHeight - 1; 745 } 746 else 747 var totalPadding = virtualSourceBoxHeight - sourceBox.viewport.clientHeight; 748 749 if (FBTrace.DBG_SOURCEFILES) 750 FBTrace.sysout("getTotalPadding clientHeight:"+sourceBox.viewport.clientHeight+" max: "+max+" gives total padding "+totalPadding); 751 752 return totalPadding; 753 }, 754 755 setViewportPadding: function(sourceBox, viewRange) 756 { 757 var firstRenderedLineElement = sourceBox.getLineNode(sourceBox.firstRenderedLine); 758 if (!firstRenderedLineElement) 759 { 760 if (FBTrace.DBG_ERRORS) 761 FBTrace.sysout("setViewportPadding FAILS, no line at "+sourceBox.firstRenderedLine, sourceBox); 762 return; 763 } 764 765 var firstRenderedLineOffset = firstRenderedLineElement.offsetTop; 766 var firstViewRangeElement = sourceBox.getLineNode(viewRange.firstLine); 767 var firstViewRangeOffset = firstViewRangeElement.offsetTop; 768 var topPadding = sourceBox.scrollTop - (firstViewRangeOffset - firstRenderedLineOffset); 769 // Because of rounding when converting from pixels to lines, topPadding can be +/- lineHeight/2, round up 770 var averageLineHeight = this.getAverageLineHeight(sourceBox); 771 var linesOfPadding = Math.floor( (topPadding + averageLineHeight)/ averageLineHeight); 772 var topPadding = (linesOfPadding - 1)* averageLineHeight; 773 774 if (FBTrace.DBG_SOURCEFILES) 775 FBTrace.sysout("setViewportPadding sourceBox.scrollTop - (firstViewRangeOffset - firstRenderedLineOffset): "+sourceBox.scrollTop+"-"+"("+firstViewRangeOffset+"-"+firstRenderedLineOffset+")="+topPadding); 776 // we want the bottomPadding to take up the rest 777 var totalPadding = this.getTotalPadding(sourceBox); 778 if (totalPadding < 0) 779 var bottomPadding = Math.abs(totalPadding); 780 else 781 var bottomPadding = Math.floor(totalPadding - topPadding); 782 783 if (bottomPadding < 0) 784 bottomPadding = 0; 785 786 if(FBTrace.DBG_SOURCEFILES) 787 { 788 FBTrace.sysout("setViewportPadding viewport.offsetHeight: "+sourceBox.viewport.offsetHeight+" viewport.clientHeight "+sourceBox.viewport.clientHeight); 789 FBTrace.sysout("setViewportPadding sourceBox.offsetHeight: "+sourceBox.offsetHeight+" sourceBox.clientHeight "+sourceBox.clientHeight); 790 FBTrace.sysout("setViewportPadding scrollTop: "+sourceBox.scrollTop+" firstRenderedLine "+sourceBox.firstRenderedLine+" bottom: "+bottomPadding+" top: "+topPadding); 791 } 792 var view = sourceBox.viewport; 793 794 // Set the size on the line number field so the padding is filled with same style as source lines. 795 view.previousSibling.style.height = topPadding + "px"; 796 view.nextSibling.style.height = bottomPadding + "px"; 797 798 //sourceRow 799 view.previousSibling.firstChild.style.height = topPadding + "px"; 800 view.nextSibling.firstChild.style.height = bottomPadding + "px"; 801 802 //sourceLine 803 view.previousSibling.firstChild.firstChild.style.height = topPadding + "px"; 804 view.nextSibling.firstChild.firstChild.style.height = bottomPadding + "px"; 805 }, 806 807 applyDecorator: function(sourceBox) 808 { 809 if (this.context.sourceBoxDecoratorTimeout) 810 { 811 this.context.clearTimeout(this.context.sourceBoxDecoratorTimeout); 812 delete this.context.sourceBoxDecoratorTimeout; 813 } 814 815 this.context.sourceBoxDecoratorTimeout = this.context.setTimeout(bindFixed(function delaySourceBoxDecorator() 816 { 817 try 818 { 819 if (sourceBox.highlighter) 820 { 821 var sticky = sourceBox.highlighter(sourceBox); 822 if (FBTrace.DBG_SOURCEFILES) 823 FBTrace.sysout("sourceBoxDecoratorTimeout highlighter sticky:"+sticky, sourceBox.highlighter); 824 if (!sticky) 825 delete sourceBox.highlighter; 826 } 827 sourceBox.decorator.decorate(sourceBox, sourceBox.repObject); 828 829 if (Firebug.uiListeners.length > 0) dispatch(Firebug.uiListeners, "onApplyDecorator", [sourceBox]); 830 if (FBTrace.DBG_SOURCEFILES) 831 FBTrace.sysout("sourceBoxDecoratorTimeout "+sourceBox.repObject, sourceBox); 832 } 833 catch (exc) 834 { 835 if (FBTrace.DBG_ERRORS) 836 FBTrace.sysout("sourcebox applyDecorator FAILS "+exc, exc); 837 } 838 }, this)); 839 }, 840 }); 841 842 843 844 845 // ************************************************************************************************ 846 }}); 847