├── .gitignore ├── .jpmignore ├── .jshintrc ├── .travis.yml ├── README.md ├── chrome.manifest ├── chrome ├── locale │ ├── de │ │ ├── notification.properties │ │ ├── popup-panel.properties │ │ └── toolbox.properties │ ├── en-US │ │ ├── notification.properties │ │ ├── popup-panel.properties │ │ └── toolbox.properties │ └── zh-CN │ │ ├── notification.properties │ │ ├── popup-panel.properties │ │ └── toolbox.properties └── skin │ └── classic │ └── shared │ ├── logo_16x16.png │ ├── logo_32x32.png │ ├── logo_64x64.png │ ├── resizer.png │ └── ua.css ├── data ├── config.js ├── img │ ├── close.svg │ └── slider.png ├── layer-form.js ├── layer-list.js ├── layer-store.js ├── lib │ ├── bootstrap │ │ └── css │ │ │ └── bootstrap.css │ ├── react │ │ └── react.js │ └── requirejs │ │ └── require.js ├── notification-config.js ├── notification-content.js ├── notification.html ├── notification.js ├── popup-frame-script.js ├── popup-panel.js ├── popup.css ├── popup.html └── popup.js ├── docs └── images │ ├── empty-panel.png │ ├── firebug-integration.png │ ├── layer-form-page.png │ ├── layer-form-panel.png │ ├── popup-panel.png │ └── start-button.png ├── index.js ├── lib ├── first-run.js ├── main.js ├── pixel-perfect-actor.js ├── pixel-perfect-front.js ├── pixel-perfect-popup.js ├── pixel-perfect-store.js ├── pixel-perfect-toolbox-overlay.js ├── start-button.js └── style-editor-overlay.js ├── license.txt ├── package.json └── test ├── common.js ├── frame-script.js ├── test-add-layer.js ├── test-layer-invert.js ├── test-layer-lock.js ├── test-layer-opacity.js ├── test-layer-position.js ├── test-layer-scale.js ├── test-layer-visibility.js ├── test-remove-layer.js ├── test-start-button.js └── test-toggle-popup.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Junk that could exist anywhere: 3 | .DS_Store 4 | *.swp 5 | *.tmp 6 | .*.gz 7 | *.patch 8 | *~ 9 | 10 | # Temporary files created by Eclipse 11 | .tmp* 12 | 13 | # Editor junk 14 | *.project 15 | /.pydevproject 16 | /.settings/ 17 | /.settings.xml 18 | /.settings.xml.old 19 | /.idea/ 20 | *.iws 21 | *.ids 22 | *.iml 23 | *.ipr 24 | 25 | # Build Files 26 | /build/ 27 | /release/ 28 | *.graphml 29 | *.xpi 30 | 31 | # Files from NPM 32 | /node_modules/ 33 | 34 | # Extensions 35 | /firebug@software.joehewitt.com 36 | 37 | # Bash 38 | *.sh 39 | *.bat 40 | -------------------------------------------------------------------------------- /.jpmignore: -------------------------------------------------------------------------------- 1 | # Doc Files 2 | /docs/ 3 | 4 | # Test Files 5 | /test/ 6 | 7 | # GIT 8 | /.git/ 9 | 10 | # Other files 11 | .gitignore 12 | .jpmignore 13 | .jshintrc 14 | .project 15 | 16 | # Existing packages 17 | *.xpi 18 | 19 | # Travis 20 | .travis.yml 21 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": "true", 3 | "predef": [ "require", "exports", "module" ], 4 | "curly": "true" 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "0.10" 5 | 6 | before_install: 7 | - "export DISPLAY=:99.0" 8 | - "sh -e /etc/init.d/xvfb start" 9 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16 -extension RANDR" 10 | 11 | before_script: 12 | - npm install jpm -g 13 | - npm install mozilla-download -g 14 | - cd .. 15 | - mozilla-download --branch fx-team --product firefox $TRAVIS_BUILD_DIR/../ 16 | - cd $TRAVIS_BUILD_DIR 17 | 18 | script: 19 | - export JPM_FIREFOX_BINARY=$TRAVIS_BUILD_DIR/../firefox/firefox 20 | - jpm test -v 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pixel Perfect [![Build Status](https://api.travis-ci.org/firebug/pixel-perfect.png)](https://travis-ci.org/firebug/pixel-perfect) 2 | ============= 3 | 4 | [![Join the chat at https://gitter.im/firebug/pixel-perfect](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/firebug/pixel-perfect?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 5 | 6 | Make your web development Pixel Perfect. 7 | 8 | * Home Page: https://github.com/firebug/pixel-perfect/wiki 9 | * Developer Guide: https://github.com/firebug/pixel-perfect/wiki/Developer-Guide 10 | 11 | Pixel Perfect is Firefox extension built on top of native developer tools in Firefox. 12 | 13 | License 14 | ------- 15 | PixelPerfect is free and open source software distributed under the 16 | [BSD License](https://github.com/firebug/pixel-perfect/blob/master/license.txt). 17 | 18 | Hacking on Pixel Perfect 2 19 | -------------------------- 20 | See Pixel Perfect [Developer Guide](https://github.com/firebug/pixel-perfect/wiki/Developer-Guide) 21 | 22 | Further Resources 23 | ----------------- 24 | * Add-on SDK: https://developer.mozilla.org/en-US/Add-ons/SDK 25 | * DevTools API: https://developer.mozilla.org/en-US/docs/Tools/DevToolsAPI 26 | * Coding Style: https://github.com/mozilla/addon-sdk/wiki/Coding-style-guide 27 | * DevTools Extension Examples: https://github.com/mozilla/addon-sdk/tree/devtools/examples 28 | * DevTools/Hacking: https://wiki.mozilla.org/DevTools/Hacking 29 | * Firefox Developer Edition: https://developer.mozilla.org/en-US/Firefox/Developer_Edition 30 | * Firebug.next wiki: https://getfirebug.com/wiki/index.php/Firebug.next 31 | -------------------------------------------------------------------------------- /chrome.manifest: -------------------------------------------------------------------------------- 1 | content pixelperfect chrome/content/ 2 | skin pixelperfect classic/1.0 chrome/skin/classic/shared/ 3 | 4 | locale pixelperfect en-US chrome/locale/en-US/ 5 | locale pixelperfect zh-CN chrome/locale/zh-CN/ 6 | locale pixelperfect de chrome/locale/de/ 7 | -------------------------------------------------------------------------------- /chrome/locale/de/notification.properties: -------------------------------------------------------------------------------- 1 | pixelPerfect.firstRun.welcome=Willkommen bei Pixel Perfect! 2 | pixelPerfect.firstRun.description=Pixel Perfect wurde ursprünglich als Erweiterung für Firebug eingeführt. Diese neue Version basiert direkt auf den nativen Firefox Entwickler-Tools und kommt mit verbesserter Benutzerfreundlichkeit und verbesserten Funktionen daher. Es kann nun mit oder ohne Firebug verwendet werden. Unter anderem werden Remote-Geräte wie Mobiltelefone unterstützt. Viel Spaß! 3 | pixelPerfect.firstRun.start=Starte Pixel Perfect 4 | -------------------------------------------------------------------------------- /chrome/locale/de/popup-panel.properties: -------------------------------------------------------------------------------- 1 | pixelPerfect.label.opacity=Deckkraft 2 | pixelPerfect.label.x=X-Achse 3 | pixelPerfect.label.y=Y-Achse 4 | pixelPerfect.label.scale=Größe 5 | pixelPerfect.label.lock=Sperren 6 | pixelPerfect.label.invert=Invertieren 7 | pixelPerfect.label.addLayer=Ebene hinzufügen 8 | pixelPerfect.help.desc=Pixel Perfect erlaubt es, (HTML)-Seitenebenen zu erstellen. Beginnen Sie mit dem hinzufügen einer neuen Ebene, indem Sie auf die untenstehende Schaltfläche klicken. 9 | pixelPerfect.help.more=Mehr erfahren 10 | -------------------------------------------------------------------------------- /chrome/locale/de/toolbox.properties: -------------------------------------------------------------------------------- 1 | pixelPerfect.title=Pixel Perfect 2 | pixelPerfect.startButton.title=Pixel Perfect 3 | pixelPerfect.startButton.tip=Machen Sie ihre Webentwicklung Pixel Perfect 4 | pixelPerfect.menu.about.label=Über Pixel Perfect… 5 | pixelPerfect.menu.about.tip=Informationen über Pixel Perfect 6 | pixelPerfect.menu.visitHomePage.label=Webseite besuchen… 7 | pixelPerfect.menu.visitHomePage.tip=Lernen Sie mehr über Pixel Perfect 8 | pixelPerfect.menu.reportIssue.label=Problem melden… 9 | pixelPerfect.menu.reportIssue.tip=Haben Sie einen Fehler gefunden oder brauchen Sie eine neue Funktion? Lassen Sie es uns wissen! 10 | pixelPerfect.menu.group.label=Diskussionsgruppe… 11 | pixelPerfect.menu.group.tip=Öffnen Sie die Diskussionsgruppenseite (mit Firebug geteilt) 12 | -------------------------------------------------------------------------------- /chrome/locale/en-US/notification.properties: -------------------------------------------------------------------------------- 1 | # LOCALIZATION NOTE (pixelPerfect.firstRun.welcome, pixelPerfect.firstRun.description, 2 | # pixelPerfect.firstRun.start): A message used in a notification dialog 3 | # that is displayed when Pixel Perfect is installed 4 | pixelPerfect.firstRun.welcome=Welcome to Pixel Perfect! 5 | pixelPerfect.firstRun.description=Pixel Perfect was originally introduced as an extension for Firebug. This new version is built on top of the native Firefox developer tools with improved user experience and features. It can now be used with or without Firebug. Among other things, remote devices such as mobile phones are also supported. Enjoy! 6 | pixelPerfect.firstRun.start=Start Pixel Perfect 7 | -------------------------------------------------------------------------------- /chrome/locale/en-US/popup-panel.properties: -------------------------------------------------------------------------------- 1 | # LOCALIZATION NOTE (pixelPerfect.label.opacity, pixelPerfect.label.x, 2 | # pixelPerfect.label.y, pixelPerfect.label.scale, 3 | # pixelPerfect.label.lock, pixelPerfect.label.addLayer): 4 | # Label for Pixel Perfect popup panel form 5 | pixelPerfect.label.opacity=Opacity 6 | pixelPerfect.label.x=X 7 | pixelPerfect.label.y=Y 8 | pixelPerfect.label.scale=Scale 9 | pixelPerfect.label.lock=Lock 10 | pixelPerfect.label.invert=Invert 11 | pixelPerfect.label.addLayer=Add Layer 12 | 13 | # LOCALIZATION NOTE (pixelPerfect.help.desc): description displayed 14 | # in Pixel Perfect panel when there are no layers. 15 | pixelPerfect.help.desc=Pixel Perfect allows creating (HTML) page layers. Start with adding a new layer by clicking on the button below. 16 | 17 | # LOCALIZATION NOTE (pixelPerfect.help.more): A label used for 'More' 18 | # link that is displayed in Pixel Perfect panel when there are no layers. 19 | pixelPerfect.help.more=Learn More 20 | -------------------------------------------------------------------------------- /chrome/locale/en-US/toolbox.properties: -------------------------------------------------------------------------------- 1 | # LOCALIZATION NOTE (pixelPerfect.title): Pixel Perfect extension title 2 | pixelPerfect.title=Pixel Perfect 3 | 4 | # LOCALIZATION NOTE (pixelPerfect.button.title, pixelPerfect.button.tip): 5 | # Label and a tooltip for Pixel Perfect start button available in the 6 | # Firefox toolbar and Style Editor panel. This button opens Pixel Perfect 7 | # popup panel. 8 | pixelPerfect.startButton.title=Pixel Perfect 9 | pixelPerfect.startButton.tip=Make your web development Pixel Perfect 10 | 11 | # LOCALIZATION NOTE (pixelPerfect.menu.about.label, pixelPerfect.menu.about.tip): 12 | # Pixel Perfect menu label. The menu is available in pixel perfect start 13 | # button located in Firefox toolbar. 14 | pixelPerfect.menu.about.label=About Pixel Perfect... 15 | pixelPerfect.menu.about.tip=Information about Pixel Perfect 16 | 17 | # LOCALIZATION NOTE (pixelPerfect.menu.visitHomePage.label, pixelPerfect.menu.visitHomePage.tip): 18 | # Pixel Perfect menu label. The menu is available in pixel perfect start 19 | # button located in Firefox toolbar. 20 | pixelPerfect.menu.visitHomePage.label=Visit Home Page... 21 | pixelPerfect.menu.visitHomePage.tip=Learn more about Pixel Perfect 22 | 23 | # LOCALIZATION NOTE (pixelPerfect.menu.reportIssue.label, pixelPerfect.menu.reportIssue.tip): 24 | # Pixel Perfect menu label. The menu is available in pixel perfect start 25 | # button located in Firefox toolbar. 26 | pixelPerfect.menu.reportIssue.label=Report Issue... 27 | pixelPerfect.menu.reportIssue.tip=Did you find a bug or do you need a new feature? Let us know! 28 | 29 | # LOCALIZATION NOTE (pixelPerfect.menu.group.label, pixelPerfect.menu.group.tip): 30 | # Pixel Perfect menu label. The menu is available in pixel perfect start 31 | # button located in Firefox toolbar. 32 | pixelPerfect.menu.group.label=Discussion Group... 33 | pixelPerfect.menu.group.tip=Open the discussion group site (shared with Firebug) 34 | -------------------------------------------------------------------------------- /chrome/locale/zh-CN/notification.properties: -------------------------------------------------------------------------------- 1 | pixelPerfect.firstRun.welcome=欢迎使用 Pixel Perfect! 2 | pixelPerfect.firstRun.description=Pixel Perfect 最初是作为一个适用于 Firebug 的扩展被引入。这个新版本建立于原生的 Firefox 开发者工具并带有改进的用户体验和功能。它可以适用于或不用于 Firebug。除此以外,远程设备(例如移动电话)现在也已支持。尽情享用吧! 3 | pixelPerfect.firstRun.start=启动 Pixel Perfect 4 | -------------------------------------------------------------------------------- /chrome/locale/zh-CN/popup-panel.properties: -------------------------------------------------------------------------------- 1 | pixelPerfect.label.opacity=不透明度 2 | pixelPerfect.label.x=X 3 | pixelPerfect.label.y=Y 4 | pixelPerfect.label.scale=比例尺 5 | pixelPerfect.label.lock=锁定 6 | pixelPerfect.label.invert=反转 7 | pixelPerfect.label.addLayer=添加图层 8 | pixelPerfect.help.desc=Pixel Perfect 带来创建 (HTML) 页面图层的功能。首先,点击下面的按钮添加一个新图层。 9 | pixelPerfect.help.more=详细了解 10 | -------------------------------------------------------------------------------- /chrome/locale/zh-CN/toolbox.properties: -------------------------------------------------------------------------------- 1 | pixelPerfect.title=Pixel Perfect 2 | pixelPerfect.startButton.title=Pixel Perfect 3 | pixelPerfect.startButton.tip=使用 Pixel Perfect 助力您的 Web 开发 4 | pixelPerfect.menu.about.label=关于 Pixel Perfect... 5 | pixelPerfect.menu.about.tip=有关 Pixel Perfect 的信息 6 | pixelPerfect.menu.visitHomePage.label=访问主页... 7 | pixelPerfect.menu.visitHomePage.tip=详细了解 Pixel Perfect 8 | pixelPerfect.menu.reportIssue.label=报告问题... 9 | pixelPerfect.menu.reportIssue.tip=您发现了一个 bug 或者您需要一个新功能?让我们知道! 10 | pixelPerfect.menu.group.label=讨论组... 11 | pixelPerfect.menu.group.tip=打开讨论组网站(与 Firebug 共享) 12 | -------------------------------------------------------------------------------- /chrome/skin/classic/shared/logo_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebug/pixel-perfect/8106cbfecb9a9a59544fa172f6d12cb9c924109c/chrome/skin/classic/shared/logo_16x16.png -------------------------------------------------------------------------------- /chrome/skin/classic/shared/logo_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebug/pixel-perfect/8106cbfecb9a9a59544fa172f6d12cb9c924109c/chrome/skin/classic/shared/logo_32x32.png -------------------------------------------------------------------------------- /chrome/skin/classic/shared/logo_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebug/pixel-perfect/8106cbfecb9a9a59544fa172f6d12cb9c924109c/chrome/skin/classic/shared/logo_64x64.png -------------------------------------------------------------------------------- /chrome/skin/classic/shared/resizer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebug/pixel-perfect/8106cbfecb9a9a59544fa172f6d12cb9c924109c/chrome/skin/classic/shared/resizer.png -------------------------------------------------------------------------------- /chrome/skin/classic/shared/ua.css: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | div:-moz-native-anonymous .pixelperfect-layer-box { 4 | position: absolute; 5 | cursor: move; 6 | pointer-events: auto; 7 | line-height: 1px; 8 | } 9 | 10 | div:-moz-native-anonymous .pixelperfect-layer-box[drag] { 11 | outline: 1px dashed rgb(51, 153, 255); 12 | } 13 | 14 | div:-moz-native-anonymous .pixelperfect-layer-box[lock] { 15 | cursor: default; 16 | pointer-events: none; 17 | } 18 | 19 | div:-moz-native-anonymous .pixelperfect-layer-box[invert] img { 20 | filter: invert(100%); 21 | } 22 | 23 | /* The following two styles are fixing issue #67 24 | https://github.com/firebug/pixel-perfect/issues/67 25 | They can both be removed as soon as the platform is fixed: 26 | https://bugzilla.mozilla.org/show_bug.cgi?id=1168113 27 | 28 | This gets yet more complex since there is currently no 29 | CSS way how to avoid fixed position: 30 | https://bugzilla.mozilla.org/show_bug.cgi?id=1230508#c26 31 | The only way is using 'scroll' events and update position 32 | of layers manually. 33 | 34 | The CSS is currently using position: fixed (in addition to 35 | the manual scroll-offset calculation). As soon as 'absolute' 36 | is supported again it should be removed */ 37 | div:-moz-native-anonymous.moz-custom-content-container { 38 | /*position: absolute;*/ 39 | position: fixed; 40 | } 41 | 42 | :-moz-native-anonymous .highlighter-container { 43 | position: fixed; 44 | } 45 | -------------------------------------------------------------------------------- /data/config.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | // RequireJS configuration 4 | require.config({ 5 | baseUrl: ".", 6 | paths: { 7 | "react": "./lib/react/react", 8 | "reps": "../../node_modules/firebug.sdk/lib/reps", 9 | } 10 | }); 11 | 12 | // Load the main panel module 13 | requirejs(["popup"]); 14 | -------------------------------------------------------------------------------- /data/img/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 16 | 18 | 22 | 26 | 30 | 34 | 35 | 37 | 41 | 45 | 46 | 54 | 62 | 69 | 72 | 73 | 82 | 91 | 98 | 101 | 102 | 103 | 105 | 106 | 108 | image/svg+xml 109 | 111 | 112 | 113 | 114 | 115 | 124 | 128 | 132 | 133 | -------------------------------------------------------------------------------- /data/img/slider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebug/pixel-perfect/8106cbfecb9a9a59544fa172f6d12cb9c924109c/data/img/slider.png -------------------------------------------------------------------------------- /data/layer-form.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define(function(require, exports, module) { 4 | 5 | // Dependencies 6 | const React = require("react"); 7 | const { Reps } = require("reps/reps"); 8 | const { LayerStore } = require("layer-store"); 9 | 10 | // Shortcuts 11 | const { SPAN, TABLE, TR, TD, BUTTON, INPUT, DIV, LABEL } = Reps.DOM; 12 | 13 | /** 14 | * @react This template implements a form allowing to see and modify 15 | * Layer properties. Every modification is immediately propagated to 16 | * the storage {@link PixelPerfectStore}. Since the storage object lives 17 | * inside the chrome scope the access is done through a proxy object 18 | * {@link LayerStore} that sends appropriate JSON messages using message 19 | * manager. 20 | */ 21 | var LayerForm = React.createClass({ 22 | getInitialState: function() { 23 | return {}; 24 | }, 25 | 26 | componentWillReceiveProps: function(nextProps) { 27 | this.setState(nextProps.layer); 28 | }, 29 | 30 | render: function() { 31 | var layer = this.props.layer; 32 | return ( 33 | TABLE({className: "form"}, 34 | TR({}, 35 | TD({className: "right"}, 36 | LABEL({className: "pixel-perfect-label", 37 | htmlFor: "pixel-perfect-opacity"}, 38 | Locale.$STR("pixelPerfect.label.opacity") + ":" 39 | ) 40 | ), 41 | TD({}, 42 | INPUT({className: "opacity", type: "range", value: layer.opacity, 43 | id: "pixel-perfect-opacity", 44 | onChange: this.onChange.bind(this, "opacity", "string")}) 45 | ), 46 | TD({}, 47 | INPUT({className: "opacity-value", size: 3, value: layer.opacity, 48 | maxLength: 3, 49 | onChange: this.onChange.bind(this, "opacity", "number")}) 50 | ) 51 | ), 52 | TR({}, 53 | TD({className: "right"}, 54 | LABEL({className: "pixel-perfect-label", 55 | htmlFor: "pixel-perfect-x"}, 56 | Locale.$STR("pixelPerfect.label.x") + ":") 57 | ), 58 | TD({className: "positionCell", colSpan: 2}, 59 | TABLE({className: "position"}, 60 | TR({}, 61 | TD({}, 62 | INPUT({size: 5, value: layer.x, type: "number", 63 | id: "pixel-perfect-x", 64 | onChange: this.onChange.bind(this, "x", "number")}) 65 | ), 66 | TD({className: "right"}, 67 | LABEL({className: "pixel-perfect-label", 68 | htmlFor: "pixel-perfect-y"}, 69 | Locale.$STR("pixelPerfect.label.y") + ":") 70 | ), 71 | TD({}, 72 | INPUT({size: 5, value: layer.y, type: "number", 73 | id: "pixel-perfect-y", 74 | onChange: this.onChange.bind(this, "y", "number")}) 75 | ) 76 | ) 77 | ) 78 | ) 79 | ), 80 | TR({}, 81 | TD({className: "right"}, 82 | LABEL({className: "pixel-perfect-label", 83 | htmlFor: "pixel-perfect-scale"}, 84 | Locale.$STR("pixelPerfect.label.scale") + ":") 85 | ), 86 | TD({colSpan: 2}, 87 | INPUT({size: 3, value: layer.scale, type: "number", step: "0.1", 88 | id: "pixel-perfect-scale", 89 | onChange: this.onChange.bind(this, "scale", "float")}) 90 | ) 91 | ), 92 | TR({}, 93 | TD({className: "right"}, 94 | LABEL({className: "pixel-perfect-label", 95 | htmlFor: "pixel-perfect-lock"}, 96 | Locale.$STR("pixelPerfect.label.lock") + ":") 97 | ), 98 | TD({colSpan: 2}, 99 | INPUT({type: "checkbox", checked: layer.lock, 100 | id: "pixel-perfect-lock", 101 | onChange: this.onChange.bind(this, "lock", "boolean")}) 102 | ) 103 | ), 104 | TR({}, 105 | TD({className: "right"}, 106 | LABEL({className: "pixel-perfect-label", 107 | htmlFor: "pixel-perfect-invert"}, 108 | Locale.$STR("pixelPerfect.label.invert") + ":") 109 | ), 110 | TD({colSpan: 2}, 111 | INPUT({type: "checkbox", checked: layer.invert, 112 | id: "pixel-perfect-invert", 113 | onChange: this.onChange.bind(this, "invert", "boolean")}) 114 | ) 115 | ), 116 | TR({}, 117 | TD({colSpan: 3}, 118 | DIV({className: "url"}, layer.url) 119 | ) 120 | ) 121 | ) 122 | ); 123 | }, 124 | 125 | // Events 126 | 127 | /** 128 | * Handler for changes made through the form's input fields. 129 | * State of the current layer is updated and changed propagated 130 | * to the {@link LayerStore}. 131 | */ 132 | onChange: function(propName, type, event) { 133 | var value; 134 | 135 | switch (type) { 136 | case "boolean": 137 | value = event.target.checked; 138 | break; 139 | case "number": 140 | value = parseInt(event.target.value, 10); 141 | break; 142 | case "float": 143 | var v = event.target.value; 144 | value = (v != "0.") ? parseFloat(v, 10) : v; 145 | break; 146 | case "string": 147 | value = event.target.value; 148 | break; 149 | } 150 | 151 | if (this.state[propName] === value) { 152 | return; 153 | } 154 | 155 | // Make sure the UI is updated. 156 | this.state[propName] = value; 157 | this.setState(this.state); 158 | 159 | var props = {}; 160 | props[propName] = value; 161 | 162 | // Immediately update modified layer inside the store object. 163 | // The {@link LayerStore} object is used as a proxy to the real 164 | // storage object that lives in the chrome scope. 165 | LayerStore.modify(this.props.layer.id, props); 166 | }, 167 | }); 168 | 169 | // Exports from this module 170 | exports.LayerForm = React.createFactory(LayerForm); 171 | }); 172 | -------------------------------------------------------------------------------- /data/layer-list.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define(function(require, exports, module) { 4 | 5 | // Dependencies 6 | const React = require("react"); 7 | const { Reps } = require("reps/reps"); 8 | const { LayerStore } = require("layer-store"); 9 | 10 | // Shortcuts 11 | const { TABLE, TBODY, TR, TD, INPUT, IMG, THEAD, TH, DIV } = Reps.DOM; 12 | 13 | /** 14 | * @react This template is responsible for displaying list of registered 15 | * layers. The user can append new as well as remove an existing layer 16 | * from/to the list. It's also possible to change order of existing layers 17 | * through drag and drop. 18 | */ 19 | var LayerList = React.createClass({ 20 | getInitialState: function() { 21 | return { 22 | layers: this.props.layers, 23 | selection: this.props.selection 24 | }; 25 | }, 26 | 27 | componentWillReceiveProps: function(nextProps) { 28 | this.setState({ 29 | layers: nextProps.layers, 30 | selection: nextProps.selection 31 | }); 32 | }, 33 | 34 | render: function() { 35 | var rows = []; 36 | var layers = this.state.layers; 37 | 38 | layers.forEach(layer => { 39 | rows.push(LayerRow({ 40 | key: layer.id, 41 | layer: layer, 42 | selected: layer.id == this.props.selection, 43 | selectLayer: this.props.selectLayer.bind(this, layer), 44 | removeLayer: this.props.removeLayer.bind(this, layer), 45 | dragStart: this.dragStart, 46 | dragEnd: this.dragEnd, 47 | })); 48 | }); 49 | 50 | // Add one extra row that displays a button for appending new layers. 51 | rows.push(AddLayerRow({ 52 | addLayer: this.props.addLayer 53 | })); 54 | 55 | return ( 56 | TABLE({className: "layerTable", onDragOver: this.dragOver}, 57 | THEAD({className: "poolRow"}, 58 | TH({width: "20px"}), 59 | TH({width: "96px"}) 60 | ), 61 | TBODY(null, rows) 62 | ) 63 | ); 64 | }, 65 | 66 | // Drag And Drop 67 | 68 | dragStart: function(event) { 69 | // Drag operation can only start by dragging layer image. 70 | var target = event.currentTarget; 71 | if (target.classList.contains("layerImage")) { 72 | this.dragged = event.currentTarget; 73 | event.dataTransfer.effectAllowed = "copyMove"; 74 | 75 | // Firefox requires calling dataTransfer.setData 76 | // for the drag to properly work 77 | event.dataTransfer.setData("text/html", null); 78 | } 79 | }, 80 | 81 | dragOver: function(event) { 82 | // Bail out if no drag in progress at this moment. 83 | if (!this.dragged) { 84 | return; 85 | } 86 | 87 | // Drop operation can only happen on an existing layer image. 88 | var target = event.target; 89 | if (!target.classList.contains("layerImage")) { 90 | return; 91 | } 92 | 93 | event.preventDefault(); 94 | 95 | // Ignore the beginning of the drag operation when the mouse is 96 | // hovering over the clicked layer. 97 | if (target == this.dragged) { 98 | return; 99 | } 100 | 101 | // Check if the mouse is hovering close to the target layer 102 | // image center. If yes, let's perform the drag-move operation 103 | // i.g. move the source layer into a new position in the list. 104 | var y = event.clientY; 105 | var rect = target.getBoundingClientRect(); 106 | var delta = (rect.height / 3); 107 | if (!(y > rect.top + delta && y < rect.bottom - delta)) { 108 | return; 109 | } 110 | 111 | var originalIndex = this.getLayerIndexById(this.dragged.dataset.id); 112 | var targetIndex = this.getLayerIndexById(target.dataset.id); 113 | 114 | // Move the dragging layer into a new position in the layers list 115 | // and ensure UI rendering by setting a new state. 116 | var layers = this.state.layers; 117 | var removed = layers.splice(originalIndex, 1)[0]; 118 | layers.splice(targetIndex, 0, removed); 119 | 120 | // Set new state (dragged image is at new index in the array). 121 | this.setState({layers: layers}); 122 | 123 | LayerStore.move(originalIndex, targetIndex); 124 | }, 125 | 126 | dragEnd: function(event) { 127 | // Nothing special here, the dragged image is already moved 128 | // into a new location during the drag-over event. 129 | }, 130 | 131 | // Helpers 132 | 133 | getLayerIndexById: function(id) { 134 | return this.state.layers.findIndex(layer => { 135 | return (layer.id == id); 136 | }) 137 | } 138 | }); 139 | 140 | /** 141 | * @react This template renders one layer (row) in the list. 142 | * Every layer is rendered as a small thumbnail displaying 143 | * the associated image. The thumbnail also displays a close 144 | * button when hovered by mouse. This button allows to remove 145 | * the layer. 146 | */ 147 | var LayerRow = React.createFactory(React.createClass({ 148 | getInitialState: function() { 149 | return { 150 | layer: {}, 151 | selected: false 152 | }; 153 | }, 154 | 155 | componentWillReceiveProps: function(nextProps) { 156 | this.setState(nextProps); 157 | }, 158 | 159 | render: function() { 160 | var layer = this.props.layer; 161 | var imageUrl = layer.dataUrl; 162 | var selected = this.props.selected ? " selected" : ""; 163 | 164 | return ( 165 | TR({className: "layerRow", onClick: this.props.selectLayer}, 166 | TD({className: "layerCell"}, 167 | INPUT({className: "visibility", type: "checkbox", 168 | checked: layer.visible, onChange: this.onVisibleChange}) 169 | ), 170 | TD({className: "layerCell"}, 171 | DIV({className: "layerImageBox" + selected}, 172 | IMG({className: "layerImage img-thumbnail", src: imageUrl, 173 | key: layer.id, "data-id": layer.id, 174 | onDragStart: this.props.dragStart, 175 | onDragEnd: this.props.dragEnd}), 176 | DIV({className: "closeButton", onClick: this.onRemove}) 177 | ) 178 | ) 179 | ) 180 | ) 181 | }, 182 | 183 | onRemove: function(event) { 184 | // Cancel the event to avoid selection of the to be removed layer. 185 | event.stopPropagation(); 186 | event.preventDefault(); 187 | 188 | // Execute provided callback, it's already bound with 189 | // a layer object associated with this row. 190 | this.props.removeLayer(); 191 | }, 192 | 193 | onVisibleChange: function(event) { 194 | var value = event.target.checked; 195 | 196 | this.state.layer["visible"] = value; 197 | this.setState(this.state); 198 | 199 | var props = { visible: value }; 200 | LayerStore.modify(this.props.layer.id, props); 201 | }, 202 | })); 203 | 204 | /** 205 | * @react This template renders a button for adding a new layer. 206 | */ 207 | var AddLayerRow = React.createFactory(React.createClass({ 208 | render: function() { 209 | return ( 210 | TR({className: "layerRow", onClick: this.props.onSelect}, 211 | TD({className: "layerCell"}), 212 | TD({className: "layerCell"}, 213 | DIV({className: "layerImageBox"}, 214 | DIV({className: "layerImage add img-thumbnail"}, 215 | DIV({onClick: this.props.addLayer}, 216 | Locale.$STR("pixelPerfect.label.addLayer") 217 | ) 218 | ) 219 | ) 220 | ) 221 | ) 222 | ) 223 | }, 224 | })); 225 | 226 | // Exports from this module 227 | exports.LayerList = React.createFactory(LayerList); 228 | }); 229 | -------------------------------------------------------------------------------- /data/layer-store.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | "use strict"; 4 | 5 | define(function(require, exports, module) { 6 | 7 | /** 8 | * This object represents a proxy to the real Store object in the chrome 9 | * scope {@link PixelPerfectStorage}. All API calls are forwarded through 10 | * a message manager (as asynchronous events) to the real store object. 11 | */ 12 | const LayerStore = 13 | /** @lends LayerStore */ 14 | { 15 | /** 16 | * Modify properties of an existing layer. 17 | */ 18 | modify: function(id, props) { 19 | postChromeMessage("modify", [id, props]); 20 | }, 21 | 22 | /** 23 | * Add a new layer. 24 | */ 25 | add: function() { 26 | postChromeMessage("add"); 27 | }, 28 | 29 | /** 30 | * Remove an existing layer. 31 | */ 32 | remove: function(id) { 33 | postChromeMessage("remove", [id]); 34 | }, 35 | 36 | /** 37 | * Change index (order) of an existing layer in the list. Move it 38 | * from the index 'from' to new index 'to'. 39 | */ 40 | move: function(from, to) { 41 | postChromeMessage("move", [from, to]); 42 | }, 43 | }; 44 | 45 | // Exports from this module 46 | exports.LayerStore = LayerStore; 47 | }); 48 | -------------------------------------------------------------------------------- /data/notification-config.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | // RequireJS configuration 4 | require.config({ 5 | baseUrl: ".", 6 | paths: { 7 | "react": "./lib/react/react", 8 | "reps": "../../node_modules/firebug.sdk/lib/reps" 9 | } 10 | }); 11 | 12 | // Load the main panel module 13 | requirejs(["notification"]); 14 | -------------------------------------------------------------------------------- /data/notification-content.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | /** 4 | * Content script for notification panel. It's only purpose is registering 5 | * a listener for 'click' event and sending a message to the chrome scope 6 | * if the user clicks on 'Start Pixel Perfect' link. 7 | */ 8 | window.addEventListener("click", function(event) { 9 | var target = event.target; 10 | if (target.id == "start") { 11 | self.port.emit("start"); 12 | } 13 | }); 14 | 15 | /** 16 | * Receive localized strings from the chrome scope. These strings are 17 | * used in the notification panel UI. 18 | */ 19 | self.port.on("locales", function(locales) { 20 | document.body.setAttribute("data-locales", JSON.stringify(locales)); 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /data/notification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /data/notification.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define(function(require, exports, module) { 4 | 5 | // Dependencies 6 | const React = require("react"); 7 | const { Reps } = require("reps/reps"); 8 | 9 | // Shortcuts 10 | const { TABLE, TR, TD, DIV, IMG, SPAN, BR } = Reps.DOM; 11 | 12 | /** 13 | * @react This template implements UI for the first-run notification panel. 14 | * The panel displays basic information about the extension and a link 15 | * that can be used to open Pixel Perfect popup panel 16 | * {@link PixelPerfectPopup}. 17 | */ 18 | var NotificationContent = React.createFactory(React.createClass({ 19 | render: function() { 20 | var style = { 21 | "textAlign": "justify" 22 | } 23 | 24 | return ( 25 | TABLE({className: "defaultContentTable"}, 26 | TR({}, 27 | TD({width: "10px;", style: style}, 28 | IMG({className: "defaultContentImage", 29 | src: "chrome://pixelperfect/skin/logo_32x32.png"}) 30 | ), 31 | TD({className: "defaultContentHeader", style: style}, 32 | SPAN({}, this.props.title) 33 | ) 34 | ), 35 | TR({}, 36 | TD({colSpan: 2, style: style}, 37 | DIV({className: "defaultContentDesc"}, this.props.welcome), 38 | BR(), 39 | DIV({className: "defaultContentDesc"}, this.props.description) 40 | ) 41 | ), 42 | TR({}, 43 | TD({colSpan: 2}, 44 | DIV({className: "layerImage add img-thumbnail"}, 45 | DIV({id: "start"}, this.props.start) 46 | ) 47 | ) 48 | ) 49 | ) 50 | ) 51 | }, 52 | })); 53 | 54 | // Get localized strings from body dataset and render the panel UI. 55 | // The strings are set into the body element from the chrome scope. 56 | // See {@link FirstRun#initialize} method for more details. 57 | var locales = JSON.parse(document.body.dataset.locales); 58 | React.render(NotificationContent(locales), document.body); 59 | }); 60 | -------------------------------------------------------------------------------- /data/popup-frame-script.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | "use strict"; 4 | 5 | (function({ 6 | content, 7 | addMessageListener, 8 | sendAsyncMessage, 9 | removeMessageListener, 10 | addEventListener}) { 11 | 12 | const Cu = Components.utils; 13 | const Cc = Components.classes; 14 | const Ci = Components.interfaces; 15 | 16 | const document = content.document; 17 | const window = content; 18 | 19 | /** 20 | * Listener for message from the inspector panel (chrome scope). 21 | * It's further distributed as DOM event, so it can be handled by 22 | * the page content script. 23 | */ 24 | function messageListener(message) { 25 | const { type, data, origin, bubbles, cancelable } = message.data; 26 | 27 | const event = new window.MessageEvent(type, { 28 | bubbles: bubbles, 29 | cancelable: cancelable, 30 | data: data, 31 | origin: origin, 32 | target: window, 33 | source: window, 34 | }); 35 | 36 | window.dispatchEvent(event); 37 | }; 38 | 39 | addMessageListener("pixelperfect/event/message", messageListener); 40 | 41 | /** 42 | * Send a message back to the parent panel (chrome scope). 43 | */ 44 | function postChromeMessage(type, args, objects) { 45 | let data = { 46 | type: type, 47 | args: args, 48 | }; 49 | 50 | sendAsyncMessage("message", data, objects); 51 | } 52 | 53 | /** 54 | * Export 'postChromeMessage' function to the frame content as soon 55 | * as it's loaded. This function allows sending messages from the 56 | * frame's content directly to the chrome scope. 57 | */ 58 | addEventListener("DOMContentLoaded", function onContentLoaded(event) { 59 | removeEventListener("DOMContentLoaded", onContentLoaded, true); 60 | 61 | Cu.exportFunction(postChromeMessage, window, { 62 | defineAs: "postChromeMessage" 63 | }); 64 | }, true); 65 | 66 | // End of scope 67 | })(this); 68 | -------------------------------------------------------------------------------- /data/popup-panel.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define(function(require, exports, module) { 4 | 5 | // Dependencies 6 | const React = require("react"); 7 | const { Reps } = require("reps/reps"); 8 | const { LayerList } = require("layer-list"); 9 | const { LayerForm } = require("layer-form"); 10 | const { LayerStore } = require("layer-store"); 11 | 12 | // Shortcuts 13 | const { TABLE, TR, TD, DIV, IMG, SPAN } = Reps.DOM; 14 | 15 | /** 16 | * @react This template implements basic layout for the popup panel. 17 | * There are two components displayed: 18 | * 1. Layer list: list of all registered layers {@link LayerList}. 19 | * 2. Layer form: a form displaying properties of the selected layer 20 | * {@link LayerForm}. 21 | * 22 | * If there are no layers, the popup panel displays default content 23 | * with instructions and one button: 'Add Layer' {@link DefaultContent}. 24 | */ 25 | var PopupPanel = React.createClass({ 26 | getInitialState: function() { 27 | return { 28 | layers: [], 29 | selection: null, 30 | }; 31 | }, 32 | 33 | render: function() { 34 | var layers = this.state.layers; 35 | var selectedLayer = this.getLayer(this.state.selection); 36 | 37 | // If there are no layers, display default content with instructions 38 | // about how to create one. 39 | if (!layers || !layers.length) { 40 | return DefaultContent({ 41 | version: this.state.version, 42 | addLayer: this.addLayer 43 | }); 44 | } 45 | 46 | // Render list of layers and a form component. 47 | return ( 48 | TABLE({className: "popupPanelTable"}, 49 | TR({}, 50 | TD({className: "layerListCell"}, 51 | DIV({className: "layerList"}, 52 | LayerList({ 53 | layers: this.state.layers, 54 | selection: this.state.selection, 55 | selectLayer: this.selectLayer, 56 | addLayer: this.addLayer, 57 | removeLayer: this.removeLayer 58 | }) 59 | ) 60 | ), 61 | TD({className: "layerFormCell"}, 62 | DIV({className: "layerForm"}, 63 | LayerForm({ 64 | layer: selectedLayer 65 | }) 66 | ) 67 | ) 68 | ) 69 | ) 70 | ) 71 | }, 72 | 73 | getLayer: function(id) { 74 | var layers = this.state.layers; 75 | for (var i=0; i 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /data/popup.js: -------------------------------------------------------------------------------- 1 | /* See license.txt for terms of usage */ 2 | 3 | define(function(require, exports, module) { 4 | 5 | // Dependencies 6 | const React = require("react"); 7 | const { PopupPanel } = require("popup-panel"); 8 | const { LayerStore } = require("layer-store"); 9 | 10 | var panel; 11 | 12 | /** 13 | * Handle refresh events sent from the chrome scope and refresh 14 | * the panel content. The data attached to the event represents 15 | * new state (or state changes) for the panel component. 16 | */ 17 | window.addEventListener("refresh", event => { 18 | var state = JSON.parse(event.data); 19 | 20 | // Initial panel content rendering. 21 | if (!panel) { 22 | panel = React.render(PopupPanel(), document.body); 23 | } 24 | 25 | // Merge new state properties into the current state. 26 | state.layers = state.layers || panel.state.layers; 27 | state.selection = state.selection || panel.state.selection; 28 | 29 | // Update default selection if necessary (the current selected 30 | // layer might be removed). 31 | state.selection = ensureSelection(state.layers, state.selection); 32 | 33 | // Finally, update the UI panel component. 34 | panel.setState(state); 35 | 36 | postChromeMessage("panel-refreshed"); 37 | }); 38 | 39 | /** 40 | * Display the page content when it's ready to avoid flashing 41 | * during the page load. 42 | * 43 | * The popup frame has limited privileges (type='content') and 44 | * can't access local files directly. Source of all images in the 45 | * layer list is set to 'data:' URLs if one of those URLs would 46 | * be wrong the document would never finished loading (or after 47 | * a long timeout?) and the content would stay empty (collapsed). 48 | */ 49 | document.addEventListener("load", event => { 50 | document.body.removeAttribute("collapsed"); 51 | onResize(); 52 | }, true); 53 | 54 | /** 55 | * Update height of the layer list according to the current height of 56 | * the document. 57 | * 58 | * xxxHonza: can we ensure that the main table (popupPanelTable) has the 59 | * right height that's equal to the height of the document and not bigger 60 | * using just CSS? 61 | */ 62 | function onResize(event) { 63 | var table = document.querySelector(".layerList"); 64 | if (table) { 65 | table.setAttribute("style", "height: " + document.body.clientHeight + "px;"); 66 | } 67 | }; 68 | 69 | window.addEventListener("resize", onResize); 70 | 71 | // Helpers 72 | 73 | function ensureSelection(layers, id) { 74 | for (var i=0; i {}}; 73 | 74 | /** 75 | * Helper actor state watcher. 76 | */ 77 | function expectState(expectedState, method) { 78 | return function(...args) { 79 | if (this.state !== expectedState) { 80 | Trace.sysout("actor.expectState; ERROR wrong state, expected '" + 81 | expectedState + "', but current state is '" + this.state + "'" + 82 | ", method: " + method); 83 | 84 | let msg = "Wrong State: Expected '" + expectedState + "', but current " + 85 | "state is '" + this.state + "'"; 86 | 87 | return Promise.reject(new Error(msg)); 88 | } 89 | 90 | try { 91 | return method.apply(this, args); 92 | } catch (err) { 93 | Cu.reportError("actor.js; expectState EXCEPTION " + err, err); 94 | } 95 | }; 96 | } 97 | 98 | /** 99 | * @actor This object represents an actor that is dynamically injected 100 | * (registered) to the debuggee target (back-end). The debuggee target 101 | * can be a running instance of the browser on local machine or remote 102 | * device such as mobile phone. The communication with this object is 103 | * always done through RDP (Remote Debugging Protocol). Read more about 104 | * {@link https://wiki.mozilla.org/Remote_Debugging_Protocol|RDP}. 105 | * 106 | * This object implements the following logic that runs on that back-end: 107 | * 1. Rendering of registered layers on top of the current page 108 | * 2. Drag and drop of layers (changing location on the page). This user 109 | * operation fires events back to the chrome scope, so the UI (coordinates) 110 | * can be properly updated. 111 | * 112 | * Layers are not inserted into the page. They exist inside 113 | * a 'Canvas Frame' that is overlapping the page to avoid page DOM 114 | * modification (which can be dangerous). The Canvas Frame is supported 115 | * by the platform and originally used by HTML Inspector highlighter. 116 | */ 117 | var PixelPerfectActor = ActorClass( 118 | /** @lends PixelPerfectActor */ 119 | { 120 | typeName: "pixelPerfectActor", 121 | 122 | /** 123 | * Events emitted by this actor. 124 | */ 125 | events: { 126 | "dragstart": { data: Arg(0, "json") }, 127 | "drag": { data: Arg(0, "json") }, 128 | "dragend": { data: Arg(0, "json") } 129 | }, 130 | 131 | // Initialization 132 | 133 | initialize: function(conn, parent) { 134 | Trace.sysout("PixelPerfectActor.initialize; parent: " + 135 | parent.actorID + ", conn: " + conn.prefix, this); 136 | 137 | Actor.prototype.initialize.call(this, conn); 138 | 139 | this.parent = parent; 140 | this.state = "detached"; 141 | this.layers = []; 142 | 143 | // Layers (displayed in anonymous canvas content) need to be recreated 144 | // when page navigation happens. 145 | this.onNavigate = this.onNavigate.bind(this); 146 | this.onWillNavigate = this.onWillNavigate.bind(this); 147 | 148 | // Event handlers 149 | this.onMouseDown = this.onMouseDown.bind(this); 150 | this.onMouseMove = this.onMouseMove.bind(this); 151 | this.onMouseUp = this.onMouseUp.bind(this); 152 | this.onMouseClick = this.onMouseClick.bind(this); 153 | this.onScroll = this.onScroll.bind(this); 154 | }, 155 | 156 | /** 157 | * The destroy is only called automatically by the framework (parent actor) 158 | * if an actor is instantiated by a parent actor. 159 | */ 160 | destroy: function() { 161 | Trace.sysout("PixelPerfectActor.destroy; state: " + this.state, arguments); 162 | 163 | if (this.state === "attached") { 164 | this.detach(); 165 | } 166 | 167 | Actor.prototype.destroy.call(this); 168 | }, 169 | 170 | /** 171 | * Automatically executed by the framework when the parent connection 172 | * is closed. 173 | */ 174 | disconnect: function() { 175 | Trace.sysout("PixelPerfectActor.disconnect; state: " + this.state, arguments); 176 | 177 | if (this.state === "attached") { 178 | this.detach(); 179 | } 180 | }, 181 | 182 | /** 183 | * Attach to this actor. Executed when the front (client) is attaching 184 | * to this actor. 185 | */ 186 | attach: method(expectState("detached", function() { 187 | Trace.sysout("PixelPerfectActor.attach;", arguments); 188 | 189 | this.state = "attached"; 190 | 191 | Events.on(this.parent, "navigate", this.onNavigate); 192 | Events.on(this.parent, "will-navigate", this.onWillNavigate); 193 | }), { 194 | request: {}, 195 | response: { 196 | type: "attached" 197 | } 198 | }), 199 | 200 | /** 201 | * Set UI stylesheet for anonymous content (sent from the client). 202 | */ 203 | loadSheet: method(expectState("attached", function(source) { 204 | Trace.sysout("PixelPerfectActor.loadSheet;", arguments); 205 | 206 | this.uaSource = source; 207 | 208 | this.buildAnonumousContent(); 209 | }), { 210 | request: { 211 | source: Arg(0, "string") 212 | }, 213 | response: { 214 | type: "sheet-loaded" 215 | } 216 | }), 217 | 218 | /** 219 | * Detach from this actor. Executed when the front (client) detaches 220 | * from this actor. 221 | */ 222 | detach: method(expectState("attached", function() { 223 | Trace.sysout("PixelPerfectActor.detach;", arguments); 224 | 225 | this.state = "detached"; 226 | this.destroyLayers(); 227 | 228 | this.layers = []; 229 | 230 | // Remove tab actor listeners 231 | Events.off(this.parent, "navigate", this.onNavigate); 232 | Events.off(this.parent, "will-navigate", this.onWillNavigate); 233 | 234 | let win = this.parent.window; 235 | let doc = win.document; 236 | 237 | // Remove drag-drop listeners 238 | DomEvents.removeListener(doc, "mousedown", this.onMouseDown); 239 | DomEvents.removeListener(doc, "mousemove", this.onMouseMove); 240 | DomEvents.removeListener(doc, "mouseup", this.onMouseUp); 241 | DomEvents.removeListener(doc, "click", this.onMouseClick); 242 | DomEvents.removeListener(doc, "scroll", this.onScroll); 243 | }), { 244 | request: {}, 245 | response: { 246 | type: "detached" 247 | } 248 | }), 249 | 250 | // Remote Actor API 251 | 252 | addLayer: method(function(layer) { 253 | Trace.sysout("PixelPerfectActor.addLayer; " + layer.id, layer); 254 | 255 | this.layers.push(layer); 256 | this.buildLayer(layer); 257 | }, { 258 | request: { 259 | layer: Arg(0, "json") 260 | }, 261 | response: { 262 | type: "layer-added" 263 | } 264 | }), 265 | 266 | removeLayer: method(function(id) { 267 | Trace.sysout("PixelPerfectActor.removeLayer; " + id); 268 | 269 | let layer = this.getLayer(id); 270 | this.destroyLayer(layer); 271 | 272 | for (let i=0; i { 384 | this.buildAnonumousContent(); 385 | }, 500); 386 | } 387 | }, 388 | 389 | onWillNavigate: function({isTopLevel}) { 390 | Trace.sysout("PixelPerfectActor.onWillNavigate; " + isTopLevel); 391 | 392 | if (isTopLevel) { 393 | // Remove anonymous content created for this page. 394 | this.destroyLayers(); 395 | } 396 | }, 397 | 398 | // Anonymous Content Builders (canvas frame) 399 | 400 | buildAnonumousContent: function() { 401 | let win = this.parent.window; 402 | if (win.closed) { 403 | return; 404 | } 405 | 406 | let doc = win.document; 407 | 408 | // For now css is injected in content as a user agent sheet because 409 | //