From 697f8f1092f9c5cd03470566bca04fd1a35198de Mon Sep 17 00:00:00 2001 From: Matt Woodrow Date: Fri, 8 Nov 2013 07:12:22 +1300 Subject: [PATCH 01/41] Bug 813041 - Layerize the scrollbar thumb on mobile. r=roc --- layout/base/nsLayoutUtils.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp index 16abed05f968..a61c205ebf12 100644 --- a/layout/base/nsLayoutUtils.cpp +++ b/layout/base/nsLayoutUtils.cpp @@ -1260,6 +1260,13 @@ nsLayoutUtils::GetActiveScrolledRootFor(nsIFrame* aFrame, nsIFrame* parent = GetCrossDocParentFrame(f); if (!parent) break; + nsIAtom* parentType = parent->GetType(); +#ifdef ANDROID + // Treat the slider thumb as being as an active scrolled root + // on mobile so that it can move without repainting. + if (parentType == nsGkAtoms::sliderFrame) + break; +#endif // Sticky frames are active if their nearest scrollable frame // is also active, just keep a record of sticky frames that we // encounter for now. @@ -1267,8 +1274,8 @@ nsLayoutUtils::GetActiveScrolledRootFor(nsIFrame* aFrame, !stickyFrame) { stickyFrame = f; } - nsIScrollableFrame* sf = do_QueryFrame(parent); - if (sf) { + if (parentType == nsGkAtoms::scrollFrame) { + nsIScrollableFrame* sf = do_QueryFrame(parent); if (sf->IsScrollingActive() && sf->GetScrolledFrame() == f) { // If we found a sticky frame inside this active scroll frame, // then use that. Otherwise use the scroll frame. From 588a269dc8e94fe2a206e7e9703eb476397491a8 Mon Sep 17 00:00:00 2001 From: Jeff Walden Date: Fri, 27 Sep 2013 11:19:43 -0700 Subject: [PATCH 02/41] Bug 921561 - Make JS_DECLARE_NEW_METHODS use C++11 "perfect" forwarding (which isn't, because it won't let you pass an expression that's a bit field #nowyouknow), to eliminate issues arising when non-const references are used in these methods with classes that don't copy nicely, or shouldn't be copied for perf reasons. r=luke --HG-- extra : rebase_source : d2b446e0ee6e5613faa962b92acc9f91584fe7ac --- js/public/Utility.h | 126 ++++++++++++++++++++++++++++++++++--------- js/src/jsanalyze.cpp | 12 +++-- 2 files changed, 110 insertions(+), 28 deletions(-) diff --git a/js/public/Utility.h b/js/public/Utility.h index cdaf19ba5f69..1755831f30f1 100644 --- a/js/public/Utility.h +++ b/js/public/Utility.h @@ -250,63 +250,141 @@ static inline void js_free(void* p) }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1))\ + QUALIFIERS T *NEWNAME(P1 &&p1) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3, P4 p4) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3, p4))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3, P4 &&p4) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3),\ + mozilla::Forward(p4)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3, p4, p5))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3, P4 &&p4, P5 &&p5) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3),\ + mozilla::Forward(p4),\ + mozilla::Forward(p5)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3, p4, p5, p6))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3, P4 &&p4, P5 &&p5, P6 &&p6) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3),\ + mozilla::Forward(p4),\ + mozilla::Forward(p5),\ + mozilla::Forward(p6)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3, p4, p5, p6, p7))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3, P4 &&p4, P5 &&p5, P6 &&p6, P7 &&p7) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3),\ + mozilla::Forward(p4),\ + mozilla::Forward(p5),\ + mozilla::Forward(p6),\ + mozilla::Forward(p7)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3, p4, p5, p6, p7, p8))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3, P4 &&p4, P5 &&p5, P6 &&p6, P7 &&p7, P8 &&p8) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3),\ + mozilla::Forward(p4),\ + mozilla::Forward(p5),\ + mozilla::Forward(p6),\ + mozilla::Forward(p7),\ + mozilla::Forward(p8)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8, P9 p9) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3, p4, p5, p6, p7, p8, p9))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3, P4 &&p4, P5 &&p5, P6 &&p6, P7 &&p7, P8 &&p8, P9 &&p9) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3),\ + mozilla::Forward(p4),\ + mozilla::Forward(p5),\ + mozilla::Forward(p6),\ + mozilla::Forward(p7),\ + mozilla::Forward(p8),\ + mozilla::Forward(p9)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8, P9 p9, P10 p10) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3, p4, p5, p6, p7, p8, p9, p10))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3, P4 &&p4, P5 &&p5, P6 &&p6, P7 &&p7, P8 &&p8, P9 &&p9, P10 &&p10) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3),\ + mozilla::Forward(p4),\ + mozilla::Forward(p5),\ + mozilla::Forward(p6),\ + mozilla::Forward(p7),\ + mozilla::Forward(p8),\ + mozilla::Forward(p9),\ + mozilla::Forward(p10)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8, P9 p9, P10 p10, P11 p11) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3, P4 &&p4, P5 &&p5, P6 &&p6, P7 &&p7, P8 &&p8, P9 &&p9, P10 &&p10, P11 &&p11) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3),\ + mozilla::Forward(p4),\ + mozilla::Forward(p5),\ + mozilla::Forward(p6),\ + mozilla::Forward(p7),\ + mozilla::Forward(p8),\ + mozilla::Forward(p9),\ + mozilla::Forward(p10),\ + mozilla::Forward(p11)))\ }\ \ template \ - QUALIFIERS T *NEWNAME(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8, P9 p9, P10 p10, P11 p11, P12 p12) {\ - JS_NEW_BODY(ALLOCATOR, T, (p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12))\ + QUALIFIERS T *NEWNAME(P1 &&p1, P2 &&p2, P3 &&p3, P4 &&p4, P5 &&p5, P6 &&p6, P7 &&p7, P8 &&p8, P9 &&p9, P10 &&p10, P11 &&p11, P12 &&p12) {\ + JS_NEW_BODY(ALLOCATOR, T,\ + (mozilla::Forward(p1),\ + mozilla::Forward(p2),\ + mozilla::Forward(p3),\ + mozilla::Forward(p4),\ + mozilla::Forward(p5),\ + mozilla::Forward(p6),\ + mozilla::Forward(p7),\ + mozilla::Forward(p8),\ + mozilla::Forward(p9),\ + mozilla::Forward(p10),\ + mozilla::Forward(p11),\ + mozilla::Forward(p12)))\ }\ JS_DECLARE_NEW_METHODS(js_new, js_malloc, static JS_ALWAYS_INLINE) diff --git a/js/src/jsanalyze.cpp b/js/src/jsanalyze.cpp index c79771130ff4..7dc85ff0522b 100644 --- a/js/src/jsanalyze.cpp +++ b/js/src/jsanalyze.cpp @@ -520,7 +520,8 @@ ScriptAnalysis::analyzeLifetimes(JSContext *cx) /* Restore all saved variables. :FIXME: maybe do this precisely. */ for (unsigned i = 0; i < savedCount; i++) { LifetimeVariable &var = *saved[i]; - var.lifetime = alloc.new_(offset, var.savedEnd, var.saved); + uint32_t savedEnd = var.savedEnd; + var.lifetime = alloc.new_(offset, savedEnd, var.saved); if (!var.lifetime) { js_free(saved); setOOM(cx); @@ -612,7 +613,8 @@ ScriptAnalysis::analyzeLifetimes(JSContext *cx) * Jumping to a place where this variable is live. Make a new * lifetime segment for the variable. */ - var.lifetime = alloc.new_(offset, var.savedEnd, var.saved); + uint32_t savedEnd = var.savedEnd; + var.lifetime = alloc.new_(offset, savedEnd, var.saved); if (!var.lifetime) { js_free(saved); setOOM(cx); @@ -676,7 +678,8 @@ ScriptAnalysis::addVariable(JSContext *cx, LifetimeVariable &var, unsigned offse } } } - var.lifetime = cx->typeLifoAlloc().new_(offset, var.savedEnd, var.saved); + uint32_t savedEnd = var.savedEnd; + var.lifetime = cx->typeLifoAlloc().new_(offset, savedEnd, var.saved); if (!var.lifetime) { setOOM(cx); return; @@ -691,7 +694,8 @@ ScriptAnalysis::killVariable(JSContext *cx, LifetimeVariable &var, unsigned offs { if (!var.lifetime) { /* Make a point lifetime indicating the write. */ - Lifetime *lifetime = cx->typeLifoAlloc().new_(offset, var.savedEnd, var.saved); + uint32_t savedEnd = var.savedEnd; + Lifetime *lifetime = cx->typeLifoAlloc().new_(offset, savedEnd, var.saved); if (!lifetime) { setOOM(cx); return; From 9858ac7aaf589543643808ac029293e3b2babe34 Mon Sep 17 00:00:00 2001 From: Jeff Walden Date: Thu, 12 Sep 2013 15:45:43 -0700 Subject: [PATCH 03/41] Bug 934669 - Make a global object's Object.prototype.watch/unwatch warn about deprecation when called for the first time. r=evilpie --HG-- extra : rebase_source : 0856fe798e107e4f0e8bd8b7896107448c6c9364 --- js/src/builtin/Object.cpp | 25 ++++++++++++++++++------- js/src/js.msg | 2 +- js/src/vm/GlobalObject.cpp | 17 +++++++++++++++++ js/src/vm/GlobalObject.h | 23 +++++++++++++---------- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/js/src/builtin/Object.cpp b/js/src/builtin/Object.cpp index 1256b43227a5..f4c4bbb69339 100644 --- a/js/src/builtin/Object.cpp +++ b/js/src/builtin/Object.cpp @@ -552,6 +552,13 @@ obj_watch(JSContext *cx, unsigned argc, Value *vp) { CallArgs args = CallArgsFromVp(argc, vp); + RootedObject obj(cx, ToObject(cx, args.thisv())); + if (!obj) + return false; + + if (!GlobalObject::warnOnceAboutWatch(cx, obj)) + return false; + if (args.length() <= 1) { js_ReportMissingArg(cx, args.calleev(), 1); return false; @@ -565,10 +572,6 @@ obj_watch(JSContext *cx, unsigned argc, Value *vp) if (!ValueToId(cx, args[0], &propid)) return false; - RootedObject obj(cx, ToObject(cx, args.thisv())); - if (!obj) - return false; - RootedValue tmp(cx); unsigned attrs; if (!CheckAccess(cx, obj, propid, JSACC_WATCH, &tmp, &attrs)) @@ -587,15 +590,23 @@ obj_unwatch(JSContext *cx, unsigned argc, Value *vp) RootedObject obj(cx, ToObject(cx, args.thisv())); if (!obj) return false; - args.rval().setUndefined(); + + if (!GlobalObject::warnOnceAboutWatch(cx, obj)) + return false; + RootedId id(cx); - if (argc != 0) { + if (args.length() != 0) { if (!ValueToId(cx, args[0], &id)) return false; } else { id = JSID_VOID; } - return JS_ClearWatchPoint(cx, obj, id, nullptr, nullptr); + + if (!JS_ClearWatchPoint(cx, obj, id, nullptr, nullptr)) + return false; + + args.rval().setUndefined(); + return true; } #endif /* JS_HAS_OBJ_WATCHPOINT */ diff --git a/js/src/js.msg b/js/src/js.msg index 8bd4f8bd0556..296811dcc926 100644 --- a/js/src/js.msg +++ b/js/src/js.msg @@ -244,7 +244,7 @@ MSG_DEF(JSMSG_UNUSED190, 190, 0, JSEXN_NONE, "") MSG_DEF(JSMSG_UNUSED191, 191, 0, JSEXN_NONE, "") MSG_DEF(JSMSG_UNUSED192, 192, 0, JSEXN_NONE, "") MSG_DEF(JSMSG_BAD_FOR_EACH_LOOP, 193, 0, JSEXN_SYNTAXERR, "invalid for each loop") -MSG_DEF(JSMSG_UNUSED194, 194, 0, JSEXN_NONE, "") +MSG_DEF(JSMSG_OBJECT_WATCH_DEPRECATED,194, 0, JSEXN_TYPEERR, "Object.prototype.watch and unwatch are very slow, non-standard, and deprecated; use a getter/setter instead") MSG_DEF(JSMSG_UNUSED195, 195, 0, JSEXN_NONE, "") MSG_DEF(JSMSG_UNUSED196, 196, 0, JSEXN_NONE, "") MSG_DEF(JSMSG_INTERNAL_INTL_ERROR, 197, 0, JSEXN_ERR, "internal error while computing Intl data") diff --git a/js/src/vm/GlobalObject.cpp b/js/src/vm/GlobalObject.cpp index 7e968403356a..26d608832250 100644 --- a/js/src/vm/GlobalObject.cpp +++ b/js/src/vm/GlobalObject.cpp @@ -504,6 +504,23 @@ GlobalObject::isRuntimeCodeGenEnabled(JSContext *cx, Handle globa return !v.isFalse(); } +/* static */ bool +GlobalObject::warnOnceAboutWatch(JSContext *cx, HandleObject obj) +{ + Rooted global(cx, &obj->global()); + HeapSlot &v = global->getSlotRef(WARNED_WATCH_DEPRECATED); + if (v.isUndefined()) { + // Warn only once per global object. + if (!JS_ReportErrorFlagsAndNumber(cx, JSREPORT_WARNING, js_GetErrorMessage, NULL, + JSMSG_OBJECT_WATCH_DEPRECATED)) + { + return false; + } + v.init(global, HeapSlot::Slot, WARNED_WATCH_DEPRECATED, BooleanValue(true)); + } + return true; +} + JSFunction * GlobalObject::createConstructor(JSContext *cx, Native ctor, JSAtom *nameArg, unsigned length, gc::AllocKind kind) diff --git a/js/src/vm/GlobalObject.h b/js/src/vm/GlobalObject.h index 401bf8b2c4e3..cbe9ee03d224 100644 --- a/js/src/vm/GlobalObject.h +++ b/js/src/vm/GlobalObject.h @@ -105,8 +105,8 @@ class GlobalObject : public JSObject static const unsigned NUMBER_FORMAT_PROTO = COLLATOR_PROTO + 1; static const unsigned DATE_TIME_FORMAT_PROTO = NUMBER_FORMAT_PROTO + 1; static const unsigned REGEXP_STATICS = DATE_TIME_FORMAT_PROTO + 1; - static const unsigned FUNCTION_NS = REGEXP_STATICS + 1; - static const unsigned RUNTIME_CODEGEN_ENABLED = FUNCTION_NS + 1; + static const unsigned WARNED_WATCH_DEPRECATED = REGEXP_STATICS + 1; + static const unsigned RUNTIME_CODEGEN_ENABLED = WARNED_WATCH_DEPRECATED + 1; static const unsigned DEBUGGERS = RUNTIME_CODEGEN_ENABLED + 1; static const unsigned INTRINSICS = DEBUGGERS + 1; static const unsigned ARRAY_TYPE = INTRINSICS + 1; @@ -114,14 +114,13 @@ class GlobalObject : public JSObject /* Total reserved-slot count for global objects. */ static const unsigned RESERVED_SLOTS = ARRAY_TYPE + 1; - void staticAsserts() { - /* - * The slot count must be in the public API for JSCLASS_GLOBAL_FLAGS, - * and we aren't going to expose GlobalObject, so just assert that the - * two values are synchronized. - */ - JS_STATIC_ASSERT(JSCLASS_GLOBAL_SLOT_COUNT == RESERVED_SLOTS); - } + /* + * The slot count must be in the public API for JSCLASS_GLOBAL_FLAGS, and + * we won't expose GlobalObject, so just assert that the two values are + * synchronized. + */ + static_assert(JSCLASS_GLOBAL_SLOT_COUNT == RESERVED_SLOTS, + "global object slot counts are inconsistent"); friend JSObject * ::js_InitObjectClass(JSContext *cx, js::HandleObject); @@ -575,6 +574,10 @@ class GlobalObject : public JSObject static bool isRuntimeCodeGenEnabled(JSContext *cx, Handle global); + // Warn about use of the deprecated watch/unwatch functions in the global + // in which |obj| was created, if no prior warning was given. + static bool warnOnceAboutWatch(JSContext *cx, HandleObject obj); + const Value &getOriginalEval() const { JS_ASSERT(getSlot(EVAL).isObject()); return getSlot(EVAL); From 56f5daecc23e284c37deeb2b32651854d73af857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20Qu=C3=A8ze?= Date: Thu, 7 Nov 2013 14:01:55 +0100 Subject: [PATCH 04/41] Bug 773988 - Allow saving a snapshot from a media stream without a known filename, r=dolske. --- browser/base/content/nsContextMenu.js | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js index a08898dbd904..fb9a5241631b 100644 --- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -986,16 +986,15 @@ nsContextMenu.prototype = { }, saveVideoFrameAsImage: function () { - urlSecurityCheck(this.mediaURL, - this._unremotePrincipal(this.browser.contentPrincipal), - Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT); let name = ""; - try { - let uri = makeURI(this.mediaURL); - let url = uri.QueryInterface(Ci.nsIURL); - if (url.fileBaseName) - name = decodeURI(url.fileBaseName) + ".jpg"; - } catch (e) { } + if (this.mediaURL) { + try { + let uri = makeURI(this.mediaURL); + let url = uri.QueryInterface(Ci.nsIURL); + if (url.fileBaseName) + name = decodeURI(url.fileBaseName) + ".jpg"; + } catch (e) { } + } if (!name) name = "snapshot.jpg"; var video = this.target; From b83275098aa3962bce7d69d8b40de21669f2d688 Mon Sep 17 00:00:00 2001 From: Paul Rouget Date: Thu, 7 Nov 2013 14:40:23 +0100 Subject: [PATCH 05/41] Bug 890195 - device-width media queries should use the page width, not the actual device width. r=bz --- .../responsivedesign/responsivedesign.jsm | 9 +++ .../responsivedesign/test/browser.ini | 1 + .../test/browser_responsive_devicewidth.js | 64 +++++++++++++++++++ docshell/base/nsDocShell.cpp | 22 +++++++ docshell/base/nsDocShell.h | 1 + docshell/base/nsIDocShell.idl | 10 ++- dom/base/nsScreen.cpp | 13 ++++ dom/base/nsScreen.h | 20 ++++++ layout/base/nsPresContext.cpp | 11 ++++ layout/base/nsPresContext.h | 2 + layout/style/nsMediaFeatures.cpp | 8 ++- 11 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 browser/devtools/responsivedesign/test/browser_responsive_devicewidth.js diff --git a/browser/devtools/responsivedesign/responsivedesign.jsm b/browser/devtools/responsivedesign/responsivedesign.jsm index 30715e9148f8..4a54f003a327 100644 --- a/browser/devtools/responsivedesign/responsivedesign.jsm +++ b/browser/devtools/responsivedesign/responsivedesign.jsm @@ -181,6 +181,12 @@ function ResponsiveUI(aWindow, aTab) this.buildUI(); this.checkMenus(); + this.docShell = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + + this.docShell.deviceSizeIsPageSize = true; + try { if (Services.prefs.getBoolPref("devtools.responsiveUI.rotate")) { this.rotate(); @@ -249,6 +255,8 @@ ResponsiveUI.prototype = { return; this.closing = true; + this.docShell.deviceSizeIsPageSize = false; + this.browser.removeEventListener("load", this.bound_onPageLoad, true); this.browser.removeEventListener("unload", this.bound_onPageUnload, true); @@ -288,6 +296,7 @@ ResponsiveUI.prototype = { this.container.removeAttribute("responsivemode"); this.stack.removeAttribute("responsivemode"); + delete this.docShell; delete this.tab.__responsiveUI; if (this.touchEventHandler) this.touchEventHandler.stop(); diff --git a/browser/devtools/responsivedesign/test/browser.ini b/browser/devtools/responsivedesign/test/browser.ini index 46c19e38d255..2b95ce11f12b 100644 --- a/browser/devtools/responsivedesign/test/browser.ini +++ b/browser/devtools/responsivedesign/test/browser.ini @@ -9,3 +9,4 @@ support-files = [browser_responsiveui.js] [browser_responsiveui_touch.js] [browser_responsiveuiaddcustompreset.js] +[browser_responsive_devicewidth.js] diff --git a/browser/devtools/responsivedesign/test/browser_responsive_devicewidth.js b/browser/devtools/responsivedesign/test/browser_responsive_devicewidth.js new file mode 100644 index 000000000000..5ece52cb0a59 --- /dev/null +++ b/browser/devtools/responsivedesign/test/browser_responsive_devicewidth.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + let instance; + let mgr = ResponsiveUI.ResponsiveUIManager; + + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function onload() { + gBrowser.selectedBrowser.removeEventListener("load", onload, true); + waitForFocus(startTest, content); + }, true); + + content.location = "data:text/html,mop"; + + function startTest() { + mgr.once("on", function() {executeSoon(onUIOpen)}); + document.getElementById("Tools:ResponsiveUI").doCommand(); + } + + function onUIOpen() { + instance = gBrowser.selectedTab.__responsiveUI; + instance.stack.setAttribute("notransition", "true"); + ok(instance, "instance of the module is attached to the tab."); + + instance.setSize(110, 500); + ok(content.innerWidth, 110, "initial width is valid"); + + let mql = content.matchMedia("(max-device-width:100px)") + + ok(!mql.matches, "media query doesn't match."); + + mql.addListener(onMediaChange); + instance.setSize(90, 500); + } + + function onMediaChange(mql) { + mql.removeListener(onMediaChange); + ok(mql.matches, "media query matches."); + ok(window.screen.width != content.screen.width, "screen.width is not the size of the screen."); + is(content.screen.width, 90, "screen.width is the width of the page."); + is(content.screen.height, 500, "screen.height is the height of the page."); + + + let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + + mql.addListener(onMediaChange2); + docShell.deviceSizeIsPageSize = false; + } + + function onMediaChange2(mql) { + mql.removeListener(onMediaChange); + ok(!mql.matches, "media query has been re-evaluated."); + ok(window.screen.width == content.screen.width, "screen.width is not the size of the screen."); + instance.stack.removeAttribute("notransition"); + document.getElementById("Tools:ResponsiveUI").doCommand(); + gBrowser.removeCurrentTab(); + finish(); + } +} diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index b7f782de70a6..ad83c81075d6 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -755,6 +755,7 @@ nsDocShell::nsDocShell(): mIsAppTab(false), mUseGlobalHistory(false), mInPrivateBrowsing(false), + mDeviceSizeIsPageSize(false), mFiredUnloadEvent(false), mEODForCurrentDocument(false), mURIResultedInDocument(false), @@ -3920,6 +3921,27 @@ nsDocShell::GetCurrentSHEntry(nsISHEntry** aEntry, bool* aOSHE) return NS_OK; } +NS_IMETHODIMP +nsDocShell::SetDeviceSizeIsPageSize(bool aValue) +{ + if (mDeviceSizeIsPageSize != aValue) { + mDeviceSizeIsPageSize = aValue; + nsRefPtr presContext; + GetPresContext(getter_AddRefs(presContext)); + if (presContext) { + presContext->MediaFeatureValuesChanged(presContext->eAlwaysRebuildStyle); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsDocShell::GetDeviceSizeIsPageSize(bool* aValue) +{ + *aValue = mDeviceSizeIsPageSize; + return NS_OK; +} + void nsDocShell::ClearFrameHistory(nsISHEntry* aEntry) { diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h index 56e292cce7d5..4715826c592a 100644 --- a/docshell/base/nsDocShell.h +++ b/docshell/base/nsDocShell.h @@ -815,6 +815,7 @@ protected: bool mIsAppTab; bool mUseGlobalHistory; bool mInPrivateBrowsing; + bool mDeviceSizeIsPageSize; // This boolean is set to true right before we fire pagehide and generally // unset when we embed a new content viewer. While it's true no navigation diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl index 919f9174433f..eac85028b032 100644 --- a/docshell/base/nsIDocShell.idl +++ b/docshell/base/nsIDocShell.idl @@ -43,7 +43,7 @@ interface nsIReflowObserver; typedef unsigned long nsLoadFlags; -[scriptable, builtinclass, uuid(1470A132-99B2-44C3-B37D-D8093B2E29BF)] +[scriptable, builtinclass, uuid(77aca3ee-7417-4cd2-994e-9bd95ca1d98a)] interface nsIDocShell : nsIDocShellTreeItem { /** @@ -907,4 +907,12 @@ interface nsIDocShell : nsIDocShellTreeItem */ [noscript, notxpcom] bool IsInvisible(); [noscript, notxpcom] void SetInvisible(in bool aIsInvisibleDochsell); + + /** + * If deviceSizeIsPageSize is set to true, device-width/height media queries + * will be calculated from the page size, not the device size. + * + * Used by the Responsive Design View. + */ + [infallible] attribute boolean deviceSizeIsPageSize; }; diff --git a/dom/base/nsScreen.cpp b/dom/base/nsScreen.cpp index 7d97296851ba..66398982bb12 100644 --- a/dom/base/nsScreen.cpp +++ b/dom/base/nsScreen.cpp @@ -390,6 +390,19 @@ nsScreen::SlowMozUnlockOrientation() return NS_OK; } +bool +nsScreen::IsDeviceSizePageSize() +{ + nsPIDOMWindow* owner = GetOwner(); + if (owner) { + nsIDocShell* docShell = owner->GetDocShell(); + if (docShell) { + return docShell->GetDeviceSizeIsPageSize(); + } + } + return false; +} + /* virtual */ JSObject* nsScreen::WrapObject(JSContext* aCx, JS::Handle aScope) diff --git a/dom/base/nsScreen.h b/dom/base/nsScreen.h index 47f19c913e66..ebdabf18724b 100644 --- a/dom/base/nsScreen.h +++ b/dom/base/nsScreen.h @@ -51,6 +51,15 @@ public: int32_t GetWidth(ErrorResult& aRv) { nsRect rect; + if (IsDeviceSizePageSize()) { + nsCOMPtr owner = GetOwner(); + if (owner) { + int32_t innerWidth = 0; + aRv = owner->GetInnerWidth(&innerWidth); + return innerWidth; + } + } + aRv = GetRect(rect); return rect.width; } @@ -58,6 +67,15 @@ public: int32_t GetHeight(ErrorResult& aRv) { nsRect rect; + if (IsDeviceSizePageSize()) { + nsCOMPtr owner = GetOwner(); + if (owner) { + int32_t innerHeight = 0; + aRv = owner->GetInnerHeight(&innerHeight); + return innerHeight; + } + } + aRv = GetRect(rect); return rect.height; } @@ -137,6 +155,8 @@ private: LockPermission GetLockOrientationPermission() const; + bool IsDeviceSizePageSize(); + nsRefPtr mEventListener; }; diff --git a/layout/base/nsPresContext.cpp b/layout/base/nsPresContext.cpp index dafcdc8ebe69..725ed546fa27 100644 --- a/layout/base/nsPresContext.cpp +++ b/layout/base/nsPresContext.cpp @@ -2688,6 +2688,17 @@ nsPresContext::AppUnitsToGfxUnits(nscoord aAppUnits) const return mDeviceContext->AppUnitsToGfxUnits(aAppUnits); } +bool +nsPresContext::IsDeviceSizePageSize() +{ + bool isDeviceSizePageSize = false; + nsCOMPtr docShell(do_QueryReferent(mContainer)); + if (docShell) { + isDeviceSizePageSize = docShell->GetDeviceSizeIsPageSize(); + } + return isDeviceSizePageSize; +} + nsRootPresContext::nsRootPresContext(nsIDocument* aDocument, nsPresContextType aType) : nsPresContext(aDocument, aType), diff --git a/layout/base/nsPresContext.h b/layout/base/nsPresContext.h index 5d4de289a2b1..0fc93bf70041 100644 --- a/layout/base/nsPresContext.h +++ b/layout/base/nsPresContext.h @@ -1000,6 +1000,8 @@ public: mExistThrottledUpdates = aExistThrottledUpdates; } + bool IsDeviceSizePageSize(); + protected: friend class nsRunnableMethod; NS_HIDDEN_(void) ThemeChangedInternal(); diff --git a/layout/style/nsMediaFeatures.cpp b/layout/style/nsMediaFeatures.cpp index e4765dc5eb09..af6942356d2f 100644 --- a/layout/style/nsMediaFeatures.cpp +++ b/layout/style/nsMediaFeatures.cpp @@ -113,14 +113,18 @@ static nsSize GetDeviceSize(nsPresContext* aPresContext) { nsSize size; - if (aPresContext->IsRootPaginatedDocument()) + + if (aPresContext->IsDeviceSizePageSize()) { + size = GetSize(aPresContext); + } else if (aPresContext->IsRootPaginatedDocument()) { // We want the page size, including unprintable areas and margins. // XXX The spec actually says we want the "page sheet size", but // how is that different? size = aPresContext->GetPageSize(); - else + } else { GetDeviceContextFor(aPresContext)-> GetDeviceSurfaceDimensions(size.width, size.height); + } return size; } From b50be088a01beed27611b26eb4e1bafcbfbce0c6 Mon Sep 17 00:00:00 2001 From: Paul Rouget Date: Thu, 7 Nov 2013 14:40:23 +0100 Subject: [PATCH 06/41] Bug 890195 - allow deviceSizeIsPageSize default value to be set via a preference. r=bz --- browser/devtools/responsivedesign/responsivedesign.jsm | 3 ++- docshell/base/nsDocShell.cpp | 4 ++++ docshell/base/nsIDocShell.idl | 6 +++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/browser/devtools/responsivedesign/responsivedesign.jsm b/browser/devtools/responsivedesign/responsivedesign.jsm index 4a54f003a327..cceda150dd9a 100644 --- a/browser/devtools/responsivedesign/responsivedesign.jsm +++ b/browser/devtools/responsivedesign/responsivedesign.jsm @@ -185,6 +185,7 @@ function ResponsiveUI(aWindow, aTab) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShell); + this._deviceSizeWasPageSize = this.docShell.deviceSizeIsPageSize; this.docShell.deviceSizeIsPageSize = true; try { @@ -255,7 +256,7 @@ ResponsiveUI.prototype = { return; this.closing = true; - this.docShell.deviceSizeIsPageSize = false; + this.docShell.deviceSizeIsPageSize = this._deviceSizeWasPageSize; this.browser.removeEventListener("load", this.bound_onPageLoad, true); this.browser.removeEventListener("unload", this.bound_onPageUnload, true); diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp index ad83c81075d6..05b5bbed5f84 100644 --- a/docshell/base/nsDocShell.cpp +++ b/docshell/base/nsDocShell.cpp @@ -4951,6 +4951,10 @@ nsDocShell::Create() gAddedPreferencesVarCache = true; } + mDeviceSizeIsPageSize = + Preferences::GetBool("docshell.device_size_is_page_size", + mDeviceSizeIsPageSize); + nsCOMPtr serv = services::GetObserverService(); if (serv) { const char* msg = mItemType == typeContent ? diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl index eac85028b032..c260f65cd6ec 100644 --- a/docshell/base/nsIDocShell.idl +++ b/docshell/base/nsIDocShell.idl @@ -912,7 +912,11 @@ interface nsIDocShell : nsIDocShellTreeItem * If deviceSizeIsPageSize is set to true, device-width/height media queries * will be calculated from the page size, not the device size. * - * Used by the Responsive Design View. + * Used by the Responsive Design View and B2G Simulator. + * + * Default is False. + * Default value can be overriden with + * docshell.device_size_is_page_size pref. */ [infallible] attribute boolean deviceSizeIsPageSize; }; From 7846d37db110a634cbac7a04cd49dc77673780e0 Mon Sep 17 00:00:00 2001 From: Raymond Lee Date: Thu, 7 Nov 2013 05:48:12 +0800 Subject: [PATCH 07/41] Bug 725943 - Refactor nsSearchService to generalize use of Lazy. r=MattN --- toolkit/components/search/nsSearchService.js | 84 +++++++---------- .../tests/xpcshell/test_serialize_file.js | 92 +++++++++++++++++++ .../search/tests/xpcshell/xpcshell.ini | 1 + 3 files changed, 125 insertions(+), 52 deletions(-) create mode 100644 toolkit/components/search/tests/xpcshell/test_serialize_file.js diff --git a/toolkit/components/search/nsSearchService.js b/toolkit/components/search/nsSearchService.js index 397aa0850dbb..f18aa6eb7fa2 100644 --- a/toolkit/components/search/nsSearchService.js +++ b/toolkit/components/search/nsSearchService.js @@ -1191,8 +1191,8 @@ Engine.prototype = { _updateURL: null, // The url to check for a new icon _iconUpdateURL: null, - // A reference to the timer used for lazily serializing the engine to file - _serializeTimer: null, + /* Deferred serialization task. */ + _lazySerializeTask: null, /** * Retrieves the data from the engine's file. If the engine's dataType is @@ -2363,28 +2363,15 @@ Engine.prototype = { return doc; }, - _lazySerializeToFile: function SRCH_ENG_serializeToFile() { - if (this._serializeTimer) { - // Reset the timer - this._serializeTimer.delay = LAZY_SERIALIZE_DELAY; - } else { - this._serializeTimer = Cc["@mozilla.org/timer;1"]. - createInstance(Ci.nsITimer); - var timerCallback = { - self: this, - notify: function SRCH_ENG_notify(aTimer) { - try { - this.self._serializeToFile(); - } catch (ex) { - LOG("Serialization from timer callback failed:\n" + ex); - } - this.self._serializeTimer = null; - } - }; - this._serializeTimer.initWithCallback(timerCallback, - LAZY_SERIALIZE_DELAY, - Ci.nsITimer.TYPE_ONE_SHOT); + get lazySerializeTask() { + if (!this._lazySerializeTask) { + let task = function taskCallback() { + this._serializeToFile(); + }.bind(this); + this._lazySerializeTask = new DeferredTask(task, LAZY_SERIALIZE_DELAY); } + + return this._lazySerializeTask; }, /** @@ -2415,6 +2402,9 @@ Engine.prototype = { } closeSafeOutputStream(fos); + + Services.obs.notifyObservers(file.clone(), SEARCH_SERVICE_TOPIC, + "write-engine-to-disk-complete"); }, /** @@ -2660,7 +2650,7 @@ Engine.prototype = { url.addParam(aName, aValue); // Serialize the changes to file lazily - this._lazySerializeToFile(); + this.lazySerializeTask.start(); }, #ifdef ANDROID @@ -3236,25 +3226,16 @@ SearchService.prototype = { }); }, - _batchTimer: null, - _batchCacheInvalidation: function SRCH_SVC__batchCacheInvalidation() { - let callback = { - self: this, - notify: function SRCH_SVC_batchTimerNotify(aTimer) { - LOG("_batchCacheInvalidation: Invalidating engine cache"); - this.self._buildCache(); - this.self._batchTimer = null; - } - }; - - if (!this._batchTimer) { - this._batchTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this._batchTimer.initWithCallback(callback, CACHE_INVALIDATION_DELAY, - Ci.nsITimer.TYPE_ONE_SHOT); - } else { - this._batchTimer.delay = CACHE_INVALIDATION_DELAY; - LOG("_batchCacheInvalidation: Batch timer reset"); + _batchTask: null, + get batchTask() { + if (!this._batchTask) { + let task = function taskCallback() { + LOG("batchTask: Invalidating engine cache"); + this._buildCache(); + }.bind(this); + this._batchTask = new DeferredTask(task, CACHE_INVALIDATION_DELAY); } + return this._batchTask; }, _addEngineToStore: function SRCH_SVC_addEngineToStore(aEngine) { @@ -3885,7 +3866,7 @@ SearchService.prototype = { engine._initFromMetadata(aName, aIconURL, aAlias, aDescription, aMethod, aTemplate); this._addEngineToStore(engine); - this._batchCacheInvalidation(); + this.batchTask.start(); }, addEngine: function SRCH_SVC_addEngine(aEngineURL, aDataType, aIconURL, @@ -3948,10 +3929,10 @@ SearchService.prototype = { engineToRemove.hidden = true; engineToRemove.alias = null; } else { - // Cancel the lazy serialization timer if it's running - if (engineToRemove._serializeTimer) { - engineToRemove._serializeTimer.cancel(); - engineToRemove._serializeTimer = null; + // Cancel the serialized task if it's running + if (engineToRemove._lazySerializeTask) { + engineToRemove._lazySerializeTask.cancel(); + engineToRemove._lazySerializeTask = null; } // Remove the engine file from disk (this might throw) @@ -4149,21 +4130,20 @@ SearchService.prototype = { LOG("nsSearchService::observe: setting current"); this.currentEngine = aEngine; } - this._batchCacheInvalidation(); + this.batchTask.start(); break; case SEARCH_ENGINE_CHANGED: case SEARCH_ENGINE_REMOVED: - this._batchCacheInvalidation(); + this.batchTask.start(); break; } break; case QUIT_APPLICATION_TOPIC: this._removeObservers(); - if (this._batchTimer) { + if (this._batchTask) { // Flush to disk immediately - this._batchTimer.cancel(); - this._buildCache(); + this._batchTask.flush(); } engineMetadataService.flush(); break; diff --git a/toolkit/components/search/tests/xpcshell/test_serialize_file.js b/toolkit/components/search/tests/xpcshell/test_serialize_file.js new file mode 100644 index 000000000000..84cf289be1ad --- /dev/null +++ b/toolkit/components/search/tests/xpcshell/test_serialize_file.js @@ -0,0 +1,92 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * test_serialize_file: Add test engines + * + * Ensure that : + * - File is created. + * - File size is bigger than 0. + * - lazySerializeTask updates the file. + */ + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://testing-common/httpd.js"); +Cu.import("resource://gre/modules/NetUtil.jsm"); + +add_test(function test_batchTask() { + let observer = function(aSubject, aTopic, aData) { + if (aTopic == "browser-search-engine-modified" && aData == "engine-loaded") { + let engine1 = Services.search.getEngineByName("Test search engine"); + let engine2 = Services.search.getEngineByName("Sherlock test search engine"); + if (engine1 && engine2) { + Services.obs.removeObserver(observer, aTopic); + // Test that files are written correctly. + let engineFile1 = engine1.wrappedJSObject._file; + let engineFile2 = engine2.wrappedJSObject._file; + do_check_true(engineFile1.exists()); + do_check_true(engineFile2.exists()); + do_check_neq(engineFile1.fileSize, 0); + do_check_neq(engineFile2.fileSize, 0); + run_next_test(); + } + } + } + + Services.obs.addObserver(observer, "browser-search-engine-modified", false); + Services.search.addEngine("http://localhost:4444/data/engine.xml", + Ci.nsISearchEngine.DATA_XML, null, false); + Services.search.addEngine("http://localhost:4444/data/engine.src", + Ci.nsISearchEngine.DATA_TEXT, + "http://localhost:4444/data/ico-size-16x16-png.ico", + false); +}); + +add_test(function test_addParam() { + let engine = Services.search.getEngineByName("Test search engine"); + engine.addParam("param-name", "param-value", null); + + function readAsyncFile(aFile, aCallback) { + NetUtil.asyncFetch(aFile, function(inputStream, status) { + do_check_true(Components.isSuccessCode(status)); + + let data = NetUtil.readInputStreamToString(inputStream, inputStream.available()); + aCallback(data); + }); + } + + let observer = function(aSubject, aTopic, aData) { + // The sherlock engine file may still be updated because the icon takes + // time be loaded, therefore, the engine name is checked here. + aSubject.QueryInterface(Ci.nsIFile); + if (aTopic == "browser-search-service" && + aData == "write-engine-to-disk-complete" && + aSubject.leafName == "test-search-engine.xml") { + Services.obs.removeObserver(observer, aTopic); + + let engineFile = engine.wrappedJSObject._file; + + readAsyncFile(engineFile, function(engineData) { + do_check_true(engineData.indexOf("param-name") > 0); + run_next_test(); + }); + } + } + Services.obs.addObserver(observer, "browser-search-service", false); +}); + +function run_test() { + updateAppInfo(); + + let httpServer = new HttpServer(); + httpServer.start(4444); + httpServer.registerDirectory("/", do_get_cwd()); + + do_register_cleanup(function cleanup() { + httpServer.stop(function() {}); + }); + + run_next_test(); +} diff --git a/toolkit/components/search/tests/xpcshell/xpcshell.ini b/toolkit/components/search/tests/xpcshell/xpcshell.ini index 8540282ca75d..23562f09bb57 100644 --- a/toolkit/components/search/tests/xpcshell/xpcshell.ini +++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini @@ -29,6 +29,7 @@ support-files = [test_notifications.js] [test_addEngine_callback.js] [test_multipleIcons.js] +[test_serialize_file.js] [test_async.js] [test_sync.js] [test_sync_fallback.js] From 79d2b45bb68dc9f8f446a161fb78fdd6c5d4984b Mon Sep 17 00:00:00 2001 From: Paul Rouget Date: Thu, 7 Nov 2013 09:14:10 -0500 Subject: [PATCH 08/41] Bug 912891 - [app manager] Implement a CUSTOM host. r=harth --- browser/devtools/framework/gDevTools.jsm | 6 +- browser/devtools/framework/test/browser.ini | 1 + .../test/browser_toolbox_custom_host.js | 60 ++++++++++++++++++ browser/devtools/framework/toolbox-hosts.js | 63 +++++++++++++++++-- browser/devtools/framework/toolbox.js | 46 ++++++++++---- browser/devtools/shared/DOMHelpers.jsm | 32 ++++++++++ 6 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 browser/devtools/framework/test/browser_toolbox_custom_host.js diff --git a/browser/devtools/framework/gDevTools.jsm b/browser/devtools/framework/gDevTools.jsm index 76afcae28d12..1948bd3b8471 100644 --- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -206,11 +206,13 @@ DevTools.prototype = { * The id of the tool to show * @param {Toolbox.HostType} hostType * The type of host (bottom, window, side) + * @param {object} hostOptions + * Options for host specifically * * @return {Toolbox} toolbox * The toolbox that was opened */ - showToolbox: function(target, toolId, hostType) { + showToolbox: function(target, toolId, hostType, hostOptions) { let deferred = promise.defer(); let toolbox = this._toolboxes.get(target); @@ -233,7 +235,7 @@ DevTools.prototype = { } else { // No toolbox for target, create one - toolbox = new devtools.Toolbox(target, toolId, hostType); + toolbox = new devtools.Toolbox(target, toolId, hostType, hostOptions); this._toolboxes.set(target, toolbox); diff --git a/browser/devtools/framework/test/browser.ini b/browser/devtools/framework/test/browser.ini index bf250c3df039..33dadc3073b9 100644 --- a/browser/devtools/framework/test/browser.ini +++ b/browser/devtools/framework/test/browser.ini @@ -22,3 +22,4 @@ support-files = head.js [browser_toolbox_window_shortcuts.js] [browser_toolbox_window_title_changes.js] [browser_toolbox_zoom.js] +[browser_toolbox_custom_host.js] diff --git a/browser/devtools/framework/test/browser_toolbox_custom_host.js b/browser/devtools/framework/test/browser_toolbox_custom_host.js new file mode 100644 index 000000000000..d8c7b27b9d4d --- /dev/null +++ b/browser/devtools/framework/test/browser_toolbox_custom_host.js @@ -0,0 +1,60 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + Cu.import("resource://gre/modules/Services.jsm"); + let temp = {} + Cu.import("resource:///modules/devtools/gDevTools.jsm", temp); + let DevTools = temp.DevTools; + Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm", temp); + let LayoutHelpers = temp.LayoutHelpers; + + Cu.import("resource://gre/modules/devtools/Loader.jsm", temp); + let devtools = temp.devtools; + + let Toolbox = devtools.Toolbox; + + let toolbox, iframe, target, tab; + + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + target = TargetFactory.forTab(gBrowser.selectedTab); + + window.addEventListener("message", onMessage); + + iframe = document.createElement("iframe"); + document.documentElement.appendChild(iframe); + + gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) { + gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true); + let options = {customIframe: iframe}; + gDevTools.showToolbox(target, null, Toolbox.HostType.CUSTOM, options) + .then(testCustomHost, console.error) + .then(null, console.error); + }, true); + + content.location = "data:text/html,test custom host"; + + function onMessage(event) { + info("onMessage: " + event.data); + let json = JSON.parse(event.data); + if (json.name == "toolbox-close") { + ok("Got the `toolbox-close` message"); + cleanup(); + } + } + + function testCustomHost(toolbox) { + is(toolbox.doc.defaultView.top, window, "Toolbox is included in browser.xul"); + is(toolbox.doc, iframe.contentDocument, "Toolbox is in the custom iframe"); + executeSoon(() => gBrowser.removeCurrentTab()); + } + + function cleanup() { + window.removeEventListener("message", onMessage); + iframe.remove(); + finish(); + } +} diff --git a/browser/devtools/framework/toolbox-hosts.js b/browser/devtools/framework/toolbox-hosts.js index f4ed648b7733..e27bcc9a6a2a 100644 --- a/browser/devtools/framework/toolbox-hosts.js +++ b/browser/devtools/framework/toolbox-hosts.js @@ -10,6 +10,7 @@ let promise = require("sdk/core/promise"); let EventEmitter = require("devtools/shared/event-emitter"); Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource:///modules/devtools/DOMHelpers.jsm"); /** * A toolbox host represents an object that contains a toolbox (e.g. the @@ -23,7 +24,8 @@ Cu.import("resource://gre/modules/Services.jsm"); exports.Hosts = { "bottom": BottomHost, "side": SidebarHost, - "window": WindowHost + "window": WindowHost, + "custom": CustomHost } /** @@ -61,18 +63,18 @@ BottomHost.prototype = { this._nbox.appendChild(this.frame); let frameLoad = function() { - this.frame.removeEventListener("DOMContentLoaded", frameLoad, true); this.emit("ready", this.frame); - deferred.resolve(this.frame); }.bind(this); this.frame.tooltip = "aHTMLTooltip"; - this.frame.addEventListener("DOMContentLoaded", frameLoad, true); // we have to load something so we can switch documents if we have to this.frame.setAttribute("src", "about:blank"); + let domHelper = new DOMHelpers(this.frame.contentWindow); + domHelper.onceDOMReady(frameLoad); + focusTab(this.hostTab); return deferred.promise; @@ -272,6 +274,59 @@ WindowHost.prototype = { } } +/** + * Host object for the toolbox in its own tab + */ +function CustomHost(hostTab, options) { + this.frame = options.customIframe; + this.uid = options.uid; + EventEmitter.decorate(this); +} + +CustomHost.prototype = { + type: "custom", + + _sendMessageToTopWindow: function CH__sendMessageToTopWindow(msg) { + // It's up to the custom frame owner (parent window) to honor + // "close" or "raise" instructions. + let topWindow = this.frame.ownerDocument.defaultView; + let json = {name:"toolbox-" + msg, uid: this.uid} + topWindow.postMessage(JSON.stringify(json), "*"); + }, + + /** + * Create a new xul window to contain the toolbox. + */ + create: function CH_create() { + return promise.resolve(this.frame); + }, + + /** + * Raise the host. + */ + raise: function CH_raise() { + this._sendMessageToTopWindow("raise"); + }, + + /** + * Set the toolbox title. + */ + setTitle: function CH_setTitle(title) { + // Not supported + }, + + /** + * Destroy the window. + */ + destroy: function WH_destroy() { + if (!this._destroyed) { + this._destroyed = true; + this._sendMessageToTopWindow("close"); + } + return promise.resolve(null); + } +} + /** * Switch to the given tab in a browser and focus the browser window */ diff --git a/browser/devtools/framework/toolbox.js b/browser/devtools/framework/toolbox.js index f226ea5da61d..095206b2606f 100644 --- a/browser/devtools/framework/toolbox.js +++ b/browser/devtools/framework/toolbox.js @@ -19,6 +19,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource:///modules/devtools/gDevTools.jsm"); Cu.import("resource:///modules/devtools/scratchpad-manager.jsm"); +Cu.import("resource:///modules/devtools/DOMHelpers.jsm"); loader.lazyGetter(this, "Hosts", () => require("devtools/framework/toolbox-hosts").Hosts); @@ -55,8 +56,10 @@ loader.lazyGetter(this, "Requisition", () => { * Tool to select initially * @param {Toolbox.HostType} hostType * Type of host that will host the toolbox (e.g. sidebar, window) + * @param {object} hostOptions + * Options for host specifically */ -function Toolbox(target, selectedTool, hostType) { +function Toolbox(target, selectedTool, hostType, hostOptions) { this._target = target; this._toolPanels = new Map(); this._telemetry = new Telemetry(); @@ -79,7 +82,7 @@ function Toolbox(target, selectedTool, hostType) { } this._defaultToolId = selectedTool; - this._host = this._createHost(hostType); + this._host = this._createHost(hostType, hostOptions); EventEmitter.decorate(this); @@ -99,7 +102,8 @@ exports.Toolbox = Toolbox; Toolbox.HostType = { BOTTOM: "bottom", SIDE: "side", - WINDOW: "window" + WINDOW: "window", + CUSTOM: "custom" }; Toolbox.prototype = { @@ -187,8 +191,6 @@ Toolbox.prototype = { let deferred = promise.defer(); let domReady = () => { - iframe.removeEventListener("DOMContentLoaded", domReady, true); - this.isReady = true; let closeButton = this.doc.getElementById("toolbox-close"); @@ -211,9 +213,11 @@ Toolbox.prototype = { }); }; - iframe.addEventListener("DOMContentLoaded", domReady, true); iframe.setAttribute("src", this._URL); + let domHelper = new DOMHelpers(iframe.contentWindow); + domHelper.onceDOMReady(domReady); + return deferred.promise; }); }, @@ -387,6 +391,7 @@ Toolbox.prototype = { for (let type in Toolbox.HostType) { let position = Toolbox.HostType[type]; if (position == this.hostType || + position == Toolbox.HostType.CUSTOM || (!sideEnabled && position == Toolbox.HostType.SIDE)) { continue; } @@ -555,8 +560,6 @@ Toolbox.prototype = { vbox.appendChild(iframe); let onLoad = () => { - iframe.removeEventListener("DOMContentLoaded", onLoad, true); - let built = definition.build(iframe.contentWindow, this); promise.resolve(built).then((panel) => { this._toolPanels.set(id, panel); @@ -566,8 +569,25 @@ Toolbox.prototype = { }); }; - iframe.addEventListener("DOMContentLoaded", onLoad, true); iframe.setAttribute("src", definition.url); + + // Depending on the host, iframe.contentWindow is not always + // defined at this moment. If it is not defined, we use an + // event listener on the iframe DOM node. If it's defined, + // we use the chromeEventHandler. We can't use a listener + // on the DOM node every time because this won't work + // if the (xul chrome) iframe is loaded in a content docshell. + if (iframe.contentWindow) { + let domHelper = new DOMHelpers(iframe.contentWindow); + domHelper.onceDOMReady(onLoad); + } else { + let callback = () => { + iframe.removeEventListener("DOMContentLoaded", callback); + onLoad(); + } + iframe.addEventListener("DOMContentLoaded", callback); + } + return deferred.promise; }, @@ -732,13 +752,13 @@ Toolbox.prototype = { * @return {Host} host * The created host object */ - _createHost: function(hostType) { + _createHost: function(hostType, options) { if (!Hosts[hostType]) { throw new Error("Unknown hostType: " + hostType); } // clean up the toolbox if its window is closed - let newHost = new Hosts[hostType](this.target.tab); + let newHost = new Hosts[hostType](this.target.tab, options); newHost.on("window-closed", this.destroy); return newHost; }, @@ -766,7 +786,9 @@ Toolbox.prototype = { this._host = newHost; - Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type); + if (this.hostType != Toolbox.HostType.CUSTOM) { + Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type); + } this._buildDockButtons(); this._addKeysToWindow(); diff --git a/browser/devtools/shared/DOMHelpers.jsm b/browser/devtools/shared/DOMHelpers.jsm index 754632ff9638..4c2bd522d38d 100644 --- a/browser/devtools/shared/DOMHelpers.jsm +++ b/browser/devtools/shared/DOMHelpers.jsm @@ -2,6 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +const Ci = Components.interfaces; +const Cu = Components.utils; +Cu.import("resource://gre/modules/Services.jsm"); + this.EXPORTED_SYMBOLS = ["DOMHelpers"]; /** @@ -13,6 +17,9 @@ this.EXPORTED_SYMBOLS = ["DOMHelpers"]; * The content window, owning the document to traverse. */ this.DOMHelpers = function DOMHelpers(aWindow) { + if (!aWindow) { + throw new Error("window can't be null or undefined"); + } this.window = aWindow; }; @@ -120,5 +127,30 @@ DOMHelpers.prototype = { { delete this.window; delete this.treeWalker; + }, + + /** + * A simple way to be notified (once) when a window becomes + * interactive (DOMContentLoaded). + * + * It is based on the chromeEventHandler. This is useful when + * chrome iframes are loaded in content docshells (in Firefox + * tabs for example). + */ + onceDOMReady: function Helpers_onLocationChange(callback) { + let window = this.window; + let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell); + let onReady = function(event) { + if (event.target == window.document) { + docShell.chromeEventHandler.removeEventListener("DOMContentLoaded", onReady, false); + // If in `callback` the URL of the window is changed and a listener to DOMContentLoaded + // is attached, the event we just received will be also be caught by the new listener. + // We want to avoid that so we execute the callback in the next queue. + Services.tm.mainThread.dispatch(callback, 0); + } + } + docShell.chromeEventHandler.addEventListener("DOMContentLoaded", onReady, false); } }; From 0dcfd9bc8cc2bd65972bbebfa550f93508205c16 Mon Sep 17 00:00:00 2001 From: Paul Rouget Date: Thu, 7 Nov 2013 09:14:22 -0500 Subject: [PATCH 09/41] Bug 912891 - [app manager] Open the toolbox in the app manager. r=ochameau --- browser/app/profile/firefox.js | 2 +- .../devtools/app-manager/content/device.js | 11 +- browser/devtools/app-manager/content/index.js | 220 +++++++++++++----- .../devtools/app-manager/content/index.xul | 10 +- .../devtools/app-manager/content/projects.js | 18 +- .../app-manager/connection-footer.css | 1 + .../shared/devtools/app-manager/index.css | 10 + 7 files changed, 185 insertions(+), 87 deletions(-) diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index acfd3b339284..527542ba46cc 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1078,7 +1078,7 @@ pref("devtools.commands.dir", ""); // Enable the app manager pref("devtools.appmanager.enabled", true); -pref("devtools.appmanager.firstrun", true); +pref("devtools.appmanager.lastTab", "help"); pref("devtools.appmanager.manifestEditor.enabled", false); // Toolbox preferences diff --git a/browser/devtools/app-manager/content/device.js b/browser/devtools/app-manager/content/device.js index fb50073c7592..84f916c53405 100644 --- a/browser/devtools/app-manager/content/device.js +++ b/browser/devtools/app-manager/content/device.js @@ -162,16 +162,13 @@ let UI = { if (!this.connected) { return; } + + let app = this.store.object.apps.all.filter(a => a.manifestURL == manifest)[0]; getTargetForApp(this.connection.client, this.listTabsResponse.webappsActor, manifest).then((target) => { - gDevTools.showToolbox(target, - null, - devtools.Toolbox.HostType.WINDOW).then(toolbox => { - this.connection.once(Connection.Events.DISCONNECTED, () => { - toolbox.destroy(); - }); - }); + + top.UI.openAndShowToolboxForTarget(target, app.name, app.iconURL); }, console.error); }, diff --git a/browser/devtools/app-manager/content/index.js b/browser/devtools/app-manager/content/index.js index 8f21eeaf226d..aa0ee4d654dd 100644 --- a/browser/devtools/app-manager/content/index.js +++ b/browser/devtools/app-manager/content/index.js @@ -7,76 +7,176 @@ Cu.import("resource:///modules/devtools/gDevTools.jsm"); const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); const {require} = devtools; const {ConnectionManager, Connection} = require("devtools/client/connection-manager"); +const promise = require("sdk/core/promise"); const prefs = require('sdk/preferences/service'); -let connection; -window.addEventListener("message", function(event) { - try { - let json = JSON.parse(event.data); - switch (json.name) { - case "connection": - let cid = +json.cid; - for (let c of ConnectionManager.connections) { - if (c.uid == cid) { - connection = c; - onNewConnection(); - break; - } - } - break; - case "closeHelp": - selectTab("projects"); - break; - default: - Cu.reportError("Unknown message: " + json.name); +let UI = { + _toolboxTabCursor: 0, + _handledTargets: new Map(), + + connection: null, + + init: function() { + this.onLoad = this.onLoad.bind(this); + this.onUnload = this.onUnload.bind(this); + this.onMessage = this.onMessage.bind(this); + this.onConnected = this.onConnected.bind(this); + this.onDisconnected = this.onDisconnected.bind(this); + + window.addEventListener("load", this.onLoad); + window.addEventListener("unload", this.onUnload); + window.addEventListener("message", this.onMessage); + }, + + onLoad: function() { + window.removeEventListener("load", this.onLoad); + let defaultPanel = prefs.get("devtools.appmanager.lastTab"); + let panelExists = !!document.querySelector("." + defaultPanel + "-panel"); + this.selectTab(panelExists ? defaultPanel : "projects"); + }, + + onUnload: function() { + window.removeEventListener("unload", this.onUnload); + window.removeEventListener("message", this.onMessage); + if (this.connection) { + this.connection.off(Connection.Status.CONNECTED, this.onConnected); + this.connection.off(Connection.Status.DISCONNECTED, this.onDisconnected); } - } catch(e) { Cu.reportError(e); } + }, - // Forward message - let panels = document.querySelectorAll(".panel"); - for (let frame of panels) { - frame.contentWindow.postMessage(event.data, "*"); - } -}, false); + onMessage: function(event) { + try { + let json = JSON.parse(event.data); + switch (json.name) { + case "connection": + let cid = +json.cid; + for (let c of ConnectionManager.connections) { + if (c.uid == cid) { + this.onNewConnection(c); + break; + } + } + break; + case "closeHelp": + this.selectTab("projects"); + break; + case "toolbox-raise": + this.selectTab(json.uid); + break; + case "toolbox-close": + this.closeToolboxTab(json.uid); + break; + default: + Cu.reportError("Unknown message: " + json.name); + } + } catch(e) { Cu.reportError(e); } -window.addEventListener("unload", function onUnload() { - window.removeEventListener("unload", onUnload); - if (connection) { - connection.off(Connection.Status.CONNECTED, onConnected); - connection.off(Connection.Status.DISCONNECTED, onDisconnected); - } -}); + // Forward message + let panels = document.querySelectorAll(".panel"); + for (let frame of panels) { + frame.contentWindow.postMessage(event.data, "*"); + } + }, -function onNewConnection() { - connection.on(Connection.Status.CONNECTED, onConnected); - connection.on(Connection.Status.DISCONNECTED, onDisconnected); -} + selectTabFromButton: function(button) { + if (!button.hasAttribute("panel")) + return; + this.selectTab(button.getAttribute("panel")); + }, -function onConnected() { - document.querySelector("#content").classList.add("connected"); -} + selectTab: function(panel) { + let isToolboxTab = false; + for (let type of ["button", "panel"]) { + let oldSelection = document.querySelector("." + type + "[selected]"); + let newSelection = document.querySelector("." + panel + "-" + type); + if (oldSelection) oldSelection.removeAttribute("selected"); + if (newSelection) { + newSelection.scrollIntoView(false); + newSelection.setAttribute("selected", "true"); + if (newSelection.classList.contains("toolbox")) { + isToolboxTab = true; + } + } + } + if (!isToolboxTab) { + prefs.set("devtools.appmanager.lastTab", panel); + } + }, -function onDisconnected() { - document.querySelector("#content").classList.remove("connected"); -} + onNewConnection: function(connection) { + this.connection = connection; + this.connection.on(Connection.Status.CONNECTED, this.onConnected); + this.connection.on(Connection.Status.DISCONNECTED, this.onDisconnected); + }, -function selectTab(id) { - for (let type of ["button", "panel"]) { - let oldSelection = document.querySelector("." + type + "[selected]"); - let newSelection = document.querySelector("." + id + "-" + type); - if (oldSelection) oldSelection.removeAttribute("selected"); - if (newSelection) newSelection.setAttribute("selected", "true"); - } - if (id != "help") { - // Might be the first time the user is accessing the actual app manager - prefs.set("devtools.appmanager.firstrun", false); + onConnected: function() { + document.querySelector("#content").classList.add("connected"); + }, + + onDisconnected: function() { + for (let [,toolbox] of this._handledTargets) { + if (toolbox) { + toolbox.destroy(); + } + } + this._handledTargets.clear(); + document.querySelector("#content").classList.remove("connected"); + }, + + createToolboxTab: function(name, iconURL, uid) { + let button = document.createElement("button"); + button.className = "button toolbox " + uid + "-button"; + button.setAttribute("panel", uid); + button.textContent = name; + button.setAttribute("style", "background-image: url(" + iconURL + ")"); + let toolboxTabs = document.querySelector("#toolbox-tabs"); + toolboxTabs.appendChild(button); + let iframe = document.createElement("iframe"); + iframe.setAttribute("flex", "1"); + iframe.className = "panel toolbox " + uid + "-panel"; + let panels = document.querySelector("#tab-panels"); + panels.appendChild(iframe); + this.selectTab(uid); + return iframe; + }, + + closeToolboxTab: function(uid) { + let buttonToDestroy = document.querySelector("." + uid + "-button"); + let panelToDestroy = document.querySelector("." + uid + "-panel"); + + if (buttonToDestroy.hasAttribute("selected")) { + let lastTab = prefs.get("devtools.appmanager.lastTab"); + this.selectTab(lastTab); + } + + buttonToDestroy.remove(); + panelToDestroy.remove(); + }, + + openAndShowToolboxForTarget: function(target, name, icon) { + let host = devtools.Toolbox.HostType.CUSTOM; + if (!this._handledTargets.has(target)) { + let uid = "uid" + this._toolboxTabCursor++; + let iframe = this.createToolboxTab(name, icon, uid); + let options = { customIframe: iframe , uid: uid }; + this._handledTargets.set(target, null); + return gDevTools.showToolbox(target, null, host, options).then(toolbox => { + this._handledTargets.set(target, toolbox); + toolbox.once("destroyed", () => { + this._handledTargets.delete(target) + }); + }); + } else { + let toolbox = this._handledTargets.get(target); + if (!toolbox) { + // Target is handled, but toolbox is still being + // created. + return promise.resolve(null); + } + return gDevTools.showToolbox(target, null, host); + } } } -let firstRun = prefs.get("devtools.appmanager.firstrun"); -if (firstRun) { - selectTab("help"); -} else { - selectTab("projects"); -} +UI.init(); diff --git a/browser/devtools/app-manager/content/index.xul b/browser/devtools/app-manager/content/index.xul index ba27f097af83..82afbeb3c874 100644 --- a/browser/devtools/app-manager/content/index.xul +++ b/browser/devtools/app-manager/content/index.xul @@ -22,11 +22,11 @@ - - - - - + + + + +