PageRenderTime 66ms CodeModel.GetById 13ms app.highlight 45ms RepoModel.GetById 2ms app.codeStats 0ms

/toolkit/content/tests/fennec-tile-testapp/chrome/content/BrowserView.js

http://github.com/zpao/v8monkey
JavaScript | 728 lines | 372 code | 143 blank | 213 comment | 34 complexity | 833e7700b16f76157ea102083a43ca1a MD5 | raw file
  1// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
  2/*
  3 * ***** BEGIN LICENSE BLOCK *****
  4 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  5 *
  6 * The contents of this file are subject to the Mozilla Public License Version
  7 * 1.1 (the "License"); you may not use this file except in compliance with
  8 * the License. You may obtain a copy of the License at
  9 * http://www.mozilla.org/MPL/
 10 *
 11 * Software distributed under the License is distributed on an "AS IS" basis,
 12 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 13 * for the specific language governing rights and limitations under the
 14 * License.
 15 *
 16 * The Original Code is Mozilla Mobile Browser.
 17 *
 18 * The Initial Developer of the Original Code is
 19 * Mozilla Corporation.
 20 * Portions created by the Initial Developer are Copyright (C) 2008
 21 * the Initial Developer. All Rights Reserved.
 22 *
 23 * Contributor(s):
 24 *   Roy Frostig <rfrostig@mozilla.com>
 25 *
 26 * Alternatively, the contents of this file may be used under the terms of
 27 * either the GNU General Public License Version 2 or later (the "GPL"), or
 28 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 29 * in which case the provisions of the GPL or the LGPL are applicable instead
 30 * of those above. If you wish to allow use of your version of this file only
 31 * under the terms of either the GPL or the LGPL, and not to allow others to
 32 * use your version of this file under the terms of the MPL, indicate your
 33 * decision by deleting the provisions above and replace them with the notice
 34 * and other provisions required by the GPL or the LGPL. If you do not delete
 35 * the provisions above, a recipient may use your version of this file under
 36 * the terms of any one of the MPL, the GPL or the LGPL.
 37 *
 38 * ***** END LICENSE BLOCK ***** */
 39
 40let Ci = Components.interfaces;
 41
 42// --- REMOVE ---
 43let noop = function() {};
 44let endl = '\n';
 45// --------------
 46
 47function BrowserView(container, visibleRect) {
 48  bindAll(this);
 49  this.init(container, visibleRect);
 50}
 51
 52/**
 53 * A BrowserView maintains state of the viewport (browser, zoom level,
 54 * dimensions) and the visible rectangle into the viewport, for every
 55 * browser it is given (cf setBrowser()).  In updates to the viewport state,
 56 * a BrowserView (using its TileManager) renders parts of the page quasi-
 57 * intelligently, with guarantees of having rendered and appended all of the
 58 * visible browser content (aka the "critical rectangle").
 59 *
 60 * State is characterized in large part by two rectangles (and an implicit third):
 61 * - Viewport: Always rooted at the origin, ie with (left, top) at (0, 0).  The
 62 *     width and height (right and bottom) of this rectangle are that of the
 63 *     current viewport, which corresponds more or less to the transformed
 64 *     browser content (scaled by zoom level).
 65 * - Visible: Corresponds to the client's viewing rectangle in viewport
 66 *     coordinates.  Has (top, left) corresponding to position, and width & height
 67 *     corresponding to the clients viewing dimensions.  Take note that the top
 68 *     and left of the visible rect are per-browser state, but that the width
 69 *     and height persist across setBrowser() calls.  This is best explained by
 70 *     a simple example: user views browser A, pans to position (x0, y0), switches
 71 *     to browser B, where she finds herself at position (x1, y1), tilts her
 72 *     device so that visible rectangle's width and height change, and switches
 73 *     back to browser A.  She expects to come back to position (x0, y0), but her
 74 *     device remains tilted.
 75 * - Critical (the implicit one): The critical rectangle is the (possibly null)
 76 *     intersection of the visible and viewport rectangles.  That is, it is that
 77 *     region of the viewport which is visible to the user.  We care about this
 78 *     because it tells us which region must be rendered as soon as it is dirtied.
 79 *     The critical rectangle is mostly state that we do not keep in BrowserView
 80 *     but that our TileManager maintains.
 81 *
 82 * Example rectangle state configurations:
 83 *
 84 *
 85 *        +-------------------------------+
 86 *        |A                              |
 87 *        |                               |
 88 *        |                               |
 89 *        |                               |
 90 *        |        +----------------+     |
 91 *        |        |B,C             |     |
 92 *        |        |                |     |
 93 *        |        |                |     |
 94 *        |        |                |     |
 95 *        |        +----------------+     |
 96 *        |                               |
 97 *        |                               |
 98 *        |                               |
 99 *        |                               |
100 *        |                               |
101 *        +-------------------------------+
102 *
103 *
104 * A = viewport ; at (0, 0)
105 * B = visible  ; at (x, y) where x > 0, y > 0
106 * C = critical ; at (x, y)
107 *
108 *
109 *
110 *        +-------------------------------+
111 *        |A                              |
112 *        |                               |
113 *        |                               |
114 *        |                               |
115 *   +----+-----------+                   |
116 *   |B   .C          |                   |
117 *   |    .           |                   |
118 *   |    .           |                   |
119 *   |    .           |                   |
120 *   +----+-----------+                   |
121 *        |                               |
122 *        |                               |
123 *        |                               |
124 *        |                               |
125 *        |                               |
126 *        +-------------------------------+
127 *
128 *
129 * A = viewport ; at (0, 0)
130 * B = visible  ; at (x, y) where x < 0, y > 0
131 * C = critical ; at (0, y)
132 *
133 *
134 * Maintaining per-browser state is a little bit of a hack involving attaching
135 * an object as the obfuscated dynamic JS property of the browser object, that
136 * hopefully no one but us will touch.  See getViewportStateFromBrowser() for
137 * the property name.
138 */
139BrowserView.prototype = (
140function() {
141
142  // -----------------------------------------------------------
143  // Privates
144  //
145
146  const kZoomLevelMin = 0.2;
147  const kZoomLevelMax = 4.0;
148  const kZoomLevelPrecision = 10000;
149
150  function visibleRectToCriticalRect(visibleRect, browserViewportState) {
151    return visibleRect.intersect(browserViewportState.viewportRect);
152  }
153
154  function clampZoomLevel(zl) {
155    let bounded = Math.min(Math.max(kZoomLevelMin, zl), kZoomLevelMax);
156    return Math.round(bounded * kZoomLevelPrecision) / kZoomLevelPrecision;
157  }
158
159  function pageZoomLevel(visibleRect, browserW, browserH) {
160    return clampZoomLevel(visibleRect.width / browserW);
161  }
162
163  function seenBrowser(browser) {
164    return !!(browser.__BrowserView__vps);
165  }
166
167  function initBrowserState(browser, visibleRect) {
168    let [browserW, browserH] = getBrowserDimensions(browser);
169
170    let zoomLevel = pageZoomLevel(visibleRect, browserW, browserH);
171    let viewportRect = (new wsRect(0, 0, browserW, browserH)).scale(zoomLevel, zoomLevel);
172
173    dump('--- initing browser to ---' + endl);
174    browser.__BrowserView__vps = new BrowserView.BrowserViewportState(viewportRect,
175                                                                      visibleRect.x,
176                                                                      visibleRect.y,
177                                                                      zoomLevel);
178    dump(browser.__BrowserView__vps.toString() + endl);
179    dump('--------------------------' + endl);
180  }
181
182  function getViewportStateFromBrowser(browser) {
183    return browser.__BrowserView__vps;
184  }
185
186  function getBrowserDimensions(browser) {
187    return [browser.scrollWidth, browser.scrollHeight];
188  }
189
190  function getContentScrollValues(browser) {
191    let cwu = getBrowserDOMWindowUtils(browser);
192    let scrollX = {};
193    let scrollY = {};
194    cwu.getScrollXY(false, scrollX, scrollY);
195
196    return [scrollX.value, scrollY.value];
197  }
198
199  function getBrowserDOMWindowUtils(browser) {
200    return browser.contentWindow
201      .QueryInterface(Ci.nsIInterfaceRequestor)
202      .getInterface(Ci.nsIDOMWindowUtils);
203  }
204
205  function getNewBatchOperationState() {
206    return {
207      viewportSizeChanged: false,
208      dirtyAll: false
209    };
210  }
211
212  function clampViewportWH(width, height, visibleRect) {
213    let minW = visibleRect.width;
214    let minH = visibleRect.height;
215    return [Math.max(width, minW), Math.max(height, minH)];
216  }
217
218  function initContainer(container, visibleRect) {
219    container.style.width    = visibleRect.width  + 'px';
220    container.style.height   = visibleRect.height + 'px';
221    container.style.overflow = '-moz-hidden-unscrollable';
222  }
223
224  function resizeContainerToViewport(container, viewportRect) {
225    container.style.width  = viewportRect.width  + 'px';
226    container.style.height = viewportRect.height + 'px';
227  }
228
229  // !!! --- RESIZE HACK BEGIN -----
230  function simulateMozAfterSizeChange(browser, width, height) {
231    let ev = document.createElement("MouseEvents");
232    ev.initEvent("FakeMozAfterSizeChange", false, false, window, 0, width, height);
233    browser.dispatchEvent(ev);
234  }
235  // !!! --- RESIZE HACK END -------
236
237  // --- Change of coordinates functions --- //
238
239
240  // The following returned object becomes BrowserView.prototype
241  return {
242
243    // -----------------------------------------------------------
244    // Public instance methods
245    //
246
247    init: function init(container, visibleRect) {
248      this._batchOps = [];
249      this._container = container;
250      this._browserViewportState = null;
251      this._renderMode = 0;
252      this._tileManager = new TileManager(this._appendTile, this._removeTile, this);
253      this.setVisibleRect(visibleRect);
254
255      // !!! --- RESIZE HACK BEGIN -----
256      // remove this eventually
257      this._resizeHack = {
258        maxSeenW: 0,
259        maxSeenH: 0
260      };
261      // !!! --- RESIZE HACK END -------
262    },
263
264    setVisibleRect: function setVisibleRect(r) {
265      let bvs = this._browserViewportState;
266      let vr  = this._visibleRect;
267
268      if (!vr)
269        this._visibleRect = vr = r.clone();
270      else
271        vr.copyFrom(r);
272
273      if (bvs) {
274        bvs.visibleX = vr.left;
275        bvs.visibleY = vr.top;
276
277        // reclamp minimally to the new visible rect
278        //this.setViewportDimensions(bvs.viewportRect.right, bvs.viewportRect.bottom);
279      } else
280        this._viewportChanged(false, false);
281    },
282
283    getVisibleRect: function getVisibleRect() {
284      return this._visibleRect.clone();
285    },
286
287    getVisibleRectX: function getVisibleRectX() { return this._visibleRect.x; },
288    getVisibleRectY: function getVisibleRectY() { return this._visibleRect.y; },
289    getVisibleRectWidth: function getVisibleRectWidth() { return this._visibleRect.width; },
290    getVisibleRectHeight: function getVisibleRectHeight() { return this._visibleRect.height; },
291
292    setViewportDimensions: function setViewportDimensions(width, height, causedByZoom) {
293      let bvs = this._browserViewportState;
294      let vis = this._visibleRect;
295
296      if (!bvs)
297        return;
298
299      //[width, height] = clampViewportWH(width, height, vis);
300      bvs.viewportRect.right  = width;
301      bvs.viewportRect.bottom = height;
302
303      // XXX we might not want the user's page to disappear from under them
304      // at this point, which could happen if the container gets resized such
305      // that visible rect becomes entirely outside of viewport rect.  might
306      // be wise to define what UX should be in this case, like a move occurs.
307      // then again, we could also argue this is the responsibility of the
308      // caller who would do such a thing...
309
310      this._viewportChanged(true, !!causedByZoom);
311    },
312
313    setZoomLevel: function setZoomLevel(zl) {
314      let bvs = this._browserViewportState;
315
316      if (!bvs)
317        return;
318
319      let newZL = clampZoomLevel(zl);
320
321      if (newZL != bvs.zoomLevel) {
322        let browserW = this.viewportToBrowser(bvs.viewportRect.right);
323        let browserH = this.viewportToBrowser(bvs.viewportRect.bottom);
324        bvs.zoomLevel = newZL; // side-effect: now scale factor in transformations is newZL
325        this.setViewportDimensions(this.browserToViewport(browserW),
326                                   this.browserToViewport(browserH));
327      }
328    },
329
330    getZoomLevel: function getZoomLevel() {
331      let bvs = this._browserViewportState;
332      if (!bvs)
333        return undefined;
334
335      return bvs.zoomLevel;
336    },
337
338    beginBatchOperation: function beginBatchOperation() {
339      this._batchOps.push(getNewBatchOperationState());
340      this.pauseRendering();
341    },
342
343    commitBatchOperation: function commitBatchOperation() {
344      let bops = this._batchOps;
345
346      if (bops.length == 0)
347        return;
348
349      let opState = bops.pop();
350      this._viewportChanged(opState.viewportSizeChanged, opState.dirtyAll);
351      this.resumeRendering();
352    },
353
354    discardBatchOperation: function discardBatchOperation() {
355      let bops = this._batchOps;
356      bops.pop();
357      this.resumeRendering();
358    },
359
360    discardAllBatchOperations: function discardAllBatchOperations() {
361      let bops = this._batchOps;
362      while (bops.length > 0)
363        this.discardBatchOperation();
364    },
365
366    moveVisibleBy: function moveVisibleBy(dx, dy) {
367      let vr = this._visibleRect;
368      let vs = this._browserViewportState;
369
370      this.onBeforeVisibleMove(dx, dy);
371      this.onAfterVisibleMove(dx, dy);
372    },
373
374    moveVisibleTo: function moveVisibleTo(x, y) {
375      let visibleRect = this._visibleRect;
376      let dx = x - visibleRect.x;
377      let dy = y - visibleRect.y;
378      this.moveBy(dx, dy);
379    },
380
381    /**
382     * Calls to this function need to be one-to-one with calls to
383     * resumeRendering()
384     */
385    pauseRendering: function pauseRendering() {
386      this._renderMode++;
387    },
388
389    /**
390     * Calls to this function need to be one-to-one with calls to
391     * pauseRendering()
392     */
393    resumeRendering: function resumeRendering(renderNow) {
394      if (this._renderMode > 0)
395        this._renderMode--;
396
397      if (renderNow || this._renderMode == 0)
398        this._tileManager.criticalRectPaint();
399    },
400
401    isRendering: function isRendering() {
402      return (this._renderMode == 0);
403    },
404
405    /**
406     * @param dx Guess delta to destination x coordinate
407     * @param dy Guess delta to destination y coordinate
408     */
409    onBeforeVisibleMove: function onBeforeVisibleMove(dx, dy) {
410      let vs = this._browserViewportState;
411      let vr = this._visibleRect;
412
413      let destCR = visibleRectToCriticalRect(vr.clone().translate(dx, dy), vs);
414
415      this._tileManager.beginCriticalMove(destCR);
416    },
417
418    /**
419     * @param dx Actual delta to destination x coordinate
420     * @param dy Actual delta to destination y coordinate
421     */
422    onAfterVisibleMove: function onAfterVisibleMove(dx, dy) {
423      let vs = this._browserViewportState;
424      let vr = this._visibleRect;
425
426      vr.translate(dx, dy);
427      vs.visibleX = vr.left;
428      vs.visibleY = vr.top;
429
430      let cr = visibleRectToCriticalRect(vr, vs);
431
432      this._tileManager.endCriticalMove(cr, this.isRendering());
433    },
434
435    setBrowser: function setBrowser(browser, skipZoom) {
436      let currentBrowser = this._browser;
437
438      let browserChanged = (currentBrowser !== browser);
439
440      if (currentBrowser) {
441        currentBrowser.removeEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
442
443        // !!! --- RESIZE HACK BEGIN -----
444        // change to the real event type and perhaps refactor the handler function name
445        currentBrowser.removeEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
446        // !!! --- RESIZE HACK END -------
447
448        this.discardAllBatchOperations();
449
450        currentBrowser.setAttribute("type", "content");
451        currentBrowser.docShell.isOffScreenBrowser = false;
452      }
453
454      this._restoreBrowser(browser);
455
456      browser.setAttribute("type", "content-primary");
457
458      this.beginBatchOperation();
459
460      browser.addEventListener("MozAfterPaint", this.handleMozAfterPaint, false);
461
462      // !!! --- RESIZE HACK BEGIN -----
463      // change to the real event type and perhaps refactor the handler function name
464      browser.addEventListener("FakeMozAfterSizeChange", this.handleMozAfterSizeChange, false);
465      // !!! --- RESIZE HACK END -------
466
467      if (!skipZoom) {
468        browser.docShell.isOffScreenBrowser = true;
469        this.zoomToPage();
470      }
471
472      this._viewportChanged(browserChanged, browserChanged);
473
474      this.commitBatchOperation();
475    },
476
477    handleMozAfterPaint: function handleMozAfterPaint(ev) {
478      let browser = this._browser;
479      let tm = this._tileManager;
480      let vs = this._browserViewportState;
481
482      let [scrollX, scrollY] = getContentScrollValues(browser);
483      let clientRects = ev.clientRects;
484
485      // !!! --- RESIZE HACK BEGIN -----
486      // remove this, cf explanation in loop below
487      let hack = this._resizeHack;
488      let hackSizeChanged = false;
489      // !!! --- RESIZE HACK END -------
490
491      let rects = [];
492      // loop backwards to avoid xpconnect penalty for .length
493      for (let i = clientRects.length - 1; i >= 0; --i) {
494        let e = clientRects.item(i);
495        let r = new wsRect(e.left + scrollX,
496                           e.top + scrollY,
497                           e.width, e.height);
498
499        this.browserToViewportRect(r);
500        r.round();
501
502        if (r.right < 0 || r.bottom < 0)
503          continue;
504
505        // !!! --- RESIZE HACK BEGIN -----
506        // remove this.  this is where we make 'lazy' calculations
507        // that hint at a browser size change and fake the size change
508        // event dispach
509        if (r.right > hack.maxW) {
510          hack.maxW = rect.right;
511          hackSizeChanged = true;
512        }
513        if (r.bottom > hack.maxH) {
514          hack.maxH = rect.bottom;
515          hackSizeChanged = true;
516        }
517        // !!! --- RESIZE HACK END -------
518
519        r.restrictTo(vs.viewportRect);
520        rects.push(r);
521      }
522
523      // !!! --- RESIZE HACK BEGIN -----
524      // remove this, cf explanation in loop above
525      if (hackSizeChanged)
526        simulateMozAfterSizeChange(browser, hack.maxW, hack.maxH);
527      // !!! --- RESIZE HACK END -------
528
529      tm.dirtyRects(rects, this.isRendering());
530    },
531
532    handleMozAfterSizeChange: function handleMozAfterPaint(ev) {
533      // !!! --- RESIZE HACK BEGIN -----
534      // get the correct properties off of the event, these are wrong because
535      // we're using a MouseEvent since it has an X and Y prop of some sort and
536      // we piggyback on that.
537      let w = ev.screenX;
538      let h = ev.screenY;
539      // !!! --- RESIZE HACK END -------
540
541      this.setViewportDimensions(w, h);
542    },
543
544    zoomToPage: function zoomToPage() {
545      let browser = this._browser;
546
547      if (!browser)
548        return;
549
550      let [w, h] = getBrowserDimensions(browser);
551      this.setZoomLevel(pageZoomLevel(this._visibleRect, w, h));
552    },
553
554    zoom: function zoom(aDirection) {
555      if (aDirection == 0)
556        return;
557
558      var zoomDelta = 0.05; // 1/20
559      if (aDirection >= 0)
560        zoomDelta *= -1;
561
562      this.zoomLevel = this._zoomLevel + zoomDelta;
563    },
564
565    viewportToBrowser: function viewportToBrowser(x) {
566      let bvs = this._browserViewportState;
567
568      if (!bvs)
569        throw "No browser is set";
570
571      return x / bvs.zoomLevel;
572    },
573
574    browserToViewport: function browserToViewport(x) {
575      let bvs = this._browserViewportState;
576
577      if (!bvs)
578        throw "No browser is set";
579
580      return x * bvs.zoomLevel;
581    },
582
583    viewportToBrowserRect: function viewportToBrowserRect(rect) {
584      let f = this.viewportToBrowser(1.0);
585      return rect.scale(f, f);
586    },
587
588    browserToViewportRect: function browserToViewportRect(rect) {
589      let f = this.browserToViewport(1.0);
590      return rect.scale(f, f);
591    },
592
593    browserToViewportCanvasContext: function browserToViewportCanvasContext(ctx) {
594      let f = this.browserToViewport(1.0);
595      ctx.scale(f, f);
596    },
597
598
599    // -----------------------------------------------------------
600    // Private instance methods
601    //
602
603    _restoreBrowser: function _restoreBrowser(browser) {
604      let vr = this._visibleRect;
605
606      if (!seenBrowser(browser))
607        initBrowserState(browser, vr);
608
609      let bvs = getViewportStateFromBrowser(browser);
610
611      this._contentWindow = browser.contentWindow;
612      this._browser = browser;
613      this._browserViewportState = bvs;
614      vr.left = bvs.visibleX;
615      vr.top  = bvs.visibleY;
616      this._tileManager.setBrowser(browser);
617    },
618
619    _viewportChanged: function _viewportChanged(viewportSizeChanged, dirtyAll) {
620      let bops = this._batchOps;
621
622      if (bops.length > 0) {
623        let opState = bops[bops.length - 1];
624
625        if (viewportSizeChanged)
626          opState.viewportSizeChanged = viewportSizeChanged;
627
628        if (dirtyAll)
629          opState.dirtyAll = dirtyAll;
630
631        return;
632      }
633
634      let bvs = this._browserViewportState;
635      let vis = this._visibleRect;
636
637      // !!! --- RESIZE HACK BEGIN -----
638      // We want to uncomment this for perf, but we can't with the hack in place
639      // because the mozAfterPaint gives us rects that we use to create the
640      // fake mozAfterResize event, so we can't just clear things.
641      /*
642      if (dirtyAll) {
643        // We're about to mark the entire viewport dirty, so we can clear any
644        // queued afterPaint events that will cause redundant draws
645        getBrowserDOMWindowUtils(this._browser).clearMozAfterPaintEvents();
646      }
647      */
648      // !!! --- RESIZE HACK END -------
649
650      if (bvs) {
651        resizeContainerToViewport(this._container, bvs.viewportRect);
652
653        this._tileManager.viewportChangeHandler(bvs.viewportRect,
654                                                visibleRectToCriticalRect(vis, bvs),
655                                                viewportSizeChanged,
656                                                dirtyAll);
657      }
658    },
659
660    _appendTile: function _appendTile(tile) {
661      let canvas = tile.getContentImage();
662
663      /*
664      canvas.style.position = "absolute";
665      canvas.style.left = tile.x + "px";
666      canvas.style.top  = tile.y + "px";
667      */
668
669      canvas.setAttribute("style", "position: absolute; left: " + tile.boundRect.left + "px; " + "top: " + tile.boundRect.top + "px;");
670
671      this._container.appendChild(canvas);
672
673      //dump('++ ' + tile.toString(true) + endl);
674    },
675
676    _removeTile: function _removeTile(tile) {
677      let canvas = tile.getContentImage();
678
679      this._container.removeChild(canvas);
680
681      //dump('-- ' + tile.toString(true) + endl);
682    }
683
684  };
685
686}
687)();
688
689
690// -----------------------------------------------------------
691// Helper structures
692//
693
694BrowserView.BrowserViewportState = function(viewportRect,
695                                            visibleX,
696                                            visibleY,
697                                            zoomLevel) {
698
699  this.init(viewportRect, visibleX, visibleY, zoomLevel);
700};
701
702BrowserView.BrowserViewportState.prototype = {
703
704  init: function init(viewportRect, visibleX, visibleY, zoomLevel) {
705    this.viewportRect = viewportRect;
706    this.visibleX     = visibleX;
707    this.visibleY     = visibleY;
708    this.zoomLevel    = zoomLevel;
709  },
710
711  clone: function clone() {
712    return new BrowserView.BrowserViewportState(this.viewportRect,
713                                                this.visibleX,
714                                                this.visibleY,
715						                                    this.zoomLevel);
716  },
717
718  toString: function toString() {
719    let props = ['\tviewportRect=' + this.viewportRect.toString(),
720                 '\tvisibleX='     + this.visibleX,
721                 '\tvisibleY='     + this.visibleY,
722                 '\tzoomLevel='    + this.zoomLevel];
723
724    return '[BrowserViewportState] {\n' + props.join(',\n') + '\n}';
725  }
726
727};
728