1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5// Custom binding for the app_window API. 6 7var appWindowNatives = requireNative('app_window_natives'); 8var runtimeNatives = requireNative('runtime'); 9var Binding = require('binding').Binding; 10var Event = require('event_bindings').Event; 11var forEach = require('utils').forEach; 12var renderViewObserverNatives = requireNative('renderViewObserverNatives'); 13 14var appWindowData = null; 15var currentAppWindow = null; 16var currentWindowInternal = null; 17 18var kSetBoundsFunction = 'setBounds'; 19var kSetSizeConstraintsFunction = 'setSizeConstraints'; 20 21// Bounds class definition. 22var Bounds = function(boundsKey) { 23 privates(this).boundsKey_ = boundsKey; 24}; 25Object.defineProperty(Bounds.prototype, 'left', { 26 get: function() { 27 return appWindowData[privates(this).boundsKey_].left; 28 }, 29 set: function(left) { 30 this.setPosition(left, null); 31 }, 32 enumerable: true 33}); 34Object.defineProperty(Bounds.prototype, 'top', { 35 get: function() { 36 return appWindowData[privates(this).boundsKey_].top; 37 }, 38 set: function(top) { 39 this.setPosition(null, top); 40 }, 41 enumerable: true 42}); 43Object.defineProperty(Bounds.prototype, 'width', { 44 get: function() { 45 return appWindowData[privates(this).boundsKey_].width; 46 }, 47 set: function(width) { 48 this.setSize(width, null); 49 }, 50 enumerable: true 51}); 52Object.defineProperty(Bounds.prototype, 'height', { 53 get: function() { 54 return appWindowData[privates(this).boundsKey_].height; 55 }, 56 set: function(height) { 57 this.setSize(null, height); 58 }, 59 enumerable: true 60}); 61Object.defineProperty(Bounds.prototype, 'minWidth', { 62 get: function() { 63 return appWindowData[privates(this).boundsKey_].minWidth; 64 }, 65 set: function(minWidth) { 66 updateSizeConstraints(privates(this).boundsKey_, { minWidth: minWidth }); 67 }, 68 enumerable: true 69}); 70Object.defineProperty(Bounds.prototype, 'maxWidth', { 71 get: function() { 72 return appWindowData[privates(this).boundsKey_].maxWidth; 73 }, 74 set: function(maxWidth) { 75 updateSizeConstraints(privates(this).boundsKey_, { maxWidth: maxWidth }); 76 }, 77 enumerable: true 78}); 79Object.defineProperty(Bounds.prototype, 'minHeight', { 80 get: function() { 81 return appWindowData[privates(this).boundsKey_].minHeight; 82 }, 83 set: function(minHeight) { 84 updateSizeConstraints(privates(this).boundsKey_, { minHeight: minHeight }); 85 }, 86 enumerable: true 87}); 88Object.defineProperty(Bounds.prototype, 'maxHeight', { 89 get: function() { 90 return appWindowData[privates(this).boundsKey_].maxHeight; 91 }, 92 set: function(maxHeight) { 93 updateSizeConstraints(privates(this).boundsKey_, { maxHeight: maxHeight }); 94 }, 95 enumerable: true 96}); 97Bounds.prototype.setPosition = function(left, top) { 98 updateBounds(privates(this).boundsKey_, { left: left, top: top }); 99}; 100Bounds.prototype.setSize = function(width, height) { 101 updateBounds(privates(this).boundsKey_, { width: width, height: height }); 102}; 103Bounds.prototype.setMinimumSize = function(minWidth, minHeight) { 104 updateSizeConstraints(privates(this).boundsKey_, 105 { minWidth: minWidth, minHeight: minHeight }); 106}; 107Bounds.prototype.setMaximumSize = function(maxWidth, maxHeight) { 108 updateSizeConstraints(privates(this).boundsKey_, 109 { maxWidth: maxWidth, maxHeight: maxHeight }); 110}; 111 112var appWindow = Binding.create('app.window'); 113appWindow.registerCustomHook(function(bindingsAPI) { 114 var apiFunctions = bindingsAPI.apiFunctions; 115 116 apiFunctions.setCustomCallback('create', 117 function(name, request, windowParams) { 118 var view = null; 119 120 // When window creation fails, |windowParams| will be undefined. 121 if (windowParams && windowParams.viewId) { 122 view = appWindowNatives.GetView( 123 windowParams.viewId, windowParams.injectTitlebar); 124 } 125 126 if (!view) { 127 // No route to created window. If given a callback, trigger it with an 128 // undefined object. 129 if (request.callback) { 130 request.callback(); 131 delete request.callback; 132 } 133 return; 134 } 135 136 if (windowParams.existingWindow) { 137 // Not creating a new window, but activating an existing one, so trigger 138 // callback with existing window and don't do anything else. 139 if (request.callback) { 140 request.callback(view.chrome.app.window.current()); 141 delete request.callback; 142 } 143 return; 144 } 145 146 // Initialize appWindowData in the newly created JS context 147 view.chrome.app.window.initializeAppWindow(windowParams); 148 149 var callback = request.callback; 150 if (callback) { 151 delete request.callback; 152 if (!view) { 153 callback(undefined); 154 return; 155 } 156 157 var willCallback = 158 renderViewObserverNatives.OnDocumentElementCreated( 159 windowParams.viewId, 160 function(success) { 161 if (success) { 162 callback(view.chrome.app.window.current()); 163 } else { 164 callback(undefined); 165 } 166 }); 167 if (!willCallback) { 168 callback(undefined); 169 } 170 } 171 }); 172 173 apiFunctions.setHandleRequest('current', function() { 174 if (!currentAppWindow) { 175 console.error('The JavaScript context calling ' + 176 'chrome.app.window.current() has no associated AppWindow.'); 177 return null; 178 } 179 return currentAppWindow; 180 }); 181 182 apiFunctions.setHandleRequest('getAll', function() { 183 var views = runtimeNatives.GetExtensionViews(-1, 'APP_WINDOW'); 184 return $Array.map(views, function(win) { 185 return win.chrome.app.window.current(); 186 }); 187 }); 188 189 apiFunctions.setHandleRequest('get', function(id) { 190 var windows = $Array.filter(chrome.app.window.getAll(), function(win) { 191 return win.id == id; 192 }); 193 return windows.length > 0 ? windows[0] : null; 194 }); 195 196 // This is an internal function, but needs to be bound into a closure 197 // so the correct JS context is used for global variables such as 198 // currentWindowInternal, appWindowData, etc. 199 apiFunctions.setHandleRequest('initializeAppWindow', function(params) { 200 currentWindowInternal = 201 Binding.create('app.currentWindowInternal').generate(); 202 var AppWindow = function() { 203 this.innerBounds = new Bounds('innerBounds'); 204 this.outerBounds = new Bounds('outerBounds'); 205 }; 206 forEach(currentWindowInternal, function(key, value) { 207 // Do not add internal functions that should not appear in the AppWindow 208 // interface. They are called by Bounds mutators. 209 if (key !== kSetBoundsFunction && key !== kSetSizeConstraintsFunction) 210 AppWindow.prototype[key] = value; 211 }); 212 AppWindow.prototype.moveTo = $Function.bind(window.moveTo, window); 213 AppWindow.prototype.resizeTo = $Function.bind(window.resizeTo, window); 214 AppWindow.prototype.contentWindow = window; 215 AppWindow.prototype.onClosed = new Event(); 216 AppWindow.prototype.onWindowFirstShownForTests = new Event(); 217 AppWindow.prototype.close = function() { 218 this.contentWindow.close(); 219 }; 220 AppWindow.prototype.getBounds = function() { 221 // This is to maintain backcompatibility with a bug on Windows and 222 // ChromeOS, which returns the position of the window but the size of 223 // the content. 224 var innerBounds = appWindowData.innerBounds; 225 var outerBounds = appWindowData.outerBounds; 226 return { left: outerBounds.left, top: outerBounds.top, 227 width: innerBounds.width, height: innerBounds.height }; 228 }; 229 AppWindow.prototype.setBounds = function(bounds) { 230 updateBounds('bounds', bounds); 231 }; 232 AppWindow.prototype.isFullscreen = function() { 233 return appWindowData.fullscreen; 234 }; 235 AppWindow.prototype.isMinimized = function() { 236 return appWindowData.minimized; 237 }; 238 AppWindow.prototype.isMaximized = function() { 239 return appWindowData.maximized; 240 }; 241 AppWindow.prototype.isAlwaysOnTop = function() { 242 return appWindowData.alwaysOnTop; 243 }; 244 AppWindow.prototype.alphaEnabled = function() { 245 return appWindowData.alphaEnabled; 246 } 247 AppWindow.prototype.handleWindowFirstShownForTests = function(callback) { 248 // This allows test apps to get have their callback run even if they 249 // call this after the first show has happened. 250 if (this.firstShowHasHappened) { 251 callback(); 252 return; 253 } 254 this.onWindowFirstShownForTests.addListener(callback); 255 } 256 257 Object.defineProperty(AppWindow.prototype, 'id', {get: function() { 258 return appWindowData.id; 259 }}); 260 261 // These properties are for testing. 262 Object.defineProperty( 263 AppWindow.prototype, 'hasFrameColor', {get: function() { 264 return appWindowData.hasFrameColor; 265 }}); 266 267 Object.defineProperty(AppWindow.prototype, 'activeFrameColor', 268 {get: function() { 269 return appWindowData.activeFrameColor; 270 }}); 271 272 Object.defineProperty(AppWindow.prototype, 'inactiveFrameColor', 273 {get: function() { 274 return appWindowData.inactiveFrameColor; 275 }}); 276 277 appWindowData = { 278 id: params.id || '', 279 innerBounds: { 280 left: params.innerBounds.left, 281 top: params.innerBounds.top, 282 width: params.innerBounds.width, 283 height: params.innerBounds.height, 284 285 minWidth: params.innerBounds.minWidth, 286 minHeight: params.innerBounds.minHeight, 287 maxWidth: params.innerBounds.maxWidth, 288 maxHeight: params.innerBounds.maxHeight 289 }, 290 outerBounds: { 291 left: params.outerBounds.left, 292 top: params.outerBounds.top, 293 width: params.outerBounds.width, 294 height: params.outerBounds.height, 295 296 minWidth: params.outerBounds.minWidth, 297 minHeight: params.outerBounds.minHeight, 298 maxWidth: params.outerBounds.maxWidth, 299 maxHeight: params.outerBounds.maxHeight 300 }, 301 fullscreen: params.fullscreen, 302 minimized: params.minimized, 303 maximized: params.maximized, 304 alwaysOnTop: params.alwaysOnTop, 305 hasFrameColor: params.hasFrameColor, 306 activeFrameColor: params.activeFrameColor, 307 inactiveFrameColor: params.inactiveFrameColor, 308 alphaEnabled: params.alphaEnabled 309 }; 310 currentAppWindow = new AppWindow; 311 }); 312}); 313 314function boundsEqual(bounds1, bounds2) { 315 if (!bounds1 || !bounds2) 316 return false; 317 return (bounds1.left == bounds2.left && bounds1.top == bounds2.top && 318 bounds1.width == bounds2.width && bounds1.height == bounds2.height); 319} 320 321function dispatchEventIfExists(target, name) { 322 // Sometimes apps like to put their own properties on the window which 323 // break our assumptions. 324 var event = target[name]; 325 if (event && (typeof event.dispatch == 'function')) 326 event.dispatch(); 327 else 328 console.warn('Could not dispatch ' + name + ', event has been clobbered'); 329} 330 331function updateAppWindowProperties(update) { 332 if (!appWindowData) 333 return; 334 335 var oldData = appWindowData; 336 update.id = oldData.id; 337 appWindowData = update; 338 339 var currentWindow = currentAppWindow; 340 341 if (!boundsEqual(oldData.innerBounds, update.innerBounds)) 342 dispatchEventIfExists(currentWindow, "onBoundsChanged"); 343 344 if (!oldData.fullscreen && update.fullscreen) 345 dispatchEventIfExists(currentWindow, "onFullscreened"); 346 if (!oldData.minimized && update.minimized) 347 dispatchEventIfExists(currentWindow, "onMinimized"); 348 if (!oldData.maximized && update.maximized) 349 dispatchEventIfExists(currentWindow, "onMaximized"); 350 351 if ((oldData.fullscreen && !update.fullscreen) || 352 (oldData.minimized && !update.minimized) || 353 (oldData.maximized && !update.maximized)) 354 dispatchEventIfExists(currentWindow, "onRestored"); 355 356 if (oldData.alphaEnabled !== update.alphaEnabled) 357 dispatchEventIfExists(currentWindow, "onAlphaEnabledChanged"); 358}; 359 360function onAppWindowShownForTests() { 361 if (!currentAppWindow) 362 return; 363 364 if (!currentAppWindow.firstShowHasHappened) 365 dispatchEventIfExists(currentAppWindow, "onWindowFirstShownForTests"); 366 367 currentAppWindow.firstShowHasHappened = true; 368} 369 370function onAppWindowClosed() { 371 if (!currentAppWindow) 372 return; 373 dispatchEventIfExists(currentAppWindow, "onClosed"); 374} 375 376function updateBounds(boundsType, bounds) { 377 if (!currentWindowInternal) 378 return; 379 380 currentWindowInternal.setBounds(boundsType, bounds); 381} 382 383function updateSizeConstraints(boundsType, constraints) { 384 if (!currentWindowInternal) 385 return; 386 387 forEach(constraints, function(key, value) { 388 // From the perspective of the API, null is used to reset constraints. 389 // We need to convert this to 0 because a value of null is interpreted 390 // the same as undefined in the browser and leaves the constraint unchanged. 391 if (value === null) 392 constraints[key] = 0; 393 }); 394 395 currentWindowInternal.setSizeConstraints(boundsType, constraints); 396} 397 398exports.binding = appWindow.generate(); 399exports.onAppWindowClosed = onAppWindowClosed; 400exports.updateAppWindowProperties = updateAppWindowProperties; 401exports.appWindowShownForTests = onAppWindowShownForTests;