 |
1 # -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
2 # This Source Code Form is subject to the terms of the Mozilla Public
3 # License, v. 2.0. If a copy of the MPL was not distributed with this
4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
6 var FullScreen = {
7 _XULNS: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
8
9 init: function() {
10 // called when we go into full screen, even if initiated by a web page script
11 window.addEventListener("fullscreen", this, true);
12 window.messageManager.addMessageListener("MozEnteredDomFullscreen", this);
13
14 if (window.fullScreen)
15 this.toggle();
16 },
17
18 uninit: function() {
19 window.messageManager.removeMessageListener("MozEnteredDomFullscreen", this);
20 this.cleanup();
21 },
22
23 toggle: function (event) {
24 var enterFS = window.fullScreen;
25
26 // We get the fullscreen event _before_ the window transitions into or out of FS mode.
27 if (event && event.type == "fullscreen")
28 enterFS = !enterFS;
29
30 // Toggle the View:FullScreen command, which controls elements like the
31 // fullscreen menuitem, and menubars.
32 let fullscreenCommand = document.getElementById("View:FullScreen");
33 if (enterFS) {
34 fullscreenCommand.setAttribute("checked", enterFS);
35 } else {
36 fullscreenCommand.removeAttribute("checked");
37 }
38
39 #ifdef XP_MACOSX
40 // Make sure the menu items are adjusted.
41 document.getElementById("enterFullScreenItem").hidden = enterFS;
42 document.getElementById("exitFullScreenItem").hidden = !enterFS;
43 #endif
44
45 if (!this._fullScrToggler) {
46 this._fullScrToggler = document.getElementById("fullscr-toggler");
47 this._fullScrToggler.addEventListener("mouseover", this._expandCallback, false);
48 this._fullScrToggler.addEventListener("dragenter", this._expandCallback, false);
49 }
50
51 // On OS X Lion we don't want to hide toolbars when entering fullscreen, unless
52 // we're entering DOM fullscreen, in which case we should hide the toolbars.
53 // If we're leaving fullscreen, then we'll go through the exit code below to
54 // make sure toolbars are made visible in the case of DOM fullscreen.
55 if (enterFS && this.useLionFullScreen) {
56 if (document.mozFullScreen) {
57 this.showXULChrome("toolbar", false);
58 }
59 else {
60 gNavToolbox.setAttribute("inFullscreen", true);
61 document.documentElement.setAttribute("inFullscreen", true);
62 }
63 return;
64 }
65
66 // show/hide menubars, toolbars (except the full screen toolbar)
67 this.showXULChrome("toolbar", !enterFS);
68
69 if (enterFS) {
70 document.addEventListener("keypress", this._keyToggleCallback, false);
71 document.addEventListener("popupshown", this._setPopupOpen, false);
72 document.addEventListener("popuphidden", this._setPopupOpen, false);
73 // We don't animate the toolbar collapse if in DOM full-screen mode,
74 // as the size of the content area would still be changing after the
75 // mozfullscreenchange event fired, which could confuse content script.
76 this._shouldAnimate = !document.mozFullScreen;
77 this.mouseoverToggle(false);
78 }
79 else {
80 // The user may quit fullscreen during an animation
81 this._cancelAnimation();
82 gNavToolbox.style.marginTop = "";
83 if (this._isChromeCollapsed)
84 this.mouseoverToggle(true);
85 // This is needed if they use the context menu to quit fullscreen
86 this._isPopupOpen = false;
87
88 document.documentElement.removeAttribute("inDOMFullscreen");
89
90 this.cleanup();
91 }
92 },
93
94 exitDomFullScreen : function() {
95 document.mozCancelFullScreen();
96 },
97
98 handleEvent: function (event) {
99 switch (event.type) {
100 case "activate":
101 if (document.mozFullScreen) {
102 this.showWarning(this.fullscreenOrigin);
103 }
104 break;
105 case "fullscreen":
106 this.toggle(event);
107 break;
108 case "transitionend":
109 if (event.propertyName == "opacity")
110 this.cancelWarning();
111 break;
112 }
113 },
114
115 receiveMessage: function(aMessage) {
116 if (aMessage.name == "MozEnteredDomFullscreen") {
117 // If we're a multiprocess browser, then the request to enter fullscreen
118 // did not bubble up to the root browser document - it stopped at the root
119 // of the content document. That means we have to kick off the switch to
120 // fullscreen here at the operating system level in the parent process
121 // ourselves.
122 let data = aMessage.data;
123 let browser = aMessage.target;
124 if (gMultiProcessBrowser && browser.getAttribute("remote") == "true") {
125 let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
126 .getInterface(Ci.nsIDOMWindowUtils);
127 windowUtils.remoteFrameFullscreenChanged(browser, data.origin);
128 }
129 this.enterDomFullscreen(browser, data.origin);
130 }
131 },
132
133 enterDomFullscreen : function(aBrowser, aOrigin) {
134 if (!document.mozFullScreen)
135 return;
136
137 // If we've received a fullscreen notification, we have to ensure that the
138 // element that's requesting fullscreen belongs to the browser that's currently
139 // active. If not, we exit fullscreen since the "full-screen document" isn't
140 // actually visible now.
141 if (gBrowser.selectedBrowser != aBrowser) {
142 document.mozCancelFullScreen();
143 return;
144 }
145
146 let focusManager = Services.focus;
147 if (focusManager.activeWindow != window) {
148 // The top-level window has lost focus since the request to enter
149 // full-screen was made. Cancel full-screen.
150 document.mozCancelFullScreen();
151 return;
152 }
153
154 document.documentElement.setAttribute("inDOMFullscreen", true);
155
156 if (gFindBarInitialized)
157 gFindBar.close();
158
159 this.showWarning(aOrigin);
160
161 // Exit DOM full-screen mode upon open, close, or change tab.
162 gBrowser.tabContainer.addEventListener("TabOpen", this.exitDomFullScreen);
163 gBrowser.tabContainer.addEventListener("TabClose", this.exitDomFullScreen);
164 gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen);
165
166 // Add listener to detect when the fullscreen window is re-focused.
167 // If a fullscreen window loses focus, we show a warning when the
168 // fullscreen window is refocused.
169 if (!this.useLionFullScreen) {
170 window.addEventListener("activate", this);
171 }
172
173 // Cancel any "hide the toolbar" animation which is in progress, and make
174 // the toolbar hide immediately.
175 this._cancelAnimation();
176 this.mouseoverToggle(false);
177 },
178
179 cleanup: function () {
180 if (window.fullScreen) {
181 MousePosTracker.removeListener(this);
182 document.removeEventListener("keypress", this._keyToggleCallback, false);
183 document.removeEventListener("popupshown", this._setPopupOpen, false);
184 document.removeEventListener("popuphidden", this._setPopupOpen, false);
185
186 this.cancelWarning();
187 gBrowser.tabContainer.removeEventListener("TabOpen", this.exitDomFullScreen);
188 gBrowser.tabContainer.removeEventListener("TabClose", this.exitDomFullScreen);
189 gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen);
190 if (!this.useLionFullScreen)
191 window.removeEventListener("activate", this);
192
193 window.messageManager
194 .broadcastAsyncMessage("DOMFullscreen:Cleanup");
195 }
196 },
197
198 getMouseTargetRect: function()
199 {
200 return this._mouseTargetRect;
201 },
202
203 // Event callbacks
204 _expandCallback: function()
205 {
206 FullScreen.mouseoverToggle(true);
207 },
208 onMouseEnter: function()
209 {
210 FullScreen.mouseoverToggle(false);
211 },
212 _keyToggleCallback: function(aEvent)
213 {
214 // if we can use the keyboard (eg Ctrl+L or Ctrl+E) to open the toolbars, we
215 // should provide a way to collapse them too.
216 if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) {
217 FullScreen._shouldAnimate = false;
218 FullScreen.mouseoverToggle(false, true);
219 }
220 // F6 is another shortcut to the address bar, but its not covered in OpenLocation()
221 else if (aEvent.keyCode == aEvent.DOM_VK_F6)
222 FullScreen.mouseoverToggle(true);
223 },
224
225 // Checks whether we are allowed to collapse the chrome
226 _isPopupOpen: false,
227 _isChromeCollapsed: false,
228 _safeToCollapse: function(forceHide)
229 {
230 if (!gPrefService.getBoolPref("browser.fullscreen.autohide"))
231 return false;
232
233 // a popup menu is open in chrome: don't collapse chrome
234 if (!forceHide && this._isPopupOpen)
235 return false;
236
237 // a textbox in chrome is focused (location bar anyone?): don't collapse chrome
238 if (document.commandDispatcher.focusedElement &&
239 document.commandDispatcher.focusedElement.ownerDocument == document &&
240 document.commandDispatcher.focusedElement.localName == "input") {
241 if (forceHide)
242 // hidden textboxes that still have focus are bad bad bad
243 document.commandDispatcher.focusedElement.blur();
244 else
245 return false;
246 }
247 return true;
248 },
249
250 _setPopupOpen: function(aEvent)
251 {
252 // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed.
253 // Otherwise, they would not affect chrome and the user would expect the chrome to go away.
254 // e.g. we wouldn't want the autoscroll icon firing this event, so when the user
255 // toggles chrome when moving mouse to the top, it doesn't go away again.
256 if (aEvent.type == "popupshown" && !FullScreen._isChromeCollapsed &&
257 aEvent.target.localName != "tooltip" && aEvent.target.localName != "window")
258 FullScreen._isPopupOpen = true;
259 else if (aEvent.type == "popuphidden" && aEvent.target.localName != "tooltip" &&
260 aEvent.target.localName != "window")
261 FullScreen._isPopupOpen = false;
262 },
263
264 // Autohide helpers for the context menu item
265 getAutohide: function(aItem)
266 {
267 aItem.setAttribute("checked", gPrefService.getBoolPref("browser.fullscreen.autohide"));
268 },
269 setAutohide: function()
270 {
271 gPrefService.setBoolPref("browser.fullscreen.autohide", !gPrefService.getBoolPref("browser.fullscreen.autohide"));
272 },
273
274 // Animate the toolbars disappearing
275 _shouldAnimate: true,
276 _isAnimating: false,
277 _animationTimeout: 0,
278 _animationHandle: 0,
279 _animateUp: function() {
280 // check again, the user may have done something before the animation was due to start
281 if (!window.fullScreen || !this._safeToCollapse(false)) {
282 this._isAnimating = false;
283 this._shouldAnimate = true;
284 return;
285 }
286
287 this._animateStartTime = window.mozAnimationStartTime;
288 if (!this._animationHandle)
289 this._animationHandle = window.mozRequestAnimationFrame(this);
290 },
291
292 sample: function (timeStamp) {
293 const duration = 1500;
294 const timePassed = timeStamp - this._animateStartTime;
295 const pos = timePassed >= duration ? 1 :
296 1 - Math.pow(1 - timePassed / duration, 4);
297
298 if (pos >= 1) {
299 // We've animated enough
300 this._cancelAnimation();
301 gNavToolbox.style.marginTop = "";
302 this.mouseoverToggle(false);
303 return;
304 }
305
306 gNavToolbox.style.marginTop = (gNavToolbox.boxObject.height * pos * -1) + "px";
307 this._animationHandle = window.mozRequestAnimationFrame(this);
308 },
309
310 _cancelAnimation: function() {
311 window.mozCancelAnimationFrame(this._animationHandle);
312 this._animationHandle = 0;
313 clearTimeout(this._animationTimeout);
314 this._isAnimating = false;
315 this._shouldAnimate = false;
316 },
317
318 cancelWarning: function(event) {
319 if (!this.warningBox)
320 return;
321 this.warningBox.removeEventListener("transitionend", this);
322 if (this.warningFadeOutTimeout) {
323 clearTimeout(this.warningFadeOutTimeout);
324 this.warningFadeOutTimeout = null;
325 }
326
327 // Ensure focus switches away from the (now hidden) warning box. If the user
328 // clicked buttons in the fullscreen key authorization UI, it would have been
329 // focused, and any key events would be directed at the (now hidden) chrome
330 // document instead of the target document.
331 gBrowser.selectedBrowser.focus();
332
333 this.warningBox.setAttribute("hidden", true);
334 this.warningBox.removeAttribute("fade-warning-out");
335 this.warningBox.removeAttribute("obscure-browser");
336 this.warningBox = null;
337 },
338
339 setFullscreenAllowed: function(isApproved) {
340 // The "remember decision" checkbox is hidden when showing for documents that
341 // the permission manager can't handle (documents with URIs without a host).
342 // We simply require those to be approved every time instead.
343 let rememberCheckbox = document.getElementById("full-screen-remember-decision");
344 let uri = BrowserUtils.makeURI(this.fullscreenOrigin);
345 if (!rememberCheckbox.hidden) {
346 if (rememberCheckbox.checked)
347 Services.perms.add(uri,
348 "fullscreen",
349 isApproved ? Services.perms.ALLOW_ACTION : Services.perms.DENY_ACTION,
350 Services.perms.EXPIRE_NEVER);
351 else if (isApproved) {
352 // The user has only temporarily approved fullscren for this fullscreen
353 // session only. Add the permission (so Gecko knows to approve any further
354 // fullscreen requests for this host in this fullscreen session) but add
355 // a listener to revoke the permission when the chrome document exits
356 // fullscreen.
357 Services.perms.add(uri,
358 "fullscreen",
359 Services.perms.ALLOW_ACTION,
360 Services.perms.EXPIRE_SESSION);
361 let host = uri.host;
362 var onFullscreenchange = function onFullscreenchange(event) {
363 if (event.target == document && document.mozFullScreenElement == null) {
364 // The chrome document has left fullscreen. Remove the temporary permission grant.
365 Services.perms.remove(host, "fullscreen");
366 document.removeEventListener("mozfullscreenchange", onFullscreenchange);
367 }
368 }
369 document.addEventListener("mozfullscreenchange", onFullscreenchange);
370 }
371 }
372 if (this.warningBox)
373 this.warningBox.setAttribute("fade-warning-out", "true");
374 // If the document has been granted fullscreen, notify Gecko so it can resume
375 // any pending pointer lock requests, otherwise exit fullscreen; the user denied
376 // the fullscreen request.
377 if (isApproved) {
378 gBrowser.selectedBrowser
379 .messageManager
380 .sendAsyncMessage("DOMFullscreen:Approved");
381 } else {
382 document.mozCancelFullScreen();
383 }
384 },
385
386 warningBox: null,
387 warningFadeOutTimeout: null,
388
389 // Shows the fullscreen approval UI, or if the domain has already been approved
390 // for fullscreen, shows a warning that the site has entered fullscreen for a short
391 // duration.
392 showWarning: function(aOrigin) {
393 if (!document.mozFullScreen ||
394 !gPrefService.getBoolPref("full-screen-api.approval-required"))
395 return;
396
397 // Set the strings on the fullscreen approval UI.
398 this.fullscreenOrigin = aOrigin;
399 let uri = BrowserUtils.makeURI(aOrigin);
400 let host = null;
401 try {
402 host = uri.host;
403 } catch (e) { }
404 let hostLabel = document.getElementById("full-screen-domain-text");
405 let rememberCheckbox = document.getElementById("full-screen-remember-decision");
406 let isApproved = false;
407 if (host) {
408 // Document's principal's URI has a host. Display a warning including the hostname and
409 // show UI to enable the user to permanently grant this host permission to enter fullscreen.
410 let utils = {};
411 Cu.import("resource://gre/modules/DownloadUtils.jsm", utils);
412 let displayHost = utils.DownloadUtils.getURIHost(uri.spec)[0];
413 let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
414
415 hostLabel.textContent = bundle.formatStringFromName("fullscreen.entered", [displayHost], 1);
416 hostLabel.removeAttribute("hidden");
417
418 rememberCheckbox.label = bundle.formatStringFromName("fullscreen.rememberDecision", [displayHost], 1);
419 rememberCheckbox.checked = false;
420 rememberCheckbox.removeAttribute("hidden");
421
422 // Note we only allow documents whose principal's URI has a host to
423 // store permission grants.
424 isApproved = Services.perms.testPermission(uri, "fullscreen") == Services.perms.ALLOW_ACTION;
425 } else {
426 hostLabel.setAttribute("hidden", "true");
427 rememberCheckbox.setAttribute("hidden", "true");
428 }
429
430 // Note: the warning box can be non-null if the warning box from the previous request
431 // wasn't hidden before another request was made.
432 if (!this.warningBox) {
433 this.warningBox = document.getElementById("full-screen-warning-container");
434 // Add a listener to clean up state after the warning is hidden.
435 this.warningBox.addEventListener("transitionend", this);
436 this.warningBox.removeAttribute("hidden");
437 } else {
438 if (this.warningFadeOutTimeout) {
439 clearTimeout(this.warningFadeOutTimeout);
440 this.warningFadeOutTimeout = null;
441 }
442 this.warningBox.removeAttribute("fade-warning-out");
443 }
444
445 // If fullscreen mode has not yet been approved for the fullscreen
446 // document's domain, show the approval UI and don't auto fade out the
447 // fullscreen warning box. Otherwise, we're just notifying of entry into
448 // fullscreen mode. Note if the resource's host is null, we must be
449 // showing a local file or a local data URI, and we require explicit
450 // approval every time.
451 let authUI = document.getElementById("full-screen-approval-pane");
452 if (isApproved) {
453 authUI.setAttribute("hidden", "true");
454 this.warningBox.removeAttribute("obscure-browser");
455 } else {
456 // Partially obscure the <browser> element underneath the approval UI.
457 this.warningBox.setAttribute("obscure-browser", "true");
458 authUI.removeAttribute("hidden");
459 }
460
461 // If we're not showing the fullscreen approval UI, we're just notifying the user
462 // of the transition, so set a timeout to fade the warning out after a few moments.
463 if (isApproved)
464 this.warningFadeOutTimeout =
465 setTimeout(
466 function() {
467 if (this.warningBox)
468 this.warningBox.setAttribute("fade-warning-out", "true");
469 }.bind(this),
470 3000);
471 },
472
473 mouseoverToggle: function(aShow, forceHide)
474 {
475 // Don't do anything if:
476 // a) we're already in the state we want,
477 // b) we're animating and will become collapsed soon, or
478 // c) we can't collapse because it would be undesirable right now
479 if (aShow != this._isChromeCollapsed || (!aShow && this._isAnimating) ||
480 (!aShow && !this._safeToCollapse(forceHide)))
481 return;
482
483 // browser.fullscreen.animateUp
484 // 0 - never animate up
485 // 1 - animate only for first collapse after entering fullscreen (default for perf's sake)
486 // 2 - animate every time it collapses
487 if (gPrefService.getIntPref("browser.fullscreen.animateUp") == 0)
488 this._shouldAnimate = false;
489
490 if (!aShow && this._shouldAnimate) {
491 this._isAnimating = true;
492 this._shouldAnimate = false;
493 this._animationTimeout = setTimeout(this._animateUp.bind(this), 800);
494 return;
495 }
496
497 // Hiding/collapsing the toolbox interferes with the tab bar's scrollbox,
498 // so we just move it off-screen instead. See bug 430687.
499 gNavToolbox.style.marginTop =
500 aShow ? "" : -gNavToolbox.getBoundingClientRect().height + "px";
501
502 this._fullScrToggler.hidden = aShow || document.mozFullScreen;
503
504 if (aShow) {
505 let rect = gBrowser.mPanelContainer.getBoundingClientRect();
506 this._mouseTargetRect = {
507 top: rect.top + 50,
508 bottom: rect.bottom,
509 left: rect.left,
510 right: rect.right
511 };
512 MousePosTracker.addListener(this);
513 } else {
514 MousePosTracker.removeListener(this);
515 }
516
517 this._isChromeCollapsed = !aShow;
518 if (gPrefService.getIntPref("browser.fullscreen.animateUp") == 2)
519 this._shouldAnimate = true;
520 },
521
522 showXULChrome: function(aTag, aShow)
523 {
524 var els = document.getElementsByTagNameNS(this._XULNS, aTag);
525
526 for (let el of els) {
527 // XXX don't interfere with previously collapsed toolbars
528 if (el.getAttribute("fullscreentoolbar") == "true") {
529 if (!aShow) {
530 // Give the main nav bar and the tab bar the fullscreen context menu,
531 // otherwise remove context menu to prevent breakage
532 el.setAttribute("saved-context", el.getAttribute("context"));
533 if (el.id == "nav-bar" || el.id == "TabsToolbar")
534 el.setAttribute("context", "autohide-context");
535 else
536 el.removeAttribute("context");
537
538 // Set the inFullscreen attribute to allow specific styling
539 // in fullscreen mode
540 el.setAttribute("inFullscreen", true);
541 }
542 else {
543 if (el.hasAttribute("saved-context")) {
544 el.setAttribute("context", el.getAttribute("saved-context"));
545 el.removeAttribute("saved-context");
546 }
547 el.removeAttribute("inFullscreen");
548 }
549 } else {
550 // use moz-collapsed so it doesn't persist hidden/collapsed,
551 // so that new windows don't have missing toolbars
552 if (aShow)
553 el.removeAttribute("moz-collapsed");
554 else
555 el.setAttribute("moz-collapsed", "true");
556 }
557 }
558
559 if (aShow) {
560 gNavToolbox.removeAttribute("inFullscreen");
561 document.documentElement.removeAttribute("inFullscreen");
562 } else {
563 gNavToolbox.setAttribute("inFullscreen", true);
564 document.documentElement.setAttribute("inFullscreen", true);
565 }
566
567 var fullscreenctls = document.getElementById("window-controls");
568 var navbar = document.getElementById("nav-bar");
569 var ctlsOnTabbar = window.toolbar.visible;
570 if (fullscreenctls.parentNode == navbar && ctlsOnTabbar) {
571 fullscreenctls.removeAttribute("flex");
572 document.getElementById("TabsToolbar").appendChild(fullscreenctls);
573 }
574 else if (fullscreenctls.parentNode.id == "TabsToolbar" && !ctlsOnTabbar) {
575 fullscreenctls.setAttribute("flex", "1");
576 navbar.appendChild(fullscreenctls);
577 }
578 fullscreenctls.hidden = aShow;
579
580 ToolbarIconColor.inferFromText();
581 }
582 };
583 XPCOMUtils.defineLazyGetter(FullScreen, "useLionFullScreen", function() {
584 // We'll only use OS X Lion full screen if we're
585 // * on OS X
586 // * on Lion or higher (Darwin 11+)
587 // * have fullscreenbutton="true"
588 #ifdef XP_MACOSX
589 return parseFloat(Services.sysinfo.getProperty("version")) >= 11 &&
590 document.documentElement.getAttribute("fullscreenbutton") == "true";
591 #else
592 return false;
593 #endif
594 });
595