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 7 var appWindowNatives = requireNative('app_window_natives'); 8 var runtimeNatives = requireNative('runtime'); 9 var Binding = require('binding').Binding; 10 var Event = require('event_bindings').Event; 11 var forEach = require('utils').forEach; 12 var renderViewObserverNatives = requireNative('renderViewObserverNatives'); 13 14 var appWindowData = null; 15 var currentAppWindow = null; 16 var currentWindowInternal = null; 17 18 var kSetBoundsFunction = 'setBounds'; 19 var kSetSizeConstraintsFunction = 'setSizeConstraints'; 20 21 // Bounds class definition. 22 var Bounds = function(boundsKey) { 23 privates(this).boundsKey_ = boundsKey; 24 }; 25 Object.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 }); 34 Object.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 }); 43 Object.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 }); 52 Object.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 }); 61 Object.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 }); 70 Object.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 }); 79 Object.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 }); 88 Object.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 }); 97 Bounds.prototype.setPosition = function(left, top) { 98 updateBounds(privates(this).boundsKey_, { left: left, top: top }); 99 }; 100 Bounds.prototype.setSize = function(width, height) { 101 updateBounds(privates(this).boundsKey_, { width: width, height: height }); 102 }; 103 Bounds.prototype.setMinimumSize = function(minWidth, minHeight) { 104 updateSizeConstraints(privates(this).boundsKey_, 105 { minWidth: minWidth, minHeight: minHeight }); 106 }; 107 Bounds.prototype.setMaximumSize = function(maxWidth, maxHeight) { 108 updateSizeConstraints(privates(this).boundsKey_, 109 { maxWidth: maxWidth, maxHeight: maxHeight }); 110 }; 111 112 var appWindow = Binding.create('app.window'); 113 appWindow.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 apiFunctions.setHandleRequest('canSetVisibleOnAllWorkspaces', function() { 197 return /Mac/.test(navigator.platform) || /Linux/.test(navigator.userAgent); 198 }); 199 200 // This is an internal function, but needs to be bound into a closure 201 // so the correct JS context is used for global variables such as 202 // currentWindowInternal, appWindowData, etc. 203 apiFunctions.setHandleRequest('initializeAppWindow', function(params) { 204 currentWindowInternal = 205 Binding.create('app.currentWindowInternal').generate(); 206 var AppWindow = function() { 207 this.innerBounds = new Bounds('innerBounds'); 208 this.outerBounds = new Bounds('outerBounds'); 209 }; 210 forEach(currentWindowInternal, function(key, value) { 211 // Do not add internal functions that should not appear in the AppWindow 212 // interface. They are called by Bounds mutators. 213 if (key !== kSetBoundsFunction && key !== kSetSizeConstraintsFunction) 214 AppWindow.prototype[key] = value; 215 }); 216 AppWindow.prototype.moveTo = $Function.bind(window.moveTo, window); 217 AppWindow.prototype.resizeTo = $Function.bind(window.resizeTo, window); 218 AppWindow.prototype.contentWindow = window; 219 AppWindow.prototype.onClosed = new Event(); 220 AppWindow.prototype.onWindowFirstShownForTests = new Event(); 221 AppWindow.prototype.close = function() { 222 this.contentWindow.close(); 223 }; 224 AppWindow.prototype.getBounds = function() { 225 // This is to maintain backcompatibility with a bug on Windows and 226 // ChromeOS, which returns the position of the window but the size of 227 // the content. 228 var innerBounds = appWindowData.innerBounds; 229 var outerBounds = appWindowData.outerBounds; 230 return { left: outerBounds.left, top: outerBounds.top, 231 width: innerBounds.width, height: innerBounds.height }; 232 }; 233 AppWindow.prototype.setBounds = function(bounds) { 234 updateBounds('bounds', bounds); 235 }; 236 AppWindow.prototype.isFullscreen = function() { 237 return appWindowData.fullscreen; 238 }; 239 AppWindow.prototype.isMinimized = function() { 240 return appWindowData.minimized; 241 }; 242 AppWindow.prototype.isMaximized = function() { 243 return appWindowData.maximized; 244 }; 245 AppWindow.prototype.isAlwaysOnTop = function() { 246 return appWindowData.alwaysOnTop; 247 }; 248 AppWindow.prototype.alphaEnabled = function() { 249 return appWindowData.alphaEnabled; 250 }; 251 AppWindow.prototype.handleWindowFirstShownForTests = function(callback) { 252 // This allows test apps to get have their callback run even if they 253 // call this after the first show has happened. 254 if (this.firstShowHasHappened) { 255 callback(); 256 return; 257 } 258 this.onWindowFirstShownForTests.addListener(callback); 259 } 260 261 Object.defineProperty(AppWindow.prototype, 'id', {get: function() { 262 return appWindowData.id; 263 }}); 264 265 // These properties are for testing. 266 Object.defineProperty( 267 AppWindow.prototype, 'hasFrameColor', {get: function() { 268 return appWindowData.hasFrameColor; 269 }}); 270 271 Object.defineProperty(AppWindow.prototype, 'activeFrameColor', 272 {get: function() { 273 return appWindowData.activeFrameColor; 274 }}); 275 276 Object.defineProperty(AppWindow.prototype, 'inactiveFrameColor', 277 {get: function() { 278 return appWindowData.inactiveFrameColor; 279 }}); 280 281 appWindowData = { 282 id: params.id || '', 283 innerBounds: { 284 left: params.innerBounds.left, 285 top: params.innerBounds.top, 286 width: params.innerBounds.width, 287 height: params.innerBounds.height, 288 289 minWidth: params.innerBounds.minWidth, 290 minHeight: params.innerBounds.minHeight, 291 maxWidth: params.innerBounds.maxWidth, 292 maxHeight: params.innerBounds.maxHeight 293 }, 294 outerBounds: { 295 left: params.outerBounds.left, 296 top: params.outerBounds.top, 297 width: params.outerBounds.width, 298 height: params.outerBounds.height, 299 300 minWidth: params.outerBounds.minWidth, 301 minHeight: params.outerBounds.minHeight, 302 maxWidth: params.outerBounds.maxWidth, 303 maxHeight: params.outerBounds.maxHeight 304 }, 305 fullscreen: params.fullscreen, 306 minimized: params.minimized, 307 maximized: params.maximized, 308 alwaysOnTop: params.alwaysOnTop, 309 hasFrameColor: params.hasFrameColor, 310 activeFrameColor: params.activeFrameColor, 311 inactiveFrameColor: params.inactiveFrameColor, 312 alphaEnabled: params.alphaEnabled 313 }; 314 currentAppWindow = new AppWindow; 315 }); 316 }); 317 318 function boundsEqual(bounds1, bounds2) { 319 if (!bounds1 || !bounds2) 320 return false; 321 return (bounds1.left == bounds2.left && bounds1.top == bounds2.top && 322 bounds1.width == bounds2.width && bounds1.height == bounds2.height); 323 } 324 325 function dispatchEventIfExists(target, name) { 326 // Sometimes apps like to put their own properties on the window which 327 // break our assumptions. 328 var event = target[name]; 329 if (event && (typeof event.dispatch == 'function')) 330 event.dispatch(); 331 else 332 console.warn('Could not dispatch ' + name + ', event has been clobbered'); 333 } 334 335 function updateAppWindowProperties(update) { 336 if (!appWindowData) 337 return; 338 339 var oldData = appWindowData; 340 update.id = oldData.id; 341 appWindowData = update; 342 343 var currentWindow = currentAppWindow; 344 345 if (!boundsEqual(oldData.innerBounds, update.innerBounds)) 346 dispatchEventIfExists(currentWindow, "onBoundsChanged"); 347 348 if (!oldData.fullscreen && update.fullscreen) 349 dispatchEventIfExists(currentWindow, "onFullscreened"); 350 if (!oldData.minimized && update.minimized) 351 dispatchEventIfExists(currentWindow, "onMinimized"); 352 if (!oldData.maximized && update.maximized) 353 dispatchEventIfExists(currentWindow, "onMaximized"); 354 355 if ((oldData.fullscreen && !update.fullscreen) || 356 (oldData.minimized && !update.minimized) || 357 (oldData.maximized && !update.maximized)) 358 dispatchEventIfExists(currentWindow, "onRestored"); 359 360 if (oldData.alphaEnabled !== update.alphaEnabled) 361 dispatchEventIfExists(currentWindow, "onAlphaEnabledChanged"); 362 }; 363 364 function onAppWindowShownForTests() { 365 if (!currentAppWindow) 366 return; 367 368 if (!currentAppWindow.firstShowHasHappened) 369 dispatchEventIfExists(currentAppWindow, "onWindowFirstShownForTests"); 370 371 currentAppWindow.firstShowHasHappened = true; 372 } 373 374 function onAppWindowClosed() { 375 if (!currentAppWindow) 376 return; 377 dispatchEventIfExists(currentAppWindow, "onClosed"); 378 } 379 380 function updateBounds(boundsType, bounds) { 381 if (!currentWindowInternal) 382 return; 383 384 currentWindowInternal.setBounds(boundsType, bounds); 385 } 386 387 function updateSizeConstraints(boundsType, constraints) { 388 if (!currentWindowInternal) 389 return; 390 391 forEach(constraints, function(key, value) { 392 // From the perspective of the API, null is used to reset constraints. 393 // We need to convert this to 0 because a value of null is interpreted 394 // the same as undefined in the browser and leaves the constraint unchanged. 395 if (value === null) 396 constraints[key] = 0; 397 }); 398 399 currentWindowInternal.setSizeConstraints(boundsType, constraints); 400 } 401 402 exports.binding = appWindow.generate(); 403 exports.onAppWindowClosed = onAppWindowClosed; 404 exports.updateAppWindowProperties = updateAppWindowProperties; 405 exports.appWindowShownForTests = onAppWindowShownForTests;