├── .babelrc ├── .gitignore ├── chrome-extension ├── devtools.js ├── devtools.html ├── panel.html ├── manifest.json ├── content-script.js └── background.js ├── src ├── agent │ ├── util │ │ ├── sendMessage.js │ │ └── serializeEntity.js │ ├── index.js │ ├── patchEntities.js │ └── Agent.js ├── ui │ ├── port.js │ ├── util │ │ └── sendMessage.js │ ├── actions │ │ ├── index.js │ │ ├── EntityActions.js │ │ └── GameActions.js │ ├── stores │ │ ├── index.js │ │ ├── ConnectionStore.js │ │ ├── EntityStore.js │ │ └── GameStore.js │ ├── components │ │ ├── ListArrow.js │ │ ├── EntityPropertyInput.js │ │ ├── Entity.js │ │ ├── EntityList.js │ │ ├── Main.js │ │ ├── EntityPropertiesList.js │ │ ├── GameControls.js │ │ └── EntityProperty.js │ ├── index.js │ ├── injectDebugger.js │ └── AgentHandler.js └── common │ └── deepUpdate.js ├── .jshintrc ├── style ├── entity-list.less └── main.less ├── package.json ├── webpack.config.js └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | chrome-extension/build/ 3 | -------------------------------------------------------------------------------- /chrome-extension/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create('Coquette', null, 'panel.html'); 2 | -------------------------------------------------------------------------------- /chrome-extension/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/agent/util/sendMessage.js: -------------------------------------------------------------------------------- 1 | var sendMessage = function(name, data) { 2 | window.postMessage({ 3 | source: 'coquette-inspect-agent', 4 | name: name, 5 | data: data || {} 6 | }, '*'); 7 | }; 8 | 9 | module.exports = sendMessage; 10 | -------------------------------------------------------------------------------- /src/ui/port.js: -------------------------------------------------------------------------------- 1 | var backgroundPageConnection = chrome.runtime.connect({ 2 | name: 'panel' 3 | }); 4 | 5 | backgroundPageConnection.postMessage({ 6 | name: 'init', 7 | tabId: chrome.devtools.inspectedWindow.tabId 8 | }); 9 | 10 | module.exports = backgroundPageConnection; 11 | -------------------------------------------------------------------------------- /src/ui/util/sendMessage.js: -------------------------------------------------------------------------------- 1 | var port = require('../port'); 2 | 3 | var sendMessage = function(name, data) { 4 | port.postMessage({ 5 | name: name, 6 | tabId: chrome.devtools.inspectedWindow.tabId, 7 | data: data || {} 8 | }); 9 | }; 10 | 11 | module.exports = sendMessage; 12 | -------------------------------------------------------------------------------- /chrome-extension/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ui/actions/index.js: -------------------------------------------------------------------------------- 1 | var EntityActions = require('./EntityActions'); 2 | var GameActions = require('./GameActions'); 3 | 4 | module.exports = { 5 | entities: EntityActions, 6 | game: GameActions, 7 | 8 | didConnect: function() { 9 | this.dispatch('connected'); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "indent": 2, 5 | "nonew": true, 6 | "undef": true, 7 | "unused": true, 8 | "sub": true, 9 | 10 | "browser": true, 11 | "node": true, 12 | "devel": true, 13 | 14 | "esnext": true, 15 | 16 | "globals": { 17 | "chrome": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/stores/index.js: -------------------------------------------------------------------------------- 1 | var EntityStore = require('./EntityStore'); 2 | var GameStore = require('./GameStore'); 3 | var ConnectionStore = require('./ConnectionStore'); 4 | 5 | module.exports = { 6 | EntityStore: new EntityStore(), 7 | GameStore: new GameStore(), 8 | ConnectionStore: new ConnectionStore() 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /src/common/deepUpdate.js: -------------------------------------------------------------------------------- 1 | var deepUpdate = function(root, path, value) { 2 | var obj = root; 3 | 4 | if (path.length > 1) { 5 | obj = path.slice(0, path.length-1).reduce((last, piece) => { 6 | return last[piece]; 7 | }, root); 8 | } 9 | 10 | var key = path[path.length-1]; 11 | obj[key] = value; 12 | }; 13 | 14 | module.exports = deepUpdate; 15 | -------------------------------------------------------------------------------- /src/ui/stores/ConnectionStore.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | 3 | var ConnectionStore = Fluxxor.createStore({ 4 | actions: { 5 | 'connected': 'onConnected' 6 | }, 7 | 8 | initialize: function() { 9 | this.isConnected = false; 10 | }, 11 | 12 | onConnected: function() { 13 | this.isConnected = true; 14 | this.emit('change'); 15 | } 16 | }); 17 | 18 | module.exports = ConnectionStore; 19 | -------------------------------------------------------------------------------- /src/agent/index.js: -------------------------------------------------------------------------------- 1 | window.__coquette_inspect_agent_injected__ = true; 2 | 3 | var Agent = require('./Agent'); 4 | var patchEntities = require('./patchEntities'); 5 | var sendMessage = require('./util/sendMessage'); 6 | 7 | if (window.__coquette__) { 8 | sendMessage('locatedCoquette'); 9 | patchEntities(window.__coquette__); 10 | new Agent(window.__coquette__); 11 | 12 | } else { 13 | sendMessage('noCoquetteFound'); 14 | } 15 | -------------------------------------------------------------------------------- /src/ui/components/ListArrow.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var classnames = require('classnames'); 3 | 4 | var ListArrow = React.createClass({ 5 | render: function() { 6 | var isActive = this.props.isActive; 7 | var arrowClass = classnames({ 8 | 'glyphicon': true, 9 | 'list-arrow': true, 10 | 'glyphicon-chevron-right': !isActive, 11 | 'glyphicon-chevron-down': isActive 12 | }); 13 | 14 | return ( 15 | 16 | ); 17 | } 18 | }); 19 | 20 | module.exports = ListArrow; 21 | -------------------------------------------------------------------------------- /src/ui/actions/EntityActions.js: -------------------------------------------------------------------------------- 1 | var sendMessage = require('../util/sendMessage'); 2 | 3 | module.exports = { 4 | didGetEntities: function(entities, subscribedDetail) { 5 | this.dispatch('didGetEntities', entities, subscribedDetail); 6 | }, 7 | 8 | subscribeToEntity: function(id) { 9 | sendMessage('subscribeToEntity', {entityId: id}); 10 | }, 11 | 12 | unsubscribeFromEntity: function(id) { 13 | sendMessage('unsubscribeFromEntity', {entityId: id}); 14 | }, 15 | 16 | updateProperty: function(data) { 17 | sendMessage('updateProperty', data); 18 | this.dispatch('didUpdateProperty', data); // allow optimistic update 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /chrome-extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "Coquette Inspect", 5 | "description": "A DevTools extension for inspecting games made with the Coquette framework", 6 | "version": "0.0.1", 7 | 8 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 9 | 10 | "permissions": [ 11 | "" 12 | ], 13 | 14 | "devtools_page": "devtools.html", 15 | "background": { 16 | "scripts": ["background.js"] 17 | }, 18 | 19 | "content_scripts": [{ 20 | "matches": [""], 21 | "js": ["content-script.js"], 22 | "run_at": "document_end", 23 | "all_frames": true 24 | }] 25 | } 26 | -------------------------------------------------------------------------------- /src/ui/index.js: -------------------------------------------------------------------------------- 1 | require('../../style/main.less'); 2 | 3 | var React = require('react'); 4 | var ReactDOM = require('react-dom'); 5 | var Main = require('./components/Main'); 6 | 7 | var injectDebugger = require('./injectDebugger'); 8 | var AgentHandler = require('./AgentHandler'); 9 | 10 | var Flux = require('fluxxor').Flux; 11 | var actions = require('./actions'); 12 | var stores = require('./stores'); 13 | 14 | var flux = new Flux(stores, actions); 15 | 16 | var agentHandler = new AgentHandler(flux); 17 | 18 | injectDebugger(); 19 | 20 | window.addEventListener('load', function() { 21 | ReactDOM.render(
, document.getElementById('container')); 22 | }); 23 | -------------------------------------------------------------------------------- /src/ui/components/EntityPropertyInput.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | var EntityPropertyInput = React.createClass({ 4 | componentDidMount: function() { 5 | var input = this.refs.input; 6 | input.select(); 7 | }, 8 | 9 | handleKeyPress: function(e) { 10 | if (e.charCode === 13) { // enter 11 | this.refs.input.blur(); 12 | } 13 | }, 14 | 15 | render: function() { 16 | var val = this.props.val; 17 | 18 | if (typeof val === 'string') { 19 | val = '"' + val + '"'; 20 | } 21 | 22 | return ( 23 | 24 | ); 25 | } 26 | }); 27 | 28 | module.exports = EntityPropertyInput; 29 | -------------------------------------------------------------------------------- /chrome-extension/content-script.js: -------------------------------------------------------------------------------- 1 | /* 2 | * agent -> **content-script.js** -> background.js -> dev tools 3 | */ 4 | window.addEventListener('message', function(event) { 5 | // Only accept messages from same frame 6 | if (event.source !== window) { 7 | return; 8 | } 9 | 10 | var message = event.data; 11 | 12 | // Only accept messages of correct format (our messages) 13 | if (typeof message !== 'object' || message === null || 14 | message.source !== 'coquette-inspect-agent') { 15 | return; 16 | } 17 | 18 | chrome.runtime.sendMessage(message); 19 | }); 20 | 21 | 22 | /* 23 | * agent <- **content-script.js** <- background.js <- dev tools 24 | */ 25 | chrome.runtime.onMessage.addListener(function(request) { 26 | request.source = 'coquette-inspect-devtools'; 27 | window.postMessage(request, '*'); 28 | }); 29 | -------------------------------------------------------------------------------- /src/ui/actions/GameActions.js: -------------------------------------------------------------------------------- 1 | var sendMessage = require('../util/sendMessage'); 2 | 3 | module.exports = { 4 | pauseGame: function() { 5 | sendMessage('pause'); 6 | }, 7 | 8 | didPauseGame: function() { 9 | this.dispatch('pausedGame'); 10 | }, 11 | 12 | unpauseGame: function() { 13 | sendMessage('unpause'); 14 | }, 15 | 16 | didUnpauseGame: function() { 17 | this.dispatch('unpausedGame'); 18 | }, 19 | 20 | step: function() { 21 | sendMessage('step'); 22 | }, 23 | 24 | didTick: function() { 25 | this.dispatch('ticked'); 26 | }, 27 | 28 | enableSelectMode: function() { 29 | sendMessage('enableSelectMode'); 30 | }, 31 | 32 | didEnableSelectMode: function() { 33 | this.dispatch('enabledSelectMode'); 34 | }, 35 | 36 | disableSelectMode: function() { 37 | sendMessage('disableSelectMode'); 38 | }, 39 | 40 | didDisableSelectMode: function() { 41 | this.dispatch('disabledSelectMode'); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/agent/patchEntities.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This method does two things: 3 | * 1. If entity.create was not already patched, patches it to generate and store a UUID on new 4 | * entities 5 | * 2. If entity.create was not already patched, generates a UUID for each existing entity 6 | */ 7 | 8 | var uuid = require('uuid'); 9 | 10 | var patchEntities = function(c) { 11 | if (c.entities.__inspect_patched__) { 12 | // Was already patched 13 | return; 14 | } 15 | 16 | // Patch over existing create method 17 | var origCreate = c.entities.create; 18 | c.entities.create = function() { 19 | var entity = origCreate.apply(this, arguments); 20 | entity.__inspect_uuid__ = uuid.v1(); 21 | return entity; 22 | }; 23 | 24 | // Add uuids to existing entities 25 | c.entities.all().forEach(function(entity) { 26 | entity.__inspect_uuid__ = uuid.v1(); 27 | }); 28 | 29 | c.entities.__inspect_patched__ = true; 30 | }; 31 | 32 | module.exports = patchEntities; 33 | -------------------------------------------------------------------------------- /style/entity-list.less: -------------------------------------------------------------------------------- 1 | .console-formatted-number, .console-formatted-boolean { 2 | color: rgb(28, 0, 207); 3 | } 4 | 5 | .console-formatted-string { 6 | color: rgb(196, 26, 22); 7 | } 8 | 9 | .console-formatted-null { 10 | color: rgb(128, 128, 128); 11 | } 12 | 13 | ul { 14 | padding-left: 17px; 15 | } 16 | 17 | ul.entity-list { 18 | cursor: default; 19 | 20 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 21 | 22 | code { 23 | background: none; 24 | font-size: 100%; 25 | padding: 0; 26 | color: rgb(136, 19, 145); 27 | } 28 | 29 | li { 30 | list-style-type: none; 31 | position: relative; 32 | 33 | .list-arrow { 34 | position: absolute; 35 | top: 5px; 36 | left: -15px; 37 | font-size: 10px; 38 | } 39 | 40 | input { 41 | &:focus { 42 | outline: 1px #aaa solid; 43 | outline-offset: 1px; 44 | } 45 | padding: 0px; 46 | border: 0; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coquette-inspect", 3 | "version": "0.1.0", 4 | "description": "", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "build": "webpack", 8 | "dev": "webpack --watch" 9 | }, 10 | "author": "Thomas Boyt ", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "babel-core": "^6.10.4", 14 | "babel-loader": "^6.2.4", 15 | "babel-preset-es2015": "^6.9.0", 16 | "babel-preset-react": "^6.11.1", 17 | "bootstrap": "^3.3.6", 18 | "css-loader": "^0.23.1", 19 | "file-loader": "^0.9.0", 20 | "less": "^2.7.1", 21 | "less-loader": "^2.2.3", 22 | "style-loader": "^0.13.1", 23 | "webpack": "^1.13.1", 24 | "webpack-create-vendor-chunk": "^0.1.1" 25 | }, 26 | "dependencies": { 27 | "classnames": "^2.2.5", 28 | "fluxxor": "^1.7.3", 29 | "lodash": "^3.10.1", 30 | "react": "^15.2.1", 31 | "react-dom": "^15.2.1", 32 | "uuid": "~2.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /style/main.less: -------------------------------------------------------------------------------- 1 | @import "~bootstrap/less/bootstrap"; 2 | @icon-font-path: '~bootstrap/fonts/'; 3 | @import "entity-list"; 4 | 5 | .fps-warning { 6 | color: red; 7 | } 8 | 9 | html, body, #container { 10 | height: 100%; 11 | } 12 | 13 | .main-container { 14 | height: 100%; 15 | } 16 | 17 | .panel { 18 | min-height: 100%; 19 | border-radius: 0; 20 | box-shadow: 0; 21 | margin-bottom: 0; 22 | } 23 | 24 | .panel-heading { 25 | border-top-right-radius: 0; 26 | border-top-left-radius: 0; 27 | position: relative; 28 | } 29 | 30 | .controls { 31 | position: absolute; 32 | right: 10px; 33 | top: 0; 34 | bottom: 0; 35 | 36 | button { 37 | height: 100%; 38 | width: 40px; 39 | font-size: 16px; 40 | 41 | &:disabled { 42 | color: #aaa; 43 | } 44 | 45 | &.activated { 46 | color: rgb(66, 129, 235); 47 | } 48 | } 49 | 50 | .fps { 51 | display: inline-block; 52 | padding-right: 20px; 53 | } 54 | } 55 | 56 | .no-connection { 57 | margin-top: 20px; 58 | } 59 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var createVendorChunk = require('webpack-create-vendor-chunk'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: { 6 | ui: './src/ui/index.js', 7 | agent: './src/agent/index.js', 8 | }, 9 | 10 | devtool: 'source-map', 11 | 12 | output: { 13 | path: 'chrome-extension/build/', 14 | publicPath: 'build', 15 | filename: '[name].bundle.js' 16 | }, 17 | 18 | plugins: [ 19 | createVendorChunk({ 20 | name: 'vendor', 21 | chunks: ['ui'], 22 | }), 23 | ], 24 | 25 | module: { 26 | loaders: [ 27 | { 28 | test: /\.js$/, loader: 'babel', exclude: /node_modules/ 29 | }, 30 | 31 | { 32 | test: /\.less$/, 33 | loader: "style-loader!css-loader!less-loader" 34 | }, 35 | 36 | { 37 | test: /(?:\.woff2?$|\.ttf$|\.svg$|\.eot$)/, 38 | loader: 'file-loader', 39 | query: { 40 | name: '/build/font/[hash].[ext]' 41 | } 42 | }, 43 | ] 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/ui/injectDebugger.js: -------------------------------------------------------------------------------- 1 | var sendMessage = require('./util/sendMessage'); 2 | 3 | // thx https://github.com/emberjs/ember-inspector/blob/master/app/adapters/chrome.js 4 | var injectDebugger = function() { 5 | /* jshint evil: true */ 6 | 7 | var injectedGlobal = 'window.__coquette_inspect_agent_injected__'; 8 | 9 | chrome.devtools.inspectedWindow.eval(injectedGlobal, function(result) { 10 | if (!result) { 11 | // script hasn't been injected yet 12 | 13 | var xhr = new XMLHttpRequest(); 14 | xhr.open('GET', chrome.extension.getURL('/build/agent.bundle.js'), false); 15 | xhr.send(); 16 | var script = xhr.responseText; 17 | 18 | chrome.devtools.inspectedWindow.eval(script, function(result, err) { 19 | if (err) { 20 | console.error(err.value); 21 | } 22 | 23 | sendMessage('connect'); 24 | }); 25 | } else { 26 | // we're already injected, so just connect 27 | sendMessage('connect'); 28 | } 29 | }); 30 | }; 31 | 32 | module.exports = injectDebugger; 33 | -------------------------------------------------------------------------------- /src/ui/components/Entity.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var FluxMixin = require('fluxxor').FluxMixin(React); 3 | var EntityPropertiesList = require('./EntityPropertiesList'); 4 | var ListArrow = require('./ListArrow'); 5 | 6 | var Entity = React.createClass({ 7 | mixins: [ 8 | FluxMixin 9 | ], 10 | 11 | propTypes: { 12 | entity: React.PropTypes.object.isRequired, 13 | isActive: React.PropTypes.bool, 14 | onClickEntity: React.PropTypes.func.isRequired 15 | }, 16 | 17 | render: function() { 18 | var isActive = this.props.isActive; 19 | var entity = this.props.entity; 20 | 21 | return ( 22 |
  • 23 | this.props.onClickEntity(entity.entityId)}> 24 | 25 | 26 | {entity.displayName || 'unknown entity'} 27 | 28 | 29 | 30 | {isActive && } 31 |
  • 32 | ); 33 | } 34 | }); 35 | 36 | module.exports = Entity; 37 | -------------------------------------------------------------------------------- /src/ui/AgentHandler.js: -------------------------------------------------------------------------------- 1 | var port = require('./port'); 2 | var injectDebugger = require('./injectDebugger'); 3 | 4 | var AgentHandler = function(flux) { 5 | this.flux = flux; 6 | 7 | port.onMessage.addListener((msg) => { this.handleMessage(msg); }); 8 | 9 | this.handlers = { 10 | connected: () => this.flux.actions.didConnect(), 11 | 12 | reloaded: () => injectDebugger(), 13 | 14 | tick: (data) => { 15 | this.flux.actions.entities.didGetEntities({ 16 | entities: data.entities, 17 | subscribedEntity: data.subscribedEntity 18 | }); 19 | 20 | this.flux.actions.game.didTick(); 21 | }, 22 | 23 | paused: () => this.flux.actions.game.didPauseGame(), 24 | unpaused: () => this.flux.actions.game.didUnpauseGame(), 25 | 26 | enabledSelectMode: () => this.flux.actions.game.didEnableSelectMode(), 27 | disabledSelectMode: () => this.flux.actions.game.didDisableSelectMode() 28 | }; 29 | }; 30 | 31 | AgentHandler.prototype.handleMessage = function(message) { 32 | var handler = this.handlers[message.name]; 33 | if (!handler) { 34 | console.warn('No handler found for event ' + message.name); 35 | return; 36 | } 37 | 38 | handler(message.data); 39 | }; 40 | 41 | module.exports = AgentHandler; 42 | -------------------------------------------------------------------------------- /src/ui/stores/EntityStore.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | 3 | var deepUpdate = require('../../common/deepUpdate'); 4 | 5 | var EntityStore = Fluxxor.createStore({ 6 | actions: { 7 | 'didGetEntities': 'onDidGetEntities', 8 | 'didUpdateProperty': 'onDidUpdateProperty' 9 | }, 10 | 11 | initialize: function() { 12 | this.entities = []; 13 | this.subscribedDetail = null; 14 | this.subscribedId = null; 15 | }, 16 | 17 | onDidGetEntities: function(data) { 18 | this.entities = data.entities; 19 | 20 | if (data.subscribedEntity) { 21 | this.subscribedId = data.subscribedEntity.__inspect_uuid__; 22 | this.subscribedDetail = data.subscribedEntity; 23 | } else { 24 | this.subscribedId = null; 25 | this.subscribedDetail = null; 26 | } 27 | 28 | this.emit('change'); 29 | }, 30 | 31 | onDidUpdateProperty: function(data) { 32 | var entity = this.entities 33 | .filter((entity) => entity.__inspect_uuid__ === data.entityId)[0]; 34 | 35 | if (!entity) { 36 | throw new Error('No entity found with id ' + data.entityId); 37 | } 38 | 39 | deepUpdate(entity, data.path, data.value); 40 | 41 | this.emit('change'); 42 | } 43 | }); 44 | 45 | module.exports = EntityStore; 46 | -------------------------------------------------------------------------------- /src/ui/components/EntityList.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var FluxMixin = require('fluxxor').FluxMixin(React); 3 | var StoreWatchMixin = require('fluxxor').StoreWatchMixin; 4 | var Entity = require('./Entity'); 5 | 6 | var EntityList = React.createClass({ 7 | mixins: [ 8 | FluxMixin, 9 | StoreWatchMixin('EntityStore') 10 | ], 11 | 12 | getStateFromFlux: function() { 13 | var store = this.getFlux().store('EntityStore'); 14 | return { 15 | entities: store.entities, 16 | subscribedId: store.subscribedId, 17 | subscribedDetail: store.subscribedDetail 18 | }; 19 | }, 20 | 21 | handleToggleOpenEntity: function(id) { 22 | if (id === this.state.subscribedId) { 23 | this.getFlux().actions.entities.unsubscribeFromEntity(id); 24 | } else { 25 | this.getFlux().actions.entities.subscribeToEntity(id); 26 | } 27 | }, 28 | 29 | render: function() { 30 | var items = this.state.entities.map((entity) => { 31 | var isActive = entity.entityId === this.state.subscribedId; 32 | 33 | return ( 34 | 36 | ); 37 | }); 38 | 39 | return ( 40 |
      41 | {items} 42 |
    43 | ); 44 | } 45 | }); 46 | 47 | module.exports = EntityList; 48 | -------------------------------------------------------------------------------- /chrome-extension/background.js: -------------------------------------------------------------------------------- 1 | var connections = {}; 2 | 3 | /* 4 | * agent -> content-script.js -> **background.js** -> dev tools 5 | */ 6 | chrome.runtime.onMessage.addListener(function(request, sender) { 7 | if (sender.tab) { 8 | var tabId = sender.tab.id; 9 | if (tabId in connections) { 10 | connections[tabId].postMessage(request); 11 | } else { 12 | console.log("Tab not found in connection list."); 13 | } 14 | } else { 15 | console.log("sender.tab not defined."); 16 | } 17 | return true; 18 | }); 19 | 20 | 21 | /* 22 | * agent <- content-script.js <- **background.js** <- dev tools 23 | */ 24 | chrome.runtime.onConnect.addListener(function(port) { 25 | 26 | // Listen to messages sent from the DevTools page 27 | port.onMessage.addListener(function(request) { 28 | console.log('incoming message from dev tools page', request); 29 | 30 | // Register initial connection 31 | if (request.name === 'init') { 32 | connections[request.tabId] = port; 33 | 34 | port.onDisconnect.addListener(function() { 35 | delete connections[request.tabId]; 36 | }); 37 | 38 | return; 39 | } 40 | 41 | // Otherwise, broadcast to agent 42 | chrome.tabs.sendMessage(request.tabId, { 43 | name: request.name, 44 | data: request.data 45 | }); 46 | }); 47 | 48 | }); 49 | 50 | chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab){ 51 | if (tabId in connections && changeInfo.status === 'complete') { 52 | // TODO: reload connection to page somehow...? 53 | connections[tabId].postMessage({ 54 | name: 'reloaded' 55 | }); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /src/ui/stores/GameStore.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require('fluxxor'); 2 | var _ = require('lodash'); 3 | 4 | // Calculate average delta time over last maxSamples frames 5 | // via http://stackoverflow.com/a/87732 6 | var mkAvgTick = function(maxSamples) { 7 | var tickIdx = 0; 8 | var tickSum = 0; 9 | var tickList = _.range(0, maxSamples).map(() => 0); 10 | 11 | return function(dt) { 12 | tickSum -= tickList[tickIdx]; 13 | tickSum += dt; 14 | tickList[tickIdx] = dt; 15 | 16 | tickIdx += 1; 17 | if (tickIdx === maxSamples) { 18 | tickIdx = 0; 19 | } 20 | 21 | return tickSum / maxSamples; 22 | }; 23 | }; 24 | 25 | var GameStore = Fluxxor.createStore({ 26 | actions: { 27 | 'pausedGame': 'onPausedGame', 28 | 'unpausedGame': 'onUnpausedGame', 29 | 'ticked': 'onTicked', 30 | 'enabledSelectMode': 'onEnabledSelectMode', 31 | 'disabledSelectMode': 'onDisabledSelectMode' 32 | }, 33 | 34 | initialize: function() { 35 | this._lastTick = null; 36 | this._avgTick = mkAvgTick(100); 37 | this.isPaused = false; 38 | }, 39 | 40 | onPausedGame: function() { 41 | this.isPaused = true; 42 | this.emit('change'); 43 | }, 44 | 45 | onUnpausedGame: function() { 46 | this.isPaused = false; 47 | this.emit('change'); 48 | }, 49 | 50 | onTicked: function() { 51 | var cur = Date.now(); 52 | 53 | if (this._lastTick !== null) { 54 | var dt = cur - this._lastTick; 55 | this.fps = Math.round(1000 / this._avgTick(dt)); 56 | } 57 | 58 | this._lastTick = cur; 59 | 60 | this.emit('change'); 61 | }, 62 | 63 | onEnabledSelectMode: function() { 64 | this.isSelecting = true; 65 | this.emit('change'); 66 | }, 67 | 68 | onDisabledSelectMode: function() { 69 | this.isSelecting = false; 70 | this.emit('change'); 71 | } 72 | }); 73 | 74 | module.exports = GameStore; 75 | -------------------------------------------------------------------------------- /src/agent/util/serializeEntity.js: -------------------------------------------------------------------------------- 1 | var cloneValue = function(val, seen, blacklist) { 2 | 3 | /* Functions */ 4 | if (typeof val === 'function') { 5 | return undefined; // TODO: there's a use case for serializing these into [object Function], 6 | // maybe just do this filtering UI-side 7 | } 8 | 9 | /* Arrays */ 10 | if (Array.isArray(val)) { 11 | if (seen.has(val)) { 12 | return '[[Circular reference]]'; 13 | } 14 | 15 | seen.set(val, true); 16 | return cloneArray(val, seen, blacklist); 17 | } 18 | 19 | /* Objects */ 20 | if (typeof val === 'object' && val !== null) { 21 | // don't serialize the Coquette object, which is often stored on entities 22 | if (val === window.__coquette__) { 23 | return '[[Coquette namespace]]'; 24 | } 25 | if (seen.has(val)) { 26 | return '[[Circular reference]]'; 27 | } 28 | if (blacklist.has(val)) { 29 | return '[[Entity ' + val.displayName + ']]'; 30 | } 31 | 32 | if (val instanceof HTMLElement) { 33 | return '[' + val.toString() + ']'; // looks like [[object HTMLElement]] 34 | } 35 | 36 | seen.set(val, true); 37 | return cloneObject(val, seen, blacklist); 38 | } 39 | 40 | /* Primitives */ 41 | return val; 42 | }; 43 | 44 | var cloneArray = function(arr, seen, blacklist) { 45 | var clone = arr.map(function(val) { 46 | return cloneValue(val, seen, blacklist); 47 | }); 48 | 49 | return clone; 50 | }; 51 | 52 | var cloneObject = function(obj, seen, blacklist) { 53 | var clone = {}; 54 | 55 | for (var key in obj) { 56 | clone[key] = cloneValue(obj[key], seen, blacklist); 57 | } 58 | 59 | return clone; 60 | }; 61 | 62 | var serializeEntity = function(entity, entities) { 63 | var entitiesMap = new WeakMap(); 64 | // Chrome doesn't support WeakMap(iterable) yet :( 65 | entities.forEach((entity) => { 66 | entitiesMap.set(entity, null); 67 | }); 68 | 69 | var seenMap = new WeakMap(); 70 | 71 | var clone = cloneObject(entity, seenMap, entitiesMap); 72 | 73 | clone.displayName = entity.displayName || entity.constructor.name; 74 | 75 | return clone; 76 | }; 77 | 78 | module.exports = serializeEntity; 79 | -------------------------------------------------------------------------------- /src/ui/components/Main.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var FluxMixin = require('fluxxor').FluxMixin(React); 3 | var StoreWatchMixin = require('fluxxor').StoreWatchMixin; 4 | var EntityList = require('./EntityList'); 5 | var GameControls = require('./GameControls'); 6 | 7 | var Main = React.createClass({ 8 | mixins: [ 9 | FluxMixin, 10 | StoreWatchMixin('ConnectionStore') 11 | ], 12 | 13 | getStateFromFlux: function() { 14 | var store = this.getFlux().store('ConnectionStore'); 15 | 16 | return { 17 | isConnected: store.isConnected 18 | }; 19 | }, 20 | 21 | renderLoaded: function() { 22 | return ( 23 |
    24 | 25 |
    26 | 27 |

    28 | Entities 29 |

    30 |
    31 |
    32 | 33 |
    34 | 35 |
    36 | ); 37 | }, 38 | 39 | renderNoConnection: function() { 40 | return ( 41 |
    42 |
    43 |
    44 |

    Coquette instance not found :(

    45 |
    46 |
    47 |

    48 | If there's a coquette application on the page, the most common reason this occurs is 49 | because its Coquette instance hasn't been exposed on the main window object as 50 | window.__coquette__. To fix, update your game's constructor, for example: 51 |

    52 | 53 |
    54 |               {/* this is the worst. */}
    55 |               {'var Game = function() {\n'}
    56 |               {'  this.c = new Coquette(this, "canvas", 500, 150, "#000");\n'}
    57 |               {'  window.__coquette__ = this.c;\n'}
    58 |               {'  // ...\n'}
    59 |               {'};\n'}
    60 |             
    61 |
    62 |
    63 |
    64 | ); 65 | }, 66 | 67 | render: function() { 68 | return ( 69 |
    70 | { this.state.isConnected ? this.renderLoaded() : this.renderNoConnection() } 71 |
    72 | ); 73 | } 74 | }); 75 | 76 | module.exports = Main; 77 | -------------------------------------------------------------------------------- /src/ui/components/EntityPropertiesList.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var FluxMixin = require('fluxxor').FluxMixin(React); 3 | var EntityProperty = require('./EntityProperty'); 4 | var ListArrow = require('./ListArrow'); 5 | 6 | 7 | // TODO: This is currently inlined in this file because of the circular dependency between 8 | // EntityObjectProperty and EntityPropertiesList :( 9 | var EntityObjectProperty = React.createClass({ 10 | getInitialState: function() { 11 | return { 12 | isOpen: false 13 | }; 14 | }, 15 | 16 | handleToggleOpen: function() { 17 | this.setState({ 18 | isOpen: !this.state.isOpen 19 | }); 20 | }, 21 | 22 | render: function() { 23 | var isOpen = this.state.isOpen; 24 | 25 | return ( 26 |
  • 27 | 28 | 29 | {this.props.prop}: Object 30 | 31 | 32 | {isOpen && } 34 |
  • 35 | ); 36 | } 37 | }); 38 | 39 | 40 | var EntityPropertiesList = React.createClass({ 41 | mixins: [ 42 | FluxMixin 43 | ], 44 | 45 | propTypes: { 46 | entity: React.PropTypes.object.isRequired, 47 | obj: React.PropTypes.oneOfType([ 48 | React.PropTypes.object, 49 | React.PropTypes.array 50 | ]), 51 | lastPath: React.PropTypes.array 52 | }, 53 | 54 | renderItem: function(key, val) { 55 | var lastPath = this.props.lastPath || []; 56 | var path = lastPath.concat(key); 57 | 58 | if (typeof val === 'object' && val !== null) { 59 | return ( 60 | 62 | ); 63 | } 64 | 65 | return ( 66 | 68 | ); 69 | }, 70 | 71 | render: function() { 72 | var obj = this.props.obj || this.props.entity; 73 | 74 | var items = Object.keys(obj) 75 | .filter((prop) => prop !== 'displayName' && prop !== '__inspect_uuid__') 76 | .map((prop) => this.renderItem(prop, obj[prop])); 77 | 78 | return ( 79 |
      80 | {items} 81 |
    82 | ); 83 | } 84 | }); 85 | 86 | module.exports = EntityPropertiesList; 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coquette-inspect [![Stories in Ready](https://badge.waffle.io/thomasboyt/coquette-inspect.png?label=ready&title=Ready)](https://waffle.io/thomasboyt/coquette-inspect) 2 | 3 | A Chrome DevTools extension for inspecting games made with the [Coquette](http://coquette.maryrosecook.com/) framework. 4 | 5 | ![](https://cloud.githubusercontent.com/assets/579628/4639937/32eca436-5417-11e4-8f2b-422e33b11d9e.gif) 6 | 7 |

    8 | The inspector in action in the spinning shapes demo. 9 |

    10 | 11 | ## Features 12 | 13 | * List entities currently in the game world 14 | * Inspect the properties of entities as they update 15 | * Change the properties of entities 16 | * Play/pause the game loop 17 | * Step through the game loop 18 | 19 | ## Installing 20 | 21 | To install: 22 | 23 | ``` 24 | git clone git@github.com:thomasboyt/coquette-inspect.git 25 | cd coquette-inspect/ 26 | npm install && ./node_modules/.bin/webpack 27 | ``` 28 | 29 | Then load the `chrome-extension` folder as an unpacked extension ([see this guide](https://developer.chrome.com/extensions/getstarted#unpacked)). 30 | 31 | If it worked, you should see a "Coquette" tab in your developer tools when you next open them. 32 | 33 | ## Usage 34 | 35 | There are two modifications you'll need to do to your Coquette apps to make them work. 36 | 37 | ### Exposing Coquette 38 | 39 | The most important one is that you expose the Coquette instance in your game as `window.__coquette__`, e.g.: 40 | 41 | ```js 42 | var Game = function() { 43 | window.__coquette__ = this.c = new Coquette(this, "canvas", 500, 150, "#000"); 44 | // ... 45 | ``` 46 | 47 | Without this, the inspector won't be able to find your Coquette instance. 48 | 49 | ### Entity display names 50 | 51 | To display your entities with their proper names (i.e. their constructors), one of two of the following need to be true: 52 | 53 | If your constructors are defined with the syntax `function Foo() {...}`, the name will be looked up with `entity.constructor.name`. This doesn't work if your function is anonymous, e.g. `var Foo = function() {...}`, because that's just how `function.name` works. See [MDN] (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name) for more detail on this weird quirk. 54 | 55 | Otherwise, you can set the `displayName` property on your entity. You can either set it inside the constructor (e.g. `this.displayName = 'Person'`), or inside the call to `entities.create` (e.g. `c.entities.create(Person, {displayName: 'Player'})`). 56 | -------------------------------------------------------------------------------- /src/ui/components/GameControls.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var FluxMixin = require('fluxxor').FluxMixin(React); 3 | var StoreWatchMixin = require('fluxxor').StoreWatchMixin; 4 | 5 | var GameState = React.createClass({ 6 | mixins: [ 7 | FluxMixin, 8 | StoreWatchMixin('GameStore') 9 | ], 10 | 11 | getStateFromFlux: function() { 12 | var store = this.getFlux().store('GameStore'); 13 | return { 14 | isPaused: store.isPaused, 15 | isSelecting: store.isSelecting, 16 | fps: store.fps 17 | }; 18 | }, 19 | 20 | handleTogglePause: function(e) { 21 | e.stopPropagation(); 22 | 23 | if (this.state.isPaused) { 24 | this.getFlux().actions.game.unpauseGame(); 25 | } else { 26 | this.getFlux().actions.game.pauseGame(); 27 | } 28 | }, 29 | 30 | handleToggleSelectEntity: function(e) { 31 | e.stopPropagation(); 32 | 33 | if (!this.state.isSelecting) { 34 | this.getFlux().actions.game.enableSelectMode(); 35 | } else { 36 | this.getFlux().actions.game.disableSelectMode(); 37 | } 38 | }, 39 | 40 | handleStep: function(e) { 41 | e.stopPropagation(); 42 | this.getFlux().actions.game.step(); 43 | }, 44 | 45 | renderPaused: function() { 46 | return ( 47 | 48 | 51 | 54 | 55 | ); 56 | }, 57 | 58 | renderPlaying: function() { 59 | var fpsClass = this.state.fps < 59 ? 'fps fps-warning' : 'fps'; 60 | 61 | return ( 62 | 63 | {this.state.fps} FPS  64 | 67 | 70 | 71 | ); 72 | }, 73 | 74 | render: function() { 75 | var selectClass = this.state.isSelecting ? 'activated' : ''; 76 | 77 | return ( 78 |
    79 | {this.state.isPaused ? this.renderPaused() : this.renderPlaying()} 80 | 81 | 84 |
    85 | ); 86 | } 87 | }); 88 | 89 | module.exports = GameState; 90 | -------------------------------------------------------------------------------- /src/ui/components/EntityProperty.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var FluxMixin = require('fluxxor').FluxMixin(React); 3 | var EntityPropertyInput = require('./EntityPropertyInput'); 4 | 5 | var isUneditable = function(value) { 6 | return (typeof value === 'string' && ( 7 | value === '[[Coquette namespace]]' || 8 | value === '[[Circular reference]]' || 9 | value.match(/^\[\[Entity .*\]\]$/) || 10 | value.match(/^\[\[object [^\s]*\]\]$/))); 11 | }; 12 | 13 | var EntityProperty = React.createClass({ 14 | mixins: [ 15 | FluxMixin 16 | ], 17 | 18 | getInitialState: function() { 19 | return { 20 | isOpen: false 21 | }; 22 | }, 23 | 24 | handleOpen: function() { 25 | if (isUneditable(this.props.value)) { 26 | return; 27 | } 28 | 29 | this.setState({ 30 | isOpen: true 31 | }); 32 | }, 33 | 34 | handleClose: function(e) { 35 | this.setState({ 36 | isOpen: false 37 | }); 38 | 39 | var value = e.target.value; 40 | 41 | if (typeof this.props.value === 'number') { 42 | value = parseInt(value, 10); 43 | } 44 | 45 | this.getFlux().actions.entities.updateProperty({ 46 | entityId: this.props.entity.__inspect_uuid__, 47 | path: this.props.path, 48 | value: value 49 | }); 50 | }, 51 | 52 | getClassFor: function(val) { 53 | if (isUneditable(val)) { 54 | return; 55 | } 56 | 57 | var className; 58 | if (val === null) { 59 | className = 'null'; 60 | } else { 61 | className = typeof val; 62 | } 63 | 64 | return 'console-formatted-' + className; 65 | }, 66 | 67 | renderValue: function() { 68 | var val = this.props.value; 69 | 70 | var className = this.getClassFor(val); 71 | var isString = className === 'console-formatted-string'; 72 | 73 | return ( 74 | 75 | {isString && '"'} 76 | {val === null ? 'null' : val.toString()} 77 | {isString && '"'} 78 | 79 | ); 80 | }, 81 | 82 | render: function() { 83 | var prop = this.props.prop; 84 | 85 | var valueDisplay; 86 | if (this.state.isOpen) { 87 | valueDisplay = ( 88 | 89 | ); 90 | } else { 91 | valueDisplay = this.renderValue(); 92 | } 93 | 94 | return ( 95 |
  • 96 | {prop}: {valueDisplay} 97 |
  • 98 | ); 99 | } 100 | 101 | }); 102 | 103 | module.exports = EntityProperty; 104 | -------------------------------------------------------------------------------- /src/agent/Agent.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | var sendMessage = require('./util/sendMessage'); 3 | var serializeEntity = require('./util/serializeEntity'); 4 | var deepUpdate = require('../common/deepUpdate'); 5 | 6 | var GAME_OBJECT_ID = 'game_object'; 7 | 8 | /** 9 | * TODO: why is this even a Class? doesn't really do anything particularly ~object-oriented~ 10 | * not sure what to refactor it into, tho 11 | */ 12 | var Agent = function(c) { 13 | this.c = c; 14 | this.game = c.entities.game; 15 | this.Coquette = c.constructor; 16 | this.canvas = c.renderer._ctx.canvas; 17 | 18 | // Agent state 19 | this.subscribedEntityId = null; 20 | 21 | // Register a displayName and ID on the game object 22 | if (!this.game.displayName) { 23 | this.game.displayName = ''; 24 | } 25 | this.game.__inspect_uuid__ = GAME_OBJECT_ID; 26 | 27 | // Kick off debug loop and message handler 28 | this.initDebugLoop(); 29 | this.initDevtoolsMessageListener(); 30 | }; 31 | 32 | Agent.prototype.initDebugLoop = function() { 33 | var debugLoop = () => { 34 | this.reportEntities(); 35 | 36 | // Ensure that this isn't re-enqueued on the same frame, or the runner gets stuck in an endless 37 | // loop. 38 | // TODO: setTimeout() seems like a non-optimal way to do this, could end up missing frames 39 | // or hurting perf? :C 40 | setTimeout(() => { 41 | this.c.runner.add(undefined, debugLoop); 42 | }); 43 | }; 44 | 45 | this.c.runner.add(undefined, debugLoop); 46 | }; 47 | 48 | Agent.prototype.initDevtoolsMessageListener = function() { 49 | window.addEventListener('message', function(event) { 50 | // Only accept messages from same frame 51 | if (event.source !== window) { 52 | return; 53 | } 54 | 55 | var message = event.data; 56 | 57 | // Only accept messages of correct format (our messages) 58 | if (typeof message !== 'object' || message === null || 59 | message.source !== 'coquette-inspect-devtools') { 60 | return; 61 | } 62 | 63 | this.handleMessage(message); 64 | }.bind(this)); 65 | }; 66 | 67 | Agent.prototype.reportEntities = function() { 68 | var entities = this.c.entities.all().concat(this.game); 69 | 70 | var entitiesList = entities.map((entity) => { 71 | return { 72 | displayName: entity.displayName || entity.constructor.name, 73 | entityId: entity.__inspect_uuid__ 74 | }; 75 | }); 76 | 77 | var id = this.subscribedEntityId; 78 | 79 | sendMessage('tick', { 80 | entities: entitiesList, 81 | subscribedEntity: this.serializeSubscribedEntity(id, entities) 82 | }); 83 | }; 84 | 85 | Agent.prototype.serializeSubscribedEntity = function(id, entities) { 86 | if (this.subscribedEntityId === null) { 87 | return; 88 | } 89 | 90 | var entity = entities.filter((entity) => id === entity.__inspect_uuid__)[0]; 91 | 92 | if (!entity) { 93 | this.subscribedEntityId = null; 94 | return; 95 | } 96 | 97 | return serializeEntity(entity, entities); 98 | }; 99 | 100 | Agent.prototype.handlers = { 101 | 102 | // Broadcast when the dev tools are opened 103 | connect: function() { 104 | sendMessage('connected'); 105 | }, 106 | 107 | pause: function() { 108 | this.c.ticker.stop(); 109 | sendMessage('paused'); 110 | }, 111 | 112 | unpause: function() { 113 | this.c.ticker.start(); 114 | sendMessage('unpaused'); 115 | }, 116 | 117 | step: function() { 118 | this.c.ticker.start(); // this sets a cb for the requestAnimationFrame() loop.. 119 | this.c.ticker.stop(); // ...and this unsets it, so that only one frame is run 120 | }, 121 | 122 | updateProperty: function(data) { 123 | /* jshint evil: true */ 124 | 125 | // find entity by UUID 126 | var entity; 127 | if (data.entityId === GAME_OBJECT_ID) { 128 | entity = this.game; 129 | } else { 130 | entity = this.c.entities.all() 131 | .filter((entity) => entity.__inspect_uuid__ === data.entityId)[0]; 132 | } 133 | 134 | if (!entity) { 135 | throw new Error('No entity found with id ' + data.entityId); 136 | } 137 | 138 | var val; 139 | try { 140 | val = eval(data.value); 141 | } catch(e) { 142 | // Don't update anything if the passed expression is invalid 143 | return; 144 | } 145 | 146 | deepUpdate(entity, data.path, val); 147 | }, 148 | 149 | subscribeToEntity: function(data) { 150 | this.subscribedEntityId = data.entityId; 151 | }, 152 | 153 | unsubscribeFromEntity: function(/*data*/) { 154 | this.subscribedEntityId = null; 155 | }, 156 | 157 | enableSelectMode: function() { 158 | this.attachSelectClickHandler(); 159 | }, 160 | 161 | disableSelectMode: function() { 162 | this.removeSelectClickHandler(); 163 | } 164 | }; 165 | 166 | 167 | Agent.prototype.attachSelectClickHandler = function() { 168 | if (this._findTargetCb) { 169 | // already enabled 170 | return; 171 | } 172 | 173 | this._findTargetCb = (e) => { 174 | e.stopPropagation(); 175 | 176 | var x = e.pageX - e.target.offsetLeft; 177 | var y = e.pageY - e.target.offsetTop; 178 | 179 | var matching = _.find(this.c.entities.all(), (obj) => { 180 | if (!obj.center || !obj.size) { 181 | return false; 182 | } 183 | return this.Coquette.Collider.Maths.pointInsideObj({x, y}, obj); 184 | }); 185 | 186 | if (matching) { 187 | this.subscribedEntityId = matching.__inspect_uuid__; 188 | } 189 | 190 | this.removeSelectClickHandler(); 191 | }; 192 | 193 | this.canvas.addEventListener('click', this._findTargetCb); 194 | this.canvas.style.cursor = 'pointer'; 195 | 196 | sendMessage('enabledSelectMode'); 197 | }; 198 | 199 | Agent.prototype.removeSelectClickHandler = function() { 200 | this.canvas.removeEventListener('click', this._findTargetCb); 201 | delete this._findTargetCb; 202 | this.canvas.style.cursor = 'default'; 203 | 204 | sendMessage('disabledSelectMode'); 205 | }; 206 | 207 | Agent.prototype.handleMessage = function(message) { 208 | var handler = this.handlers[message.name]; 209 | if (!handler) { 210 | console.warn('No handler found for event ' + name); 211 | return; 212 | } 213 | 214 | handler.call(this, message.data); 215 | }; 216 | 217 | module.exports = Agent; 218 | --------------------------------------------------------------------------------