Home | History | Annotate | Download | only in resources
      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;