• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;