From 3c5caf63328bb43262b8b8ced7f19ecf66abc637 Mon Sep 17 00:00:00 2001 From: Gaia Pushbot Date: Tue, 10 Dec 2013 13:00:23 -0800 Subject: [PATCH 01/58] Bumping gaia.json for 4 gaia-central revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/ebc87df16535 Author: Gareth Aye Desc: Merge pull request #14544 from gaye/non-native-marionette Use non-native marionette-js-runner for buildbot ======== https://hg.mozilla.org/integration/gaia-central/rev/50345a17dc8d Author: Gareth Aye Desc: Use non-native marionette-js-runner for buildbot ======== https://hg.mozilla.org/integration/gaia-central/rev/6f750c8ddf5b Author: Zac Desc: Merge pull request #14015 from viorelaioia/bug_942220 Bug 942220 - Add explicit wait in tap_back in contact_details.py ======== https://hg.mozilla.org/integration/gaia-central/rev/a5b09f705e4f Author: Viorela Ioia Desc: Bug 942220 - Add explicit wait in tap_back in contact_details.py --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index aee727b389e2..99da93e90011 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,4 +1,4 @@ { - "revision": "2e4d09abb604dab914f1f29001012d872b57ef9e", + "revision": "ebc87df165350fba64aa288f472cc61f82535bbc", "repo_path": "/integration/gaia-central" } From 9de016148ab2980f43072c516cdecbc3d03a5c2e Mon Sep 17 00:00:00 2001 From: Ben Turner Date: Thu, 21 Nov 2013 16:51:26 -0800 Subject: [PATCH 02/58] Bug 947575 - Fix IPDL unit tests on windows, r=bsmedberg. --HG-- extra : transplant_source : %04B6%5C%3A%ED3E%06o%90%1Bx%60%96b.%0F%27P --- ipc/ipdl/ipdl/lower.py | 4 ++-- ipc/ipdl/test/cxx/PTestActorPunning.ipdl | 2 ++ ipc/ipdl/test/cxx/PTestBridgeSub.ipdl | 1 + ipc/ipdl/test/cxx/TestRPC.cpp | 3 ++- ipc/ipdl/test/cxx/TestUrgency.cpp | 5 +++-- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/ipc/ipdl/ipdl/lower.py b/ipc/ipdl/ipdl/lower.py index 1fc762150e69..972c19834362 100644 --- a/ipc/ipdl/ipdl/lower.py +++ b/ipc/ipdl/ipdl/lower.py @@ -4128,7 +4128,7 @@ class _GenerateProtocolActorCode(ipdl.ast.Visitor): msgvar = self.msgvar tdvar = ExprVar('td') pidvar = ExprVar('pid') - pvar = ExprVar('p') + pvar = ExprVar('protocolid') iffail = StmtIf(ExprNot(ExprCall( ExprVar('mozilla::ipc::UnpackChannelOpened'), args=[ _backstagePass(), @@ -4180,7 +4180,7 @@ class _GenerateProtocolActorCode(ipdl.ast.Visitor): StmtBreak() ]) label = _messageStartName(actor.ptype) - if actor.side is 'child': + if actor.side == 'child': label += 'Child' return CaseLabel(label), case diff --git a/ipc/ipdl/test/cxx/PTestActorPunning.ipdl b/ipc/ipdl/test/cxx/PTestActorPunning.ipdl index 6b47ec31e31e..2c09f301297c 100644 --- a/ipc/ipdl/test/cxx/PTestActorPunning.ipdl +++ b/ipc/ipdl/test/cxx/PTestActorPunning.ipdl @@ -3,6 +3,8 @@ include protocol PTestActorPunningPunned; include protocol PTestActorPunningSub; include "mozilla/_ipdltest/IPDLUnitTestUtils.h"; +using struct mozilla::_ipdltest::Bad from "mozilla/_ipdltest/IPDLUnitTestUtils.h"; + namespace mozilla { namespace _ipdltest { diff --git a/ipc/ipdl/test/cxx/PTestBridgeSub.ipdl b/ipc/ipdl/test/cxx/PTestBridgeSub.ipdl index 1216fa1c7c60..fc2d4f4f002d 100644 --- a/ipc/ipdl/test/cxx/PTestBridgeSub.ipdl +++ b/ipc/ipdl/test/cxx/PTestBridgeSub.ipdl @@ -1,3 +1,4 @@ +include protocol PTestBridgeMainSub; namespace mozilla { namespace _ipdltest { diff --git a/ipc/ipdl/test/cxx/TestRPC.cpp b/ipc/ipdl/test/cxx/TestRPC.cpp index 5194ad370b55..b0565d513b84 100644 --- a/ipc/ipdl/test/cxx/TestRPC.cpp +++ b/ipc/ipdl/test/cxx/TestRPC.cpp @@ -1,8 +1,9 @@ #include "TestRPC.h" #include "IPDLUnitTests.h" // fail etc. +#if defined(OS_POSIX) #include -#if !defined(OS_POSIX) +#else #include #endif diff --git a/ipc/ipdl/test/cxx/TestUrgency.cpp b/ipc/ipdl/test/cxx/TestUrgency.cpp index 9d92bc5dc2c6..fde5422bb66b 100644 --- a/ipc/ipdl/test/cxx/TestUrgency.cpp +++ b/ipc/ipdl/test/cxx/TestUrgency.cpp @@ -1,8 +1,9 @@ #include "TestUrgency.h" #include "IPDLUnitTests.h" // fail etc. +#if defined(OS_POSIX) #include -#if !defined(OS_POSIX) +#else #include #endif @@ -166,7 +167,7 @@ TestUrgencyChild::AnswerReply2(uint32_t *reply) bool TestUrgencyChild::AnswerFinalTest_Hang() { - sleep(10); + Sleep(10); return true; } From 436bc9d8e68caae290ae46e753a3d48c6ce07696 Mon Sep 17 00:00:00 2001 From: Ben Turner Date: Wed, 23 Oct 2013 01:28:24 -0700 Subject: [PATCH 03/58] Bug 939182 - Clean up message loop code, r=bsmedberg. --HG-- extra : transplant_source : -v1T%0A%B7%FC%B3q%CE//%D7%EB%3C%2C1%B4%F8%F1 --- ipc/glue/MessagePump.cpp | 131 +++++++++++++++++++++++---------------- ipc/glue/MessagePump.h | 69 +++++++++++---------- 2 files changed, 116 insertions(+), 84 deletions(-) diff --git a/ipc/glue/MessagePump.cpp b/ipc/glue/MessagePump.cpp index 39ecef4ba5e2..47c09461de98 100644 --- a/ipc/glue/MessagePump.cpp +++ b/ipc/glue/MessagePump.cpp @@ -4,16 +4,23 @@ #include "MessagePump.h" +#include "nsIRunnable.h" +#include "nsIThread.h" +#include "nsITimer.h" + +#include "base/basictypes.h" +#include "base/logging.h" +#include "base/scoped_nsautorelease_pool.h" +#include "mozilla/Assertions.h" +#include "mozilla/DebugOnly.h" #include "nsComponentManagerUtils.h" +#include "nsDebug.h" #include "nsServiceManagerUtils.h" #include "nsString.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" #include "prthread.h" -#include "base/logging.h" -#include "base/scoped_nsautorelease_pool.h" - #ifdef MOZ_WIDGET_ANDROID #include "AndroidBridge.h" #endif @@ -22,42 +29,37 @@ #include "ipc/Nuwa.h" #endif -using mozilla::ipc::DoWorkRunnable; -using mozilla::ipc::MessagePump; -using mozilla::ipc::MessagePumpForChildProcess; using base::TimeTicks; +using namespace mozilla::ipc; -NS_IMPL_ISUPPORTS2(DoWorkRunnable, nsIRunnable, nsITimerCallback) +static mozilla::DebugOnly gFirstDelegate; -NS_IMETHODIMP -DoWorkRunnable::Run() +namespace mozilla { +namespace ipc { + +class DoWorkRunnable MOZ_FINAL : public nsIRunnable, + public nsITimerCallback { - MessageLoop* loop = MessageLoop::current(); - NS_ASSERTION(loop, "Shouldn't be null!"); - if (loop) { - bool nestableTasksAllowed = loop->NestableTasksAllowed(); - - // MessageLoop::RunTask() disallows nesting, but our Frankenventloop - // will always dispatch DoWork() below from what looks to - // MessageLoop like a nested context. So we unconditionally allow - // nesting here. - loop->SetNestableTasksAllowed(true); - loop->DoWork(); - loop->SetNestableTasksAllowed(nestableTasksAllowed); +public: + DoWorkRunnable(MessagePump* aPump) + : mPump(aPump) + { + MOZ_ASSERT(aPump); } - return NS_OK; -} -NS_IMETHODIMP -DoWorkRunnable::Notify(nsITimer* aTimer) -{ - MessageLoop* loop = MessageLoop::current(); - NS_ASSERTION(loop, "Shouldn't be null!"); - if (loop) { - mPump->DoDelayedWork(loop); - } - return NS_OK; -} + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE + NS_DECL_NSITIMERCALLBACK + +private: + ~DoWorkRunnable() + { } + + MessagePump* mPump; +}; + +} /* namespace ipc */ +} /* namespace mozilla */ MessagePump::MessagePump() : mThread(nullptr) @@ -65,17 +67,21 @@ MessagePump::MessagePump() mDoWorkEvent = new DoWorkRunnable(this); } +MessagePump::~MessagePump() +{ +} + void MessagePump::Run(MessagePump::Delegate* aDelegate) { - NS_ASSERTION(keep_running_, "Quit must have been called outside of Run!"); - NS_ASSERTION(NS_IsMainThread(), "Called Run on the wrong thread!"); + MOZ_ASSERT(keep_running_); + MOZ_ASSERT(NS_IsMainThread()); mThread = NS_GetCurrentThread(); - NS_ASSERTION(mThread, "This should never be null!"); + MOZ_ASSERT(mThread); mDelayedWorkTimer = do_CreateInstance(NS_TIMER_CONTRACTID); - NS_ASSERTION(mDelayedWorkTimer, "Failed to create timer!"); + MOZ_ASSERT(mDelayedWorkTimer); base::ScopedNSAutoreleasePool autoReleasePool; @@ -199,34 +205,56 @@ MessagePump::DoDelayedWork(base::MessagePump::Delegate* aDelegate) } } -#ifdef DEBUG -namespace { -MessagePump::Delegate* gFirstDelegate = nullptr; +NS_IMPL_ISUPPORTS2(DoWorkRunnable, nsIRunnable, nsITimerCallback) + +NS_IMETHODIMP +DoWorkRunnable::Run() +{ + MessageLoop* loop = MessageLoop::current(); + MOZ_ASSERT(loop); + + bool nestableTasksAllowed = loop->NestableTasksAllowed(); + + // MessageLoop::RunTask() disallows nesting, but our Frankenventloop will + // always dispatch DoWork() below from what looks to MessageLoop like a nested + // context. So we unconditionally allow nesting here. + loop->SetNestableTasksAllowed(true); + loop->DoWork(); + loop->SetNestableTasksAllowed(nestableTasksAllowed); + + return NS_OK; +} + +NS_IMETHODIMP +DoWorkRunnable::Notify(nsITimer* aTimer) +{ + MessageLoop* loop = MessageLoop::current(); + MOZ_ASSERT(loop); + + mPump->DoDelayedWork(loop); + + return NS_OK; } -#endif void -MessagePumpForChildProcess::Run(MessagePump::Delegate* aDelegate) +MessagePumpForChildProcess::Run(base::MessagePump::Delegate* aDelegate) { if (mFirstRun) { -#ifdef DEBUG - NS_ASSERTION(aDelegate && gFirstDelegate == nullptr, "Huh?!"); + MOZ_ASSERT(aDelegate && !gFirstDelegate); gFirstDelegate = aDelegate; -#endif + mFirstRun = false; if (NS_FAILED(XRE_RunAppShell())) { NS_WARNING("Failed to run app shell?!"); } -#ifdef DEBUG - NS_ASSERTION(aDelegate && aDelegate == gFirstDelegate, "Huh?!"); + + MOZ_ASSERT(aDelegate && aDelegate == gFirstDelegate); gFirstDelegate = nullptr; -#endif + return; } -#ifdef DEBUG - NS_ASSERTION(aDelegate && aDelegate == gFirstDelegate, "Huh?!"); -#endif + MOZ_ASSERT(aDelegate && aDelegate == gFirstDelegate); // We can get to this point in startup with Tasks in our loop's // incoming_queue_ or pending_queue_, but without a matching @@ -245,7 +273,6 @@ MessagePumpForChildProcess::Run(MessagePump::Delegate* aDelegate) loop->SetNestableTasksAllowed(nestableTasksAllowed); - // Really run. mozilla::ipc::MessagePump::Run(aDelegate); } diff --git a/ipc/glue/MessagePump.h b/ipc/glue/MessagePump.h index c447f7ab7bb3..c8d6fc35701f 100644 --- a/ipc/glue/MessagePump.h +++ b/ipc/glue/MessagePump.h @@ -5,69 +5,74 @@ #ifndef __IPC_GLUE_MESSAGEPUMP_H__ #define __IPC_GLUE_MESSAGEPUMP_H__ -#include "base/basictypes.h" #include "base/message_pump_default.h" #include "base/time.h" - +#include "mozilla/Attributes.h" #include "nsAutoPtr.h" #include "nsCOMPtr.h" -#include "nsIRunnable.h" -#include "nsIThread.h" -#include "nsITimer.h" -#include "mozilla/Attributes.h" +class nsIThread; +class nsITimer; namespace mozilla { namespace ipc { -class MessagePump; - -class DoWorkRunnable MOZ_FINAL : public nsIRunnable, - public nsITimerCallback -{ -public: - DoWorkRunnable(MessagePump* aPump) - : mPump(aPump) { } - - NS_DECL_THREADSAFE_ISUPPORTS - NS_DECL_NSIRUNNABLE - NS_DECL_NSITIMERCALLBACK - -private: - MessagePump* mPump; -}; +class DoWorkRunnable; class MessagePump : public base::MessagePumpDefault { + friend class DoWorkRunnable; public: MessagePump(); - virtual void Run(base::MessagePump::Delegate* aDelegate); - virtual void ScheduleWork(); - virtual void ScheduleWorkForNestedLoop(); - virtual void ScheduleDelayedWork(const base::TimeTicks& delayed_work_time); + // From base::MessagePump. + virtual void + Run(base::MessagePump::Delegate* aDelegate) MOZ_OVERRIDE; - void DoDelayedWork(base::MessagePump::Delegate* aDelegate); + // From base::MessagePump. + virtual void + ScheduleWork() MOZ_OVERRIDE; + + // From base::MessagePump. + virtual void + ScheduleWorkForNestedLoop() MOZ_OVERRIDE; + + // From base::MessagePump. + virtual void + ScheduleDelayedWork(const base::TimeTicks& aDelayedWorkTime) MOZ_OVERRIDE; + +protected: + virtual ~MessagePump(); private: - nsRefPtr mDoWorkEvent; - nsCOMPtr mDelayedWorkTimer; + // Only called by DoWorkRunnable. + void DoDelayedWork(base::MessagePump::Delegate* aDelegate); - // Weak! +protected: + // mDelayedWorkTimer and mThread are set in Run() by this class or its + // subclasses. + nsCOMPtr mDelayedWorkTimer; nsIThread* mThread; + +private: + // Only accessed by this class. + nsRefPtr mDoWorkEvent; }; -class MessagePumpForChildProcess : public MessagePump +class MessagePumpForChildProcess MOZ_FINAL: public MessagePump { public: MessagePumpForChildProcess() : mFirstRun(true) { } - virtual void Run(base::MessagePump::Delegate* aDelegate); + virtual void Run(base::MessagePump::Delegate* aDelegate) MOZ_OVERRIDE; private: + ~MessagePumpForChildProcess() + { } + bool mFirstRun; }; From 4d2b15f34cbb3e25cd7a979e23999e060ebe188b Mon Sep 17 00:00:00 2001 From: Ben Turner Date: Wed, 23 Oct 2013 05:01:20 -0700 Subject: [PATCH 04/58] Bug 939182 - Add 'eventWasProcessed' argument to nsIThreadObserver::afterProcessNextEvent(), r=bsmedberg. --HG-- extra : transplant_source : %5E%80p%D6%C6A%23%0AZ%06%23%16%155%DB%CE%F5%5CEx --- dom/ipc/ContentParent.cpp | 3 +- js/xpconnect/src/nsXPConnect.cpp | 3 +- layout/style/Loader.cpp | 3 +- .../base/src/nsSocketTransportService2.cpp | 5 ++-- netwerk/cache2/CacheIOThread.cpp | 3 +- widget/cocoa/nsAppShell.h | 3 +- widget/cocoa/nsAppShell.mm | 6 ++-- widget/xpwidgets/nsBaseAppShell.cpp | 3 +- xpcom/threads/LazyIdleThread.cpp | 9 ++++-- xpcom/threads/nsIThreadInternal.idl | 30 ++++++++++++------- xpcom/threads/nsThread.cpp | 7 +++-- 11 files changed, 47 insertions(+), 28 deletions(-) diff --git a/dom/ipc/ContentParent.cpp b/dom/ipc/ContentParent.cpp index ea43d0ecf114..dd5848a0731d 100644 --- a/dom/ipc/ContentParent.cpp +++ b/dom/ipc/ContentParent.cpp @@ -2724,7 +2724,8 @@ ContentParent::OnProcessNextEvent(nsIThreadInternal *thread, /* void afterProcessNextEvent (in nsIThreadInternal thread, in unsigned long recursionDepth); */ NS_IMETHODIMP ContentParent::AfterProcessNextEvent(nsIThreadInternal *thread, - uint32_t recursionDepth) + uint32_t recursionDepth, + bool eventWasProcessed) { return NS_OK; } diff --git a/js/xpconnect/src/nsXPConnect.cpp b/js/xpconnect/src/nsXPConnect.cpp index b7e8097548ef..1c893e9db937 100644 --- a/js/xpconnect/src/nsXPConnect.cpp +++ b/js/xpconnect/src/nsXPConnect.cpp @@ -1120,7 +1120,8 @@ nsXPConnect::OnProcessNextEvent(nsIThreadInternal *aThread, bool aMayWait, NS_IMETHODIMP nsXPConnect::AfterProcessNextEvent(nsIThreadInternal *aThread, - uint32_t aRecursionDepth) + uint32_t aRecursionDepth, + bool aEventWasProcessed) { // Watch out for unpaired events during observer registration. if (MOZ_UNLIKELY(mEventDepth == 0)) diff --git a/layout/style/Loader.cpp b/layout/style/Loader.cpp index 21a9b3b0f72c..74d47ca8f3b9 100644 --- a/layout/style/Loader.cpp +++ b/layout/style/Loader.cpp @@ -449,7 +449,8 @@ SheetLoadData::OnProcessNextEvent(nsIThreadInternal* aThread, NS_IMETHODIMP SheetLoadData::AfterProcessNextEvent(nsIThreadInternal* aThread, - uint32_t aRecursionDepth) + uint32_t aRecursionDepth, + bool aEventWasProcessed) { // We want to fire our load even before or after event processing, // whichever comes first. diff --git a/netwerk/base/src/nsSocketTransportService2.cpp b/netwerk/base/src/nsSocketTransportService2.cpp index e0af48aa2e56..d4af9a08a0fc 100644 --- a/netwerk/base/src/nsSocketTransportService2.cpp +++ b/netwerk/base/src/nsSocketTransportService2.cpp @@ -631,7 +631,8 @@ nsSocketTransportService::OnProcessNextEvent(nsIThreadInternal *thread, NS_IMETHODIMP nsSocketTransportService::AfterProcessNextEvent(nsIThreadInternal* thread, - uint32_t depth) + uint32_t depth, + bool eventWasProcessed) { return NS_OK; } @@ -1128,5 +1129,3 @@ nsSocketTransportService::GetSocketConnections(nsTArray *data) for (uint32_t i = 0; i < mIdleCount; i++) AnalyzeConnection(data, &mIdleList[i], false); } - - diff --git a/netwerk/cache2/CacheIOThread.cpp b/netwerk/cache2/CacheIOThread.cpp index ebc1c8ce1a4a..0003f4f38b52 100644 --- a/netwerk/cache2/CacheIOThread.cpp +++ b/netwerk/cache2/CacheIOThread.cpp @@ -241,7 +241,8 @@ NS_IMETHODIMP CacheIOThread::OnProcessNextEvent(nsIThreadInternal *thread, bool return NS_OK; } -NS_IMETHODIMP CacheIOThread::AfterProcessNextEvent(nsIThreadInternal *thread, uint32_t recursionDepth) +NS_IMETHODIMP CacheIOThread::AfterProcessNextEvent(nsIThreadInternal *thread, uint32_t recursionDepth, + bool eventWasProcessed) { return NS_OK; } diff --git a/widget/cocoa/nsAppShell.h b/widget/cocoa/nsAppShell.h index 2281f5f05d7d..9a3b27c7a547 100644 --- a/widget/cocoa/nsAppShell.h +++ b/widget/cocoa/nsAppShell.h @@ -70,7 +70,8 @@ public: NS_IMETHOD OnProcessNextEvent(nsIThreadInternal *aThread, bool aMayWait, uint32_t aRecursionDepth); NS_IMETHOD AfterProcessNextEvent(nsIThreadInternal *aThread, - uint32_t aRecursionDepth); + uint32_t aRecursionDepth, + bool aEventWasProcessed); // public only to be visible to Objective-C code that must call it void WillTerminate(); diff --git a/widget/cocoa/nsAppShell.mm b/widget/cocoa/nsAppShell.mm index 903b3c3d0098..55a059fec587 100644 --- a/widget/cocoa/nsAppShell.mm +++ b/widget/cocoa/nsAppShell.mm @@ -840,7 +840,8 @@ nsAppShell::OnProcessNextEvent(nsIThreadInternal *aThread, bool aMayWait, // public NS_IMETHODIMP nsAppShell::AfterProcessNextEvent(nsIThreadInternal *aThread, - uint32_t aRecursionDepth) + uint32_t aRecursionDepth, + bool aEventWasProcessed) { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT; @@ -856,7 +857,8 @@ nsAppShell::AfterProcessNextEvent(nsIThreadInternal *aThread, ::CFArrayRemoveValueAtIndex(mAutoreleasePools, count - 1); [pool release]; - return nsBaseAppShell::AfterProcessNextEvent(aThread, aRecursionDepth); + return nsBaseAppShell::AfterProcessNextEvent(aThread, aRecursionDepth, + aEventWasProcessed); NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT; } diff --git a/widget/xpwidgets/nsBaseAppShell.cpp b/widget/xpwidgets/nsBaseAppShell.cpp index d7573dfc0831..c8d37700de29 100644 --- a/widget/xpwidgets/nsBaseAppShell.cpp +++ b/widget/xpwidgets/nsBaseAppShell.cpp @@ -399,7 +399,8 @@ nsBaseAppShell::ScheduleSyncSection(nsIRunnable* aRunnable, bool aStable) // Called from the main thread NS_IMETHODIMP nsBaseAppShell::AfterProcessNextEvent(nsIThreadInternal *thr, - uint32_t recursionDepth) + uint32_t recursionDepth, + bool eventWasProcessed) { // We've just finished running an event, so we're in a stable state. RunSyncSections(true, recursionDepth); diff --git a/xpcom/threads/LazyIdleThread.cpp b/xpcom/threads/LazyIdleThread.cpp index 71bf2ea53718..75e0c1699932 100644 --- a/xpcom/threads/LazyIdleThread.cpp +++ b/xpcom/threads/LazyIdleThread.cpp @@ -501,14 +501,17 @@ LazyIdleThread::OnProcessNextEvent(nsIThreadInternal* /* aThread */, NS_IMETHODIMP LazyIdleThread::AfterProcessNextEvent(nsIThreadInternal* /* aThread */, - uint32_t /* aRecursionDepth */) + uint32_t /* aRecursionDepth */, + bool aEventWasProcessed) { bool shouldNotifyIdle; { MutexAutoLock lock(mMutex); - MOZ_ASSERT(mPendingEventCount, "Mismatched calls to observer methods!"); - --mPendingEventCount; + if (aEventWasProcessed) { + MOZ_ASSERT(mPendingEventCount, "Mismatched calls to observer methods!"); + --mPendingEventCount; + } if (mThreadIsShuttingDown) { // We're shutting down, no need to fire any timer. diff --git a/xpcom/threads/nsIThreadInternal.idl b/xpcom/threads/nsIThreadInternal.idl index e0e31987f58d..5bbc15ad5d14 100644 --- a/xpcom/threads/nsIThreadInternal.idl +++ b/xpcom/threads/nsIThreadInternal.idl @@ -6,8 +6,8 @@ #include "nsIThread.idl" +interface nsIRunnable; interface nsIThreadObserver; -interface nsIThreadEventFilter; /** * The XPCOM thread object implements this interface, which allows a consumer @@ -33,11 +33,12 @@ interface nsIThreadInternal : nsIThread readonly attribute unsigned long recursionDepth; /** - * Add an observer that will *only* receive onProcessNextEvent and - * afterProcessNextEvent callbacks. Always called on the target thread, and - * the implementation does not have to be threadsafe. Order of callbacks is - * not guaranteed (i.e. afterProcessNextEvent may be called first depending on - * whether or not the observer is added in a nested loop). Holds a strong ref. + * Add an observer that will *only* receive onProcessNextEvent, + * beforeProcessNextEvent. and afterProcessNextEvent callbacks. Always called + * on the target thread, and the implementation does not have to be + * threadsafe. Order of callbacks is not guaranteed (i.e. + * afterProcessNextEvent may be called first depending on whether or not the + * observer is added in a nested loop). Holds a strong ref. */ void addObserver(in nsIThreadObserver observer); @@ -77,7 +78,7 @@ interface nsIThreadInternal : nsIThread * afterProcessNextEvent, then another that inherits the first and adds * onDispatchedEvent. */ -[scriptable, uuid(81D0B509-F198-4417-8020-08EB4271491F)] +[scriptable, uuid(09b424c3-26b0-4128-9039-d66f85b02c63)] interface nsIThreadObserver : nsISupports { /** @@ -90,8 +91,9 @@ interface nsIThreadObserver : nsISupports void onDispatchedEvent(in nsIThreadInternal thread); /** - * This method is called (from nsIThread::ProcessNextEvent) before an event - * is processed. This method is only called on the target thread. + * This method is called when nsIThread::ProcessNextEvent is called. It does + * not guarantee that an event is actually going to be processed. This method + * is only called on the target thread. * * @param thread * The thread being asked to process another event. @@ -107,14 +109,20 @@ interface nsIThreadObserver : nsISupports /** * This method is called (from nsIThread::ProcessNextEvent) after an event - * is processed. This method is only called on the target thread. + * is processed. It does not guarantee that an event was actually processed + * (depends on the value of |eventWasProcessed|. This method is only called + * on the target thread. * * @param thread * The thread that processed another event. * @param recursionDepth * Indicates the number of calls to ProcessNextEvent on the call stack in * addition to the current call. + * @param eventWasProcessed + * Indicates whether an event was actually processed. May be false if the + * |mayWait| flag was false when calling nsIThread::ProcessNextEvent(). */ void afterProcessNextEvent(in nsIThreadInternal thread, - in unsigned long recursionDepth); + in unsigned long recursionDepth, + in bool eventWasProcessed); }; diff --git a/xpcom/threads/nsThread.cpp b/xpcom/threads/nsThread.cpp index 9cc4e2528e8f..4ff0c48c08b0 100644 --- a/xpcom/threads/nsThread.cpp +++ b/xpcom/threads/nsThread.cpp @@ -619,13 +619,14 @@ nsThread::ProcessNextEvent(bool mayWait, bool *result) --mRunningEvent; - NOTIFY_EVENT_OBSERVERS(AfterProcessNextEvent, (this, mRunningEvent)); + NOTIFY_EVENT_OBSERVERS(AfterProcessNextEvent, + (this, mRunningEvent, *result)); if (obs) - obs->AfterProcessNextEvent(this, mRunningEvent); + obs->AfterProcessNextEvent(this, mRunningEvent, *result); if (notifyMainThreadObserver && sMainThreadObserver) - sMainThreadObserver->AfterProcessNextEvent(this, mRunningEvent); + sMainThreadObserver->AfterProcessNextEvent(this, mRunningEvent, *result); return rv; } From 057f36931202ed11b5002b6665f0ba1a1cca31db Mon Sep 17 00:00:00 2001 From: Ben Turner Date: Wed, 23 Oct 2013 05:01:24 -0700 Subject: [PATCH 05/58] Bug 939182 - Integrate the chromium MessageLoop into nsThread, r=bsmedberg. --HG-- extra : transplant_source : %82%D3%C5%E1%A4%E5%00%1C%C3%82%97v%7F%BA%20Ja%AA%C7%E5 --- ipc/chromium/src/base/message_loop.cc | 4 ++ ipc/chromium/src/base/message_loop.h | 7 +++- ipc/glue/MessagePump.cpp | 57 ++++++++++++++++++++++++++- ipc/glue/MessagePump.h | 13 ++++++ xpcom/threads/moz.build | 2 + xpcom/threads/nsThread.cpp | 53 +++++++++++++++---------- 6 files changed, 114 insertions(+), 22 deletions(-) diff --git a/ipc/chromium/src/base/message_loop.cc b/ipc/chromium/src/base/message_loop.cc index 5f70be1d6335..3ac4d579a2e1 100644 --- a/ipc/chromium/src/base/message_loop.cc +++ b/ipc/chromium/src/base/message_loop.cc @@ -118,6 +118,10 @@ MessageLoop::MessageLoop(Type type) run_depth_base_ = 2; return; } + if (type_ == TYPE_MOZILLA_NONMAINTHREAD) { + pump_ = new mozilla::ipc::MessagePumpForNonMainThreads(); + return; + } #if defined(OS_WIN) // TODO(rvargas): Get rid of the OS guards. diff --git a/ipc/chromium/src/base/message_loop.h b/ipc/chromium/src/base/message_loop.h index acf97f8baaea..d04590684b7d 100644 --- a/ipc/chromium/src/base/message_loop.h +++ b/ipc/chromium/src/base/message_loop.h @@ -203,12 +203,17 @@ public: // This type of ML is used in Mozilla parent processes which initialize // XPCOM and use the gecko event loop. // + // TYPE_MOZILLA_NONMAINTHREAD + // This type of ML is used in Mozilla parent processes which initialize + // XPCOM and use the nsThread event loop. + // enum Type { TYPE_DEFAULT, TYPE_UI, TYPE_IO, TYPE_MOZILLA_CHILD, - TYPE_MOZILLA_UI + TYPE_MOZILLA_UI, + TYPE_MOZILLA_NONMAINTHREAD }; // Normally, it is not necessary to instantiate a MessageLoop. Instead, it diff --git a/ipc/glue/MessagePump.cpp b/ipc/glue/MessagePump.cpp index 47c09461de98..36818092b567 100644 --- a/ipc/glue/MessagePump.cpp +++ b/ipc/glue/MessagePump.cpp @@ -75,7 +75,8 @@ void MessagePump::Run(MessagePump::Delegate* aDelegate) { MOZ_ASSERT(keep_running_); - MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(NS_IsMainThread(), + "Use mozilla::ipc::MessagePumpForNonMainThreads instead!"); mThread = NS_GetCurrentThread(); MOZ_ASSERT(mThread); @@ -276,3 +277,57 @@ MessagePumpForChildProcess::Run(base::MessagePump::Delegate* aDelegate) // Really run. mozilla::ipc::MessagePump::Run(aDelegate); } + +void +MessagePumpForNonMainThreads::Run(base::MessagePump::Delegate* aDelegate) +{ + MOZ_ASSERT(keep_running_); + MOZ_ASSERT(!NS_IsMainThread(), "Use mozilla::ipc::MessagePump instead!"); + + mThread = NS_GetCurrentThread(); + MOZ_ASSERT(mThread); + + mDelayedWorkTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + MOZ_ASSERT(mDelayedWorkTimer); + + if (NS_FAILED(mDelayedWorkTimer->SetTarget(mThread))) { + MOZ_CRASH("Failed to set timer target!"); + } + + for (;;) { + bool didWork = NS_ProcessNextEvent(mThread, false) ? true : false; + if (!keep_running_) { + break; + } + + didWork |= aDelegate->DoDelayedWork(&delayed_work_time_); + + if (didWork && delayed_work_time_.is_null()) { + mDelayedWorkTimer->Cancel(); + } + + if (!keep_running_) { + break; + } + + if (didWork) { + continue; + } + + didWork = aDelegate->DoIdleWork(); + if (!keep_running_) { + break; + } + + if (didWork) { + continue; + } + + // This will either sleep or process an event. + NS_ProcessNextEvent(mThread, true); + } + + mDelayedWorkTimer->Cancel(); + + keep_running_ = true; +} diff --git a/ipc/glue/MessagePump.h b/ipc/glue/MessagePump.h index c8d6fc35701f..0850444fa861 100644 --- a/ipc/glue/MessagePump.h +++ b/ipc/glue/MessagePump.h @@ -76,6 +76,19 @@ private: bool mFirstRun; }; +class MessagePumpForNonMainThreads MOZ_FINAL : public MessagePump +{ +public: + MessagePumpForNonMainThreads() + { } + + virtual void Run(base::MessagePump::Delegate* aDelegate) MOZ_OVERRIDE; + +private: + ~MessagePumpForNonMainThreads() + { } +}; + } /* namespace ipc */ } /* namespace mozilla */ diff --git a/xpcom/threads/moz.build b/xpcom/threads/moz.build index 94a0234ad6df..d846126fad19 100644 --- a/xpcom/threads/moz.build +++ b/xpcom/threads/moz.build @@ -57,3 +57,5 @@ LOCAL_INCLUDES += [ ] FINAL_LIBRARY = 'xpcom_core' + +include('/ipc/chromium/chromium-config.mozbuild') diff --git a/xpcom/threads/nsThread.cpp b/xpcom/threads/nsThread.cpp index 4ff0c48c08b0..573c3a7a8dd1 100644 --- a/xpcom/threads/nsThread.cpp +++ b/xpcom/threads/nsThread.cpp @@ -4,9 +4,17 @@ * 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/. */ +#include "nsThread.h" + +#include "base/message_loop.h" + +// Chromium's logging can sometimes leak through... +#ifdef LOG +#undef LOG +#endif + #include "mozilla/ReentrantMonitor.h" #include "nsMemoryPressure.h" -#include "nsThread.h" #include "nsThreadManager.h" #include "nsIClassInfoImpl.h" #include "nsIProgrammingLanguage.h" @@ -214,6 +222,7 @@ public: } NS_IMETHOD Run() { mThread->mShutdownContext = mShutdownContext; + MessageLoop::current()->Quit(); return NS_OK; } private: @@ -241,28 +250,32 @@ nsThread::ThreadFunc(void *arg) event->Run(); // unblocks nsThread::Init event = nullptr; - // Now, process incoming events... - while (!self->ShuttingDown()) - NS_ProcessNextEvent(self); + { // Scope for MessageLoop. + nsAutoPtr loop( + new MessageLoop(MessageLoop::TYPE_MOZILLA_NONMAINTHREAD)); - // Do NS_ProcessPendingEvents but with special handling to set - // mEventsAreDoomed atomically with the removal of the last event. The key - // invariant here is that we will never permit PutEvent to succeed if the - // event would be left in the queue after our final call to - // NS_ProcessPendingEvents. - while (true) { - { - MutexAutoLock lock(self->mLock); - if (!self->mEvents.HasPendingEvent()) { - // No events in the queue, so we will stop now. Don't let any more - // events be added, since they won't be processed. It is critical - // that no PutEvent can occur between testing that the event queue is - // empty and setting mEventsAreDoomed! - self->mEventsAreDoomed = true; - break; + // Now, process incoming events... + loop->Run(); + + // Do NS_ProcessPendingEvents but with special handling to set + // mEventsAreDoomed atomically with the removal of the last event. The key + // invariant here is that we will never permit PutEvent to succeed if the + // event would be left in the queue after our final call to + // NS_ProcessPendingEvents. + while (true) { + { + MutexAutoLock lock(self->mLock); + if (!self->mEvents.HasPendingEvent()) { + // No events in the queue, so we will stop now. Don't let any more + // events be added, since they won't be processed. It is critical + // that no PutEvent can occur between testing that the event queue is + // empty and setting mEventsAreDoomed! + self->mEventsAreDoomed = true; + break; + } } + NS_ProcessPendingEvents(self); } - NS_ProcessPendingEvents(self); } // Inform the threadmanager that this thread is going away From ae412f7c08fd29ffb6711fb5ce96b1a18c9fac6e Mon Sep 17 00:00:00 2001 From: Ben Turner Date: Mon, 9 Dec 2013 11:57:29 -0800 Subject: [PATCH 06/58] Bug 939182 - Prevent mock timers from interfering with the message pump machinery, r=bsmedberg. --HG-- extra : transplant_source : %2AEm%1D%2C%0Abd%83%B6%BEO%96%D4%BB%13%29%82%9D4 --- ipc/glue/MessagePump.cpp | 9 ++++++--- ipc/glue/moz.build | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ipc/glue/MessagePump.cpp b/ipc/glue/MessagePump.cpp index 36818092b567..edaf734b9c49 100644 --- a/ipc/glue/MessagePump.cpp +++ b/ipc/glue/MessagePump.cpp @@ -18,6 +18,7 @@ #include "nsServiceManagerUtils.h" #include "nsString.h" #include "nsThreadUtils.h" +#include "nsTimerImpl.h" #include "nsXULAppAPI.h" #include "prthread.h" @@ -32,6 +33,8 @@ using base::TimeTicks; using namespace mozilla::ipc; +NS_DEFINE_NAMED_CID(NS_TIMER_CID); + static mozilla::DebugOnly gFirstDelegate; namespace mozilla { @@ -81,7 +84,7 @@ MessagePump::Run(MessagePump::Delegate* aDelegate) mThread = NS_GetCurrentThread(); MOZ_ASSERT(mThread); - mDelayedWorkTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + mDelayedWorkTimer = do_CreateInstance(kNS_TIMER_CID); MOZ_ASSERT(mDelayedWorkTimer); base::ScopedNSAutoreleasePool autoReleasePool; @@ -172,7 +175,7 @@ MessagePump::ScheduleDelayedWork(const base::TimeTicks& aDelayedTime) #endif if (!mDelayedWorkTimer) { - mDelayedWorkTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + mDelayedWorkTimer = do_CreateInstance(kNS_TIMER_CID); if (!mDelayedWorkTimer) { // Called before XPCOM has started up? We can't do this correctly. NS_WARNING("Delayed task might not run!"); @@ -287,7 +290,7 @@ MessagePumpForNonMainThreads::Run(base::MessagePump::Delegate* aDelegate) mThread = NS_GetCurrentThread(); MOZ_ASSERT(mThread); - mDelayedWorkTimer = do_CreateInstance(NS_TIMER_CONTRACTID); + mDelayedWorkTimer = do_CreateInstance(kNS_TIMER_CID); MOZ_ASSERT(mDelayedWorkTimer); if (NS_FAILED(mDelayedWorkTimer->SetTarget(mThread))) { diff --git a/ipc/glue/moz.build b/ipc/glue/moz.build index 6e6e2745bfad..e15ede75d198 100644 --- a/ipc/glue/moz.build +++ b/ipc/glue/moz.build @@ -103,6 +103,11 @@ IPDL_SOURCES = [ 'URIParams.ipdlh', ] + +LOCAL_INCLUDES += [ + '/xpcom/threads', +] + include('/ipc/chromium/chromium-config.mozbuild') FINAL_LIBRARY = 'xul' From 3233aad341315c5308bc95dcf0b64156afaaa86d Mon Sep 17 00:00:00 2001 From: Ben Turner Date: Thu, 14 Nov 2013 10:06:17 -0800 Subject: [PATCH 07/58] Bug 939196 - Allow nsThread to have nested event queues, r=bsmedberg. --HG-- extra : transplant_source : %DE%AF%BF%7E%94%5E%07%CDL%9C%01%28%9F6%8D%90%12%95%06s --- xpcom/threads/nsIThreadInternal.idl | 24 +++++- xpcom/threads/nsThread.cpp | 120 +++++++++++++++++++++++----- xpcom/threads/nsThread.h | 60 +++++++++++++- 3 files changed, 182 insertions(+), 22 deletions(-) diff --git a/xpcom/threads/nsIThreadInternal.idl b/xpcom/threads/nsIThreadInternal.idl index 5bbc15ad5d14..1c2782e4cedf 100644 --- a/xpcom/threads/nsIThreadInternal.idl +++ b/xpcom/threads/nsIThreadInternal.idl @@ -13,7 +13,7 @@ interface nsIThreadObserver; * The XPCOM thread object implements this interface, which allows a consumer * to observe dispatch activity on the thread. */ -[scriptable, uuid(504e9e1f-70e1-4f33-a785-5840a4680414)] +[scriptable, uuid(b24c5af3-43c2-4d17-be14-94d6648a305f)] interface nsIThreadInternal : nsIThread { /** @@ -47,6 +47,28 @@ interface nsIThreadInternal : nsIThread * observer will never be called again by the thread. */ void removeObserver(in nsIThreadObserver observer); + + /** + * This method causes any events currently enqueued on the thread to be + * suppressed until PopEventQueue is called, and any event dispatched to this + * thread's nsIEventTarget will queue as well. Calls to PushEventQueue may be + * nested and must each be paired with a call to PopEventQueue in order to + * restore the original state of the thread. The returned nsIEventTarget may + * be used to push events onto the nested queue. Dispatching will be disabled + * once the event queue is popped. The thread will only ever process pending + * events for the innermost event queue. Must only be called on the target + * thread. + */ + [noscript] nsIEventTarget pushEventQueue(); + + /** + * Revert a call to PushEventQueue. When an event queue is popped, any events + * remaining in the queue are appended to the elder queue. This also causes + * the nsIEventTarget returned from PushEventQueue to stop dispatching events. + * Must only be called on the target thread, and with the innermost event + * queue. + */ + [noscript] void popEventQueue(in nsIEventTarget aInnermostTarget); }; /** diff --git a/xpcom/threads/nsThread.cpp b/xpcom/threads/nsThread.cpp index 573c3a7a8dd1..ff6a609c8f56 100644 --- a/xpcom/threads/nsThread.cpp +++ b/xpcom/threads/nsThread.cpp @@ -265,7 +265,7 @@ nsThread::ThreadFunc(void *arg) while (true) { { MutexAutoLock lock(self->mLock); - if (!self->mEvents.HasPendingEvent()) { + if (!self->mEvents->HasPendingEvent()) { // No events in the queue, so we will stop now. Don't let any more // events be added, since they won't be processed. It is critical // that no PutEvent can occur between testing that the event queue is @@ -299,6 +299,7 @@ int sCanaryOutputFD = -1; nsThread::nsThread(MainThreadFlag aMainThread, uint32_t aStackSize) : mLock("nsThread.mLock") + , mEvents(&mEventsRoot) , mPriority(PRIORITY_NORMAL) , mThread(nullptr) , mRunningEvent(0) @@ -338,7 +339,7 @@ nsThread::Init() // that mThread is set properly. { MutexAutoLock lock(mLock); - mEvents.PutEvent(startup); + mEventsRoot.PutEvent(startup); } // Wait for thread to call ThreadManager::SetupCurrentThread, which completes @@ -357,15 +358,16 @@ nsThread::InitCurrentThread() } nsresult -nsThread::PutEvent(nsIRunnable *event) +nsThread::PutEvent(nsIRunnable *event, nsNestedEventTarget *target) { { MutexAutoLock lock(mLock); - if (mEventsAreDoomed) { + nsChainedEventQueue *queue = target ? target->mQueue : &mEventsRoot; + if (!queue || (queue == &mEventsRoot && mEventsAreDoomed)) { NS_WARNING("An event was posted to a thread that will never run it (rejected)"); return NS_ERROR_UNEXPECTED; } - if (!mEvents.PutEvent(event)) + if (!queue->PutEvent(event)) return NS_ERROR_OUT_OF_MEMORY; } @@ -376,18 +378,14 @@ nsThread::PutEvent(nsIRunnable *event) return NS_OK; } -//----------------------------------------------------------------------------- -// nsIEventTarget - -NS_IMETHODIMP -nsThread::Dispatch(nsIRunnable *event, uint32_t flags) +nsresult +nsThread::DispatchInternal(nsIRunnable *event, uint32_t flags, + nsNestedEventTarget *target) { - LOG(("THRD(%p) Dispatch [%p %x]\n", this, event, flags)); - if (NS_WARN_IF(!event)) return NS_ERROR_INVALID_ARG; - if (gXPCOMThreadsShutDown && MAIN_THREAD != mIsMainThread) { + if (gXPCOMThreadsShutDown && MAIN_THREAD != mIsMainThread && !target) { return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } @@ -404,7 +402,7 @@ nsThread::Dispatch(nsIRunnable *event, uint32_t flags) new nsThreadSyncDispatch(thread, event); if (!wrapper) return NS_ERROR_OUT_OF_MEMORY; - nsresult rv = PutEvent(wrapper); + nsresult rv = PutEvent(wrapper, target); // Don't wait for the event to finish if we didn't dispatch it... if (NS_FAILED(rv)) return rv; @@ -415,7 +413,18 @@ nsThread::Dispatch(nsIRunnable *event, uint32_t flags) } NS_ASSERTION(flags == NS_DISPATCH_NORMAL, "unexpected dispatch flags"); - return PutEvent(event); + return PutEvent(event, target); +} + +//----------------------------------------------------------------------------- +// nsIEventTarget + +NS_IMETHODIMP +nsThread::Dispatch(nsIRunnable *event, uint32_t flags) +{ + LOG(("THRD(%p) Dispatch [%p %x]\n", this, event, flags)); + + return DispatchInternal(event, flags, nullptr); } NS_IMETHODIMP @@ -467,7 +476,7 @@ nsThread::Shutdown() if (!event) return NS_ERROR_OUT_OF_MEMORY; // XXXroc What if posting the event fails due to OOM? - PutEvent(event); + PutEvent(event, nullptr); // We could still end up with other events being added after the shutdown // task, but that's okay because we process pending events in ThreadFunc @@ -503,7 +512,7 @@ nsThread::HasPendingEvents(bool *result) if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) return NS_ERROR_NOT_SAME_THREAD; - *result = mEvents.GetEvent(false, nullptr); + *result = mEvents->GetEvent(false, nullptr); return NS_OK; } @@ -614,7 +623,7 @@ nsThread::ProcessNextEvent(bool mayWait, bool *result) // If we are shutting down, then do not wait for new events. nsCOMPtr event; - mEvents.GetEvent(mayWait && !ShuttingDown(), getter_AddRefs(event)); + mEvents->GetEvent(mayWait && !ShuttingDown(), getter_AddRefs(event)); *result = (event.get() != nullptr); @@ -754,6 +763,62 @@ nsThread::RemoveObserver(nsIThreadObserver *observer) return NS_OK; } +NS_IMETHODIMP +nsThread::PushEventQueue(nsIEventTarget **result) +{ + if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) + return NS_ERROR_NOT_SAME_THREAD; + + nsChainedEventQueue *queue = new nsChainedEventQueue(); + queue->mEventTarget = new nsNestedEventTarget(this, queue); + + { + MutexAutoLock lock(mLock); + queue->mNext = mEvents; + mEvents = queue; + } + + NS_ADDREF(*result = queue->mEventTarget); + return NS_OK; +} + +NS_IMETHODIMP +nsThread::PopEventQueue(nsIEventTarget *innermostTarget) +{ + if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) + return NS_ERROR_NOT_SAME_THREAD; + + if (NS_WARN_IF(!innermostTarget)) + return NS_ERROR_NULL_POINTER; + + // Don't delete or release anything while holding the lock. + nsAutoPtr queue; + nsRefPtr target; + + { + MutexAutoLock lock(mLock); + + // Make sure we're popping the innermost event target. + if (NS_WARN_IF(mEvents->mEventTarget != innermostTarget)) + return NS_ERROR_UNEXPECTED; + + MOZ_ASSERT(mEvents != &mEventsRoot); + + queue = mEvents; + mEvents = mEvents->mNext; + + nsCOMPtr event; + while (queue->GetEvent(false, getter_AddRefs(event))) + mEvents->PutEvent(event); + + // Don't let the event target post any more events. + queue->mEventTarget.swap(target); + target->mQueue = nullptr; + } + + return NS_OK; +} + nsresult nsThread::SetMainThreadObserver(nsIThreadObserver* aObserver) { @@ -782,3 +847,22 @@ nsThreadSyncDispatch::Run() } return NS_OK; } + +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS1(nsThread::nsNestedEventTarget, nsIEventTarget) + +NS_IMETHODIMP +nsThread::nsNestedEventTarget::Dispatch(nsIRunnable *event, uint32_t flags) +{ + LOG(("THRD(%p) Dispatch [%p %x] to nested loop %p\n", mThread.get(), event, + flags, this)); + + return mThread->DispatchInternal(event, flags, this); +} + +NS_IMETHODIMP +nsThread::nsNestedEventTarget::IsOnCurrentThread(bool *result) +{ + return mThread->IsOnCurrentThread(result); +} diff --git a/xpcom/threads/nsThread.h b/xpcom/threads/nsThread.h index 0a54808d63ac..402a4cdaa97d 100644 --- a/xpcom/threads/nsThread.h +++ b/xpcom/threads/nsThread.h @@ -15,6 +15,7 @@ #include "nsString.h" #include "nsTObserverArray.h" #include "mozilla/Attributes.h" +#include "nsAutoPtr.h" // A native thread class nsThread MOZ_FINAL : public nsIThreadInternal, @@ -56,6 +57,11 @@ public: private: static nsIThreadObserver* sMainThreadObserver; + class nsChainedEventQueue; + + class nsNestedEventTarget; + friend class nsNestedEventTarget; + friend class nsThreadShutdownEvent; ~nsThread(); @@ -73,9 +79,56 @@ private: // Wrappers for event queue methods: bool GetEvent(bool mayWait, nsIRunnable **event) { - return mEvents.GetEvent(mayWait, event); + return mEvents->GetEvent(mayWait, event); } - nsresult PutEvent(nsIRunnable *event); + nsresult PutEvent(nsIRunnable *event, nsNestedEventTarget *target); + + nsresult DispatchInternal(nsIRunnable *event, uint32_t flags, + nsNestedEventTarget *target); + + // Wrapper for nsEventQueue that supports chaining. + class nsChainedEventQueue { + public: + nsChainedEventQueue() + : mNext(nullptr) { + } + + bool GetEvent(bool mayWait, nsIRunnable **event) { + return mQueue.GetEvent(mayWait, event); + } + + bool PutEvent(nsIRunnable *event) { + return mQueue.PutEvent(event); + } + + bool HasPendingEvent() { + return mQueue.HasPendingEvent(); + } + + nsChainedEventQueue *mNext; + nsRefPtr mEventTarget; + + private: + nsEventQueue mQueue; + }; + + class nsNestedEventTarget MOZ_FINAL : public nsIEventTarget { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET + + nsNestedEventTarget(nsThread *thread, nsChainedEventQueue *queue) + : mThread(thread), mQueue(queue) { + } + + nsRefPtr mThread; + + // This is protected by mThread->mLock. + nsChainedEventQueue* mQueue; + + private: + ~nsNestedEventTarget() {} + }; // This lock protects access to mObserver, mEvents and mEventsAreDoomed. // All of those fields are only modified on the thread itself (never from @@ -89,7 +142,8 @@ private: // Only accessed on the target thread. nsAutoTObserverArray, 2> mEventObservers; - nsEventQueue mEvents; + nsChainedEventQueue *mEvents; // never null + nsChainedEventQueue mEventsRoot; int32_t mPriority; PRThread *mThread; From 6a625e7742ca098ccca040776d007e8ce1bc91d7 Mon Sep 17 00:00:00 2001 From: Rodrigo Silveira Date: Mon, 9 Dec 2013 10:41:58 -0800 Subject: [PATCH 08/58] Bug 944255 - about:start bookmarks and history tiles cut off when using split views r=sfoster * * * Bug 944255 - Test fix --HG-- extra : rebase_source : cee8e82050c3c95bd83285712567e08e3b9db5cf --- .../base/tests/mochitest/helpers/ViewStateHelper.js | 2 ++ browser/metro/modules/View.jsm | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/browser/metro/base/tests/mochitest/helpers/ViewStateHelper.js b/browser/metro/base/tests/mochitest/helpers/ViewStateHelper.js index 8340f0e7b663..6860f72219a4 100644 --- a/browser/metro/base/tests/mochitest/helpers/ViewStateHelper.js +++ b/browser/metro/base/tests/mochitest/helpers/ViewStateHelper.js @@ -46,6 +46,7 @@ function setPortraitViewstate() { ContentAreaObserver._updateViewState("portrait"); ContentAreaObserver._dispatchBrowserEvent("SizeChanged"); + yield waitForMessage("Content:SetWindowSize:Complete", browser.messageManager); // Make sure it renders the new mode properly yield waitForMs(0); @@ -54,6 +55,7 @@ function setPortraitViewstate() { function restoreViewstate() { ContentAreaObserver._updateViewState("landscape"); ContentAreaObserver._dispatchBrowserEvent("SizeChanged"); + yield waitForMessage("Content:SetWindowSize:Complete", Browser.selectedBrowser.messageManager); ok(isLandscapeMode(), "restoreViewstate should restore landscape mode."); diff --git a/browser/metro/modules/View.jsm b/browser/metro/modules/View.jsm index d2e771856583..cd60b89181fc 100644 --- a/browser/metro/modules/View.jsm +++ b/browser/metro/modules/View.jsm @@ -27,18 +27,18 @@ function makeURI(aURL, aOriginCharset, aBaseURI) { function View(aSet) { this._set = aSet; this._set.controller = this; + this._window = aSet.ownerDocument.defaultView; + + this.onResize = () => this._adjustDOMforViewState(); + this._window.addEventListener("resize", this.onResize); - this.viewStateObserver = { - observe: (aSubject, aTopic, aData) => this._adjustDOMforViewState(aData) - }; - Services.obs.addObserver(this.viewStateObserver, "metro_viewstate_changed", false); ColorUtils.init(); this._adjustDOMforViewState(); } View.prototype = { destruct: function () { - Services.obs.removeObserver(this.viewStateObserver, "metro_viewstate_changed"); + this._window.removeEventListener("resize", this.onResize); }, _adjustDOMforViewState: function _adjustDOMforViewState(aState) { From 1c07861355a461a2afeb25254a189703403ac691 Mon Sep 17 00:00:00 2001 From: Sam Foster Date: Tue, 10 Dec 2013 15:15:14 -0800 Subject: [PATCH 09/58] Bug 915499 - Fix image loading/rendering logic in circular-progress-indicator binding. r=rsilveira --- .../content/bindings/circularprogress.xml | 54 +++++++++++-------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/browser/metro/base/content/bindings/circularprogress.xml b/browser/metro/base/content/bindings/circularprogress.xml index 77bab785ae66..7e71ee23b20b 100644 --- a/browser/metro/base/content/bindings/circularprogress.xml +++ b/browser/metro/base/content/bindings/circularprogress.xml @@ -20,6 +20,7 @@ + document.getAnonymousElementByAttribute(this, "anonid", "progressRing"); @@ -29,7 +30,6 @@ @@ -44,31 +44,35 @@ let startAngle = 1.5 * Math.PI; let endAngle = startAngle + (2 * Math.PI * (percentComplete / 100)); - let ctx = this._progressCircleCtx; - ctx.clearRect(0, 0, - this._progressCanvas.width, this._progressCanvas.height); - - // Save the state, so we can undo the clipping - ctx.save(); - - ctx.beginPath(); - let center = this._progressCanvas.width / 2; - ctx.arc(center, center, center, startAngle, endAngle, false); - ctx.lineTo(center, center); - ctx.closePath(); - ctx.clip(); - - // Draw circle image. - if (this._img && this._img.src) { - ctx.drawImage(this._img, 0, 0); - } else { - this._img.onload = function() { - ctx.drawImage(this._img, 0, 0); - }.bind(this); + if (!this._img) { + this._img = new Image(); + this._img.onload = () => { + this.updateProgress(this.getAttribute("progress")) + } this._img.src = PROGRESS_RING_IMG; } + else if (this._img.complete) { + let ctx = this._progressCircleCtx; + ctx.clearRect(0, 0, + this._progressCanvas.width, this._progressCanvas.height); - ctx.restore(); + // Save the state, so we can undo the clipping + ctx.save(); + + ctx.beginPath(); + let center = this._progressCanvas.width / 2; + ctx.arc(center, center, center, startAngle, endAngle, false); + ctx.lineTo(center, center); + ctx.closePath(); + ctx.clip(); + + // Draw circle image. + ctx.drawImage(this._img, 0, 0); + + ctx.restore(); + } else { + // Image is still loading + } return [startAngle, endAngle]; ]]> @@ -76,6 +80,10 @@ {}; + } this._progressCircleCtx.clearRect(0, 0, this._progressCanvas.width, this._progressCanvas.height); this.removeAttribute("progress"); From a2fe6df6b72d1fed6337c4eba390a939296e54b5 Mon Sep 17 00:00:00 2001 From: Mike Conley Date: Tue, 10 Dec 2013 18:26:37 -0500 Subject: [PATCH 10/58] Bug 944114 follow-up - pass MouseEvent.button instead of the MouseEvent to _countMouseUpEvent. r=me. --- browser/modules/BrowserUITelemetry.jsm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/browser/modules/BrowserUITelemetry.jsm b/browser/modules/BrowserUITelemetry.jsm index 4edd53cbd231..d1469418b0e9 100644 --- a/browser/modules/BrowserUITelemetry.jsm +++ b/browser/modules/BrowserUITelemetry.jsm @@ -86,9 +86,9 @@ this.BrowserUITelemetry = { }, _countableEvents: {}, - _countMouseUpEvent: function(aCategory, aAction, aMouseUpEvent) { + _countMouseUpEvent: function(aCategory, aAction, aButton) { const BUTTONS = ["left", "middle", "right"]; - let buttonKey = BUTTONS[aMouseUpEvent.button]; + let buttonKey = BUTTONS[aButton]; if (buttonKey) { let countObject = this._ensureObjectChain([aCategory, aAction, buttonKey], 0); @@ -134,7 +134,7 @@ this.BrowserUITelemetry = { if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) { // Base case - we clicked directly on one of our built-in items, // and we can go ahead and register that click. - this._countMouseUpEvent("click-builtin-item", item.id, aEvent); + this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button); } }, From feab2c0374ce9493b5409c828723bcff9b2f28c0 Mon Sep 17 00:00:00 2001 From: Jared Wein Date: Tue, 10 Dec 2013 17:50:08 -0500 Subject: [PATCH 11/58] Bug 901207 - Cmd/Ctrl+K should open menu panel when search bar was moved there. r=Gijs --HG-- extra : rebase_source : bde48e905cb3b2eb12e148efb8867b52f23f8610 --- browser/base/content/browser.js | 34 +++++- .../customizableui/content/panelUI.js | 15 ++- .../customizableui/src/CustomizableUI.jsm | 22 ++++ .../customizableui/test/browser.ini | 1 + .../test/browser_901207_searchbar_in_panel.js | 100 ++++++++++++++++++ .../components/customizableui/test/head.js | 26 ++++- 6 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 browser/components/customizableui/test/browser_901207_searchbar_in_panel.js diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 95e950a271e9..649bb5091852 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2929,13 +2929,37 @@ const BrowserSearch = { return; } #endif - var searchBar = this.searchBar; - if (searchBar && window.fullScreen) + let openSearchPageIfFieldIsNotActive = function(aSearchBar) { + if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) + openUILinkIn(Services.search.defaultEngine.searchForm, "current"); + }; + + let searchBar = this.searchBar; + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + if (placement && placement.area == CustomizableUI.AREA_PANEL) { + PanelUI.show().then(() => { + // The panel is not constructed until the first time it is shown. + searchBar = this.searchBar; + searchBar.select(); + openSearchPageIfFieldIsNotActive(searchBar); + }); + return; + } else if (placement.area == CustomizableUI.AREA_NAVBAR && searchBar && + searchBar.parentNode.classList.contains("overflowedItem")) { + let navBar = document.getElementById(CustomizableUI.AREA_NAVBAR); + navBar.overflowable.show().then(() => { + // The searchBar gets moved when the overflow panel opens. + searchBar = this.searchBar; + searchBar.select(); + openSearchPageIfFieldIsNotActive(searchBar); + }); + return; + } + if (searchBar && window.fullScreen) { FullScreen.mouseoverToggle(true); - if (searchBar) searchBar.select(); - if (!searchBar || document.activeElement != searchBar.textbox.inputField) - openUILinkIn(Services.search.defaultEngine.searchForm, "current"); + } + openSearchPageIfFieldIsNotActive(searchBar); }, /** diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js index 89122282d19f..70f020e86d1f 100644 --- a/browser/components/customizableui/content/panelUI.js +++ b/browser/components/customizableui/content/panelUI.js @@ -6,6 +6,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler", "resource:///modules/ScrollbarSampler.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); /** * Maintains the state and dispatches events for the main menu panel. */ @@ -116,9 +118,11 @@ const PanelUI = { * @param aEvent the event (if any) that triggers showing the menu. */ show: function(aEvent) { - if (this.panel.state == "open" || this.panel.state == "showing" || + let deferred = Promise.defer(); + if (this.panel.state == "open" || document.documentElement.hasAttribute("customizing")) { - return; + deferred.resolve(); + return deferred.promise; } this.ensureReady().then(() => { @@ -144,7 +148,14 @@ const PanelUI = { aEvent.sourceEvent.target.localName == "key"; this.panel.setAttribute("noautofocus", !keyboardOpened); this.panel.openPopup(iconAnchor || anchor, "bottomcenter topright"); + + this.panel.addEventListener("popupshown", function onPopupShown() { + this.removeEventListener("popupshown", onPopupShown); + deferred.resolve(); + }); }); + + return deferred.promise; }, /** diff --git a/browser/components/customizableui/src/CustomizableUI.jsm b/browser/components/customizableui/src/CustomizableUI.jsm index 43d6e7911747..de01bb7eb0ec 100644 --- a/browser/components/customizableui/src/CustomizableUI.jsm +++ b/browser/components/customizableui/src/CustomizableUI.jsm @@ -18,6 +18,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() { const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties"; return Services.strings.createBundle(kUrl); @@ -3108,6 +3110,26 @@ OverflowableToolbar.prototype = { } }, + show: function() { + let deferred = Promise.defer(); + if (this._panel.state == "open") { + deferred.resolve(); + return deferred.promise; + } + let doc = this._panel.ownerDocument; + this._panel.hidden = false; + let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon"); + this._panel.openPopup(anchor || this._chevron, "bottomcenter topright"); + this._chevron.open = true; + + this._panel.addEventListener("popupshown", function onPopupShown() { + this.removeEventListener("popupshown", onPopupShown); + deferred.resolve(); + }); + + return deferred.promise; + }, + _onClickChevron: function(aEvent) { if (this._chevron.open) this._panel.hidePopup(); diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini index b40fab717906..15bb529953f6 100644 --- a/browser/components/customizableui/test/browser.ini +++ b/browser/components/customizableui/test/browser.ini @@ -20,6 +20,7 @@ support-files = [browser_892955_isWidgetRemovable_for_removed_widgets.js] [browser_892956_destroyWidget_defaultPlacements.js] [browser_909779_overflow_toolbars_new_window.js] +[browser_901207_searchbar_in_panel.js] [browser_913972_currentset_overflow.js] [browser_914138_widget_API_overflowable_toolbar.js] diff --git a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js new file mode 100644 index 000000000000..ff5a358855bb --- /dev/null +++ b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +let gTests = [ + { + desc: "Ctrl+K should open the menu panel and focus the search bar if the search bar is in the panel.", + run: function() { + let searchbar = document.getElementById("searchbar"); + gCustomizeMode.addToPanel(searchbar); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); + + let shownPanelPromise = promisePanelShown(window); + EventUtils.synthesizeKey("k", { ctrlKey: true }); + yield shownPanelPromise; + + logActiveElement(); + is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); + + let hiddenPanelPromise = promisePanelHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + }, + teardown: function() { + CustomizableUI.reset(); + } + }, + { + desc: "Ctrl+K should give focus to the searchbar when the searchbar is in the menupanel and the panel is already opened.", + run: function() { + let searchbar = document.getElementById("searchbar"); + gCustomizeMode.addToPanel(searchbar); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); + + let shownPanelPromise = promisePanelShown(window); + PanelUI.toggle({type: "command"}); + yield shownPanelPromise; + + EventUtils.synthesizeKey("k", { ctrlKey: true }); + logActiveElement(); + is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); + + let hiddenPanelPromise = promisePanelHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + }, + teardown: function() { + CustomizableUI.reset(); + } + }, + { + desc: "Ctrl+K should open the overflow panel and focus the search bar if the search bar is overflowed.", + setup: function() { + this.originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + + window.resizeTo(480, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(!navbar.querySelector("#search-container"), "Search container should be overflowing"); + }, + run: function() { + let searchbar = document.getElementById("searchbar"); + + let shownPanelPromise = promiseOverflowShown(window); + EventUtils.synthesizeKey("k", { ctrlKey: true }); + yield shownPanelPromise; + + let chevron = document.getElementById("nav-bar-overflow-button"); + yield waitForCondition(function() chevron.open); + logActiveElement(); + is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); + + let hiddenPanelPromise = promiseOverflowHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + }, + teardown: function() { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + window.resizeTo(this.originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar."); + } + }, +]; + +function test() { + waitForExplicitFinish(); + runTests(gTests); +} + +function logActiveElement() { + let element = document.activeElement; + info("Active element: " + element ? + element + " (" + element.localName + "#" + element.id + "." + [...element.classList].join(".") + ")" : + "null"); +} diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js index 1de61e07fc21..63be435bd30a 100644 --- a/browser/components/customizableui/test/head.js +++ b/browser/components/customizableui/test/head.js @@ -196,31 +196,49 @@ function openAndLoadWindow(aOptions, aWaitForDelayedStartup=false) { function promisePanelShown(win) { let panelEl = win.PanelUI.panel; + return promisePanelElementShown(win, panelEl); +} + +function promiseOverflowShown(win) { + let panelEl = win.document.getElementById("widget-overflow"); + return promisePanelElementShown(win, panelEl); +} + +function promisePanelElementShown(win, aPanel) { let deferred = Promise.defer(); let timeoutId = win.setTimeout(() => { deferred.reject("Panel did not show within 20 seconds."); }, 20000); function onPanelOpen(e) { - panelEl.removeEventListener("popupshown", onPanelOpen); + aPanel.removeEventListener("popupshown", onPanelOpen); win.clearTimeout(timeoutId); deferred.resolve(); }; - panelEl.addEventListener("popupshown", onPanelOpen); + aPanel.addEventListener("popupshown", onPanelOpen); return deferred.promise; } function promisePanelHidden(win) { let panelEl = win.PanelUI.panel; + return promisePanelElementHidden(win, panelEl); +} + +function promiseOverflowHidden(win) { + let panelEl = document.getElementById("widget-overflow"); + return promisePanelElementHidden(win, panelEl); +} + +function promisePanelElementHidden(win, aPanel) { let deferred = Promise.defer(); let timeoutId = win.setTimeout(() => { deferred.reject("Panel did not hide within 20 seconds."); }, 20000); function onPanelClose(e) { - panelEl.removeEventListener("popuphidden", onPanelClose); + aPanel.removeEventListener("popuphidden", onPanelClose); win.clearTimeout(timeoutId); deferred.resolve(); } - panelEl.addEventListener("popuphidden", onPanelClose); + aPanel.addEventListener("popuphidden", onPanelClose); return deferred.promise; } From 240140ddbd8f78a6e55ef8c3a4efbb5b3707aa49 Mon Sep 17 00:00:00 2001 From: Mike Conley Date: Tue, 10 Dec 2013 18:43:39 -0500 Subject: [PATCH 12/58] Backed out changeset 3332e2f1ede3 due to missing Australis in commit message. --- browser/base/content/browser.js | 34 +----- .../customizableui/content/panelUI.js | 15 +-- .../customizableui/src/CustomizableUI.jsm | 22 ---- .../customizableui/test/browser.ini | 1 - .../test/browser_901207_searchbar_in_panel.js | 100 ------------------ .../components/customizableui/test/head.js | 26 +---- 6 files changed, 11 insertions(+), 187 deletions(-) delete mode 100644 browser/components/customizableui/test/browser_901207_searchbar_in_panel.js diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 649bb5091852..95e950a271e9 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2929,37 +2929,13 @@ const BrowserSearch = { return; } #endif - let openSearchPageIfFieldIsNotActive = function(aSearchBar) { - if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) - openUILinkIn(Services.search.defaultEngine.searchForm, "current"); - }; - - let searchBar = this.searchBar; - let placement = CustomizableUI.getPlacementOfWidget("search-container"); - if (placement && placement.area == CustomizableUI.AREA_PANEL) { - PanelUI.show().then(() => { - // The panel is not constructed until the first time it is shown. - searchBar = this.searchBar; - searchBar.select(); - openSearchPageIfFieldIsNotActive(searchBar); - }); - return; - } else if (placement.area == CustomizableUI.AREA_NAVBAR && searchBar && - searchBar.parentNode.classList.contains("overflowedItem")) { - let navBar = document.getElementById(CustomizableUI.AREA_NAVBAR); - navBar.overflowable.show().then(() => { - // The searchBar gets moved when the overflow panel opens. - searchBar = this.searchBar; - searchBar.select(); - openSearchPageIfFieldIsNotActive(searchBar); - }); - return; - } - if (searchBar && window.fullScreen) { + var searchBar = this.searchBar; + if (searchBar && window.fullScreen) FullScreen.mouseoverToggle(true); + if (searchBar) searchBar.select(); - } - openSearchPageIfFieldIsNotActive(searchBar); + if (!searchBar || document.activeElement != searchBar.textbox.inputField) + openUILinkIn(Services.search.defaultEngine.searchForm, "current"); }, /** diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js index 70f020e86d1f..89122282d19f 100644 --- a/browser/components/customizableui/content/panelUI.js +++ b/browser/components/customizableui/content/panelUI.js @@ -6,8 +6,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler", "resource:///modules/ScrollbarSampler.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); /** * Maintains the state and dispatches events for the main menu panel. */ @@ -118,11 +116,9 @@ const PanelUI = { * @param aEvent the event (if any) that triggers showing the menu. */ show: function(aEvent) { - let deferred = Promise.defer(); - if (this.panel.state == "open" || + if (this.panel.state == "open" || this.panel.state == "showing" || document.documentElement.hasAttribute("customizing")) { - deferred.resolve(); - return deferred.promise; + return; } this.ensureReady().then(() => { @@ -148,14 +144,7 @@ const PanelUI = { aEvent.sourceEvent.target.localName == "key"; this.panel.setAttribute("noautofocus", !keyboardOpened); this.panel.openPopup(iconAnchor || anchor, "bottomcenter topright"); - - this.panel.addEventListener("popupshown", function onPopupShown() { - this.removeEventListener("popupshown", onPopupShown); - deferred.resolve(); - }); }); - - return deferred.promise; }, /** diff --git a/browser/components/customizableui/src/CustomizableUI.jsm b/browser/components/customizableui/src/CustomizableUI.jsm index de01bb7eb0ec..43d6e7911747 100644 --- a/browser/components/customizableui/src/CustomizableUI.jsm +++ b/browser/components/customizableui/src/CustomizableUI.jsm @@ -18,8 +18,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() { const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties"; return Services.strings.createBundle(kUrl); @@ -3110,26 +3108,6 @@ OverflowableToolbar.prototype = { } }, - show: function() { - let deferred = Promise.defer(); - if (this._panel.state == "open") { - deferred.resolve(); - return deferred.promise; - } - let doc = this._panel.ownerDocument; - this._panel.hidden = false; - let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon"); - this._panel.openPopup(anchor || this._chevron, "bottomcenter topright"); - this._chevron.open = true; - - this._panel.addEventListener("popupshown", function onPopupShown() { - this.removeEventListener("popupshown", onPopupShown); - deferred.resolve(); - }); - - return deferred.promise; - }, - _onClickChevron: function(aEvent) { if (this._chevron.open) this._panel.hidePopup(); diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini index 15bb529953f6..b40fab717906 100644 --- a/browser/components/customizableui/test/browser.ini +++ b/browser/components/customizableui/test/browser.ini @@ -20,7 +20,6 @@ support-files = [browser_892955_isWidgetRemovable_for_removed_widgets.js] [browser_892956_destroyWidget_defaultPlacements.js] [browser_909779_overflow_toolbars_new_window.js] -[browser_901207_searchbar_in_panel.js] [browser_913972_currentset_overflow.js] [browser_914138_widget_API_overflowable_toolbar.js] diff --git a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js deleted file mode 100644 index ff5a358855bb..000000000000 --- a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js +++ /dev/null @@ -1,100 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * 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/. */ - -let gTests = [ - { - desc: "Ctrl+K should open the menu panel and focus the search bar if the search bar is in the panel.", - run: function() { - let searchbar = document.getElementById("searchbar"); - gCustomizeMode.addToPanel(searchbar); - let placement = CustomizableUI.getPlacementOfWidget("search-container"); - is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); - - let shownPanelPromise = promisePanelShown(window); - EventUtils.synthesizeKey("k", { ctrlKey: true }); - yield shownPanelPromise; - - logActiveElement(); - is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); - - let hiddenPanelPromise = promisePanelHidden(window); - EventUtils.synthesizeKey("VK_ESCAPE", {}); - yield hiddenPanelPromise; - }, - teardown: function() { - CustomizableUI.reset(); - } - }, - { - desc: "Ctrl+K should give focus to the searchbar when the searchbar is in the menupanel and the panel is already opened.", - run: function() { - let searchbar = document.getElementById("searchbar"); - gCustomizeMode.addToPanel(searchbar); - let placement = CustomizableUI.getPlacementOfWidget("search-container"); - is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); - - let shownPanelPromise = promisePanelShown(window); - PanelUI.toggle({type: "command"}); - yield shownPanelPromise; - - EventUtils.synthesizeKey("k", { ctrlKey: true }); - logActiveElement(); - is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); - - let hiddenPanelPromise = promisePanelHidden(window); - EventUtils.synthesizeKey("VK_ESCAPE", {}); - yield hiddenPanelPromise; - }, - teardown: function() { - CustomizableUI.reset(); - } - }, - { - desc: "Ctrl+K should open the overflow panel and focus the search bar if the search bar is overflowed.", - setup: function() { - this.originalWindowWidth = window.outerWidth; - let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); - ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); - ok(CustomizableUI.inDefaultState, "Should start in default state."); - - window.resizeTo(480, window.outerHeight); - yield waitForCondition(() => navbar.hasAttribute("overflowing")); - ok(!navbar.querySelector("#search-container"), "Search container should be overflowing"); - }, - run: function() { - let searchbar = document.getElementById("searchbar"); - - let shownPanelPromise = promiseOverflowShown(window); - EventUtils.synthesizeKey("k", { ctrlKey: true }); - yield shownPanelPromise; - - let chevron = document.getElementById("nav-bar-overflow-button"); - yield waitForCondition(function() chevron.open); - logActiveElement(); - is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); - - let hiddenPanelPromise = promiseOverflowHidden(window); - EventUtils.synthesizeKey("VK_ESCAPE", {}); - yield hiddenPanelPromise; - }, - teardown: function() { - let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); - window.resizeTo(this.originalWindowWidth, window.outerHeight); - yield waitForCondition(() => !navbar.hasAttribute("overflowing")); - ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar."); - } - }, -]; - -function test() { - waitForExplicitFinish(); - runTests(gTests); -} - -function logActiveElement() { - let element = document.activeElement; - info("Active element: " + element ? - element + " (" + element.localName + "#" + element.id + "." + [...element.classList].join(".") + ")" : - "null"); -} diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js index 63be435bd30a..1de61e07fc21 100644 --- a/browser/components/customizableui/test/head.js +++ b/browser/components/customizableui/test/head.js @@ -196,49 +196,31 @@ function openAndLoadWindow(aOptions, aWaitForDelayedStartup=false) { function promisePanelShown(win) { let panelEl = win.PanelUI.panel; - return promisePanelElementShown(win, panelEl); -} - -function promiseOverflowShown(win) { - let panelEl = win.document.getElementById("widget-overflow"); - return promisePanelElementShown(win, panelEl); -} - -function promisePanelElementShown(win, aPanel) { let deferred = Promise.defer(); let timeoutId = win.setTimeout(() => { deferred.reject("Panel did not show within 20 seconds."); }, 20000); function onPanelOpen(e) { - aPanel.removeEventListener("popupshown", onPanelOpen); + panelEl.removeEventListener("popupshown", onPanelOpen); win.clearTimeout(timeoutId); deferred.resolve(); }; - aPanel.addEventListener("popupshown", onPanelOpen); + panelEl.addEventListener("popupshown", onPanelOpen); return deferred.promise; } function promisePanelHidden(win) { let panelEl = win.PanelUI.panel; - return promisePanelElementHidden(win, panelEl); -} - -function promiseOverflowHidden(win) { - let panelEl = document.getElementById("widget-overflow"); - return promisePanelElementHidden(win, panelEl); -} - -function promisePanelElementHidden(win, aPanel) { let deferred = Promise.defer(); let timeoutId = win.setTimeout(() => { deferred.reject("Panel did not hide within 20 seconds."); }, 20000); function onPanelClose(e) { - aPanel.removeEventListener("popuphidden", onPanelClose); + panelEl.removeEventListener("popuphidden", onPanelClose); win.clearTimeout(timeoutId); deferred.resolve(); } - aPanel.addEventListener("popuphidden", onPanelClose); + panelEl.addEventListener("popuphidden", onPanelClose); return deferred.promise; } From 20b2193738cb1de08854b62baf3433a9f86fbdd7 Mon Sep 17 00:00:00 2001 From: Mike Conley Date: Tue, 10 Dec 2013 18:44:51 -0500 Subject: [PATCH 13/58] Backed out changeset c083d7a68fd2 due to missing Australis in commit message. --- browser/modules/BrowserUITelemetry.jsm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/browser/modules/BrowserUITelemetry.jsm b/browser/modules/BrowserUITelemetry.jsm index d1469418b0e9..4edd53cbd231 100644 --- a/browser/modules/BrowserUITelemetry.jsm +++ b/browser/modules/BrowserUITelemetry.jsm @@ -86,9 +86,9 @@ this.BrowserUITelemetry = { }, _countableEvents: {}, - _countMouseUpEvent: function(aCategory, aAction, aButton) { + _countMouseUpEvent: function(aCategory, aAction, aMouseUpEvent) { const BUTTONS = ["left", "middle", "right"]; - let buttonKey = BUTTONS[aButton]; + let buttonKey = BUTTONS[aMouseUpEvent.button]; if (buttonKey) { let countObject = this._ensureObjectChain([aCategory, aAction, buttonKey], 0); @@ -134,7 +134,7 @@ this.BrowserUITelemetry = { if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) { // Base case - we clicked directly on one of our built-in items, // and we can go ahead and register that click. - this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button); + this._countMouseUpEvent("click-builtin-item", item.id, aEvent); } }, From 109e913c41f112cd912d1e19b15460c2bb20e0b0 Mon Sep 17 00:00:00 2001 From: Mike Conley Date: Tue, 10 Dec 2013 18:26:37 -0500 Subject: [PATCH 14/58] Bug 944114 [Australis] follow-up - pass MouseEvent.button instead of the MouseEvent to _countMouseUpEvent. r=me. --- browser/modules/BrowserUITelemetry.jsm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/browser/modules/BrowserUITelemetry.jsm b/browser/modules/BrowserUITelemetry.jsm index 4edd53cbd231..d1469418b0e9 100644 --- a/browser/modules/BrowserUITelemetry.jsm +++ b/browser/modules/BrowserUITelemetry.jsm @@ -86,9 +86,9 @@ this.BrowserUITelemetry = { }, _countableEvents: {}, - _countMouseUpEvent: function(aCategory, aAction, aMouseUpEvent) { + _countMouseUpEvent: function(aCategory, aAction, aButton) { const BUTTONS = ["left", "middle", "right"]; - let buttonKey = BUTTONS[aMouseUpEvent.button]; + let buttonKey = BUTTONS[aButton]; if (buttonKey) { let countObject = this._ensureObjectChain([aCategory, aAction, buttonKey], 0); @@ -134,7 +134,7 @@ this.BrowserUITelemetry = { if (ALL_BUILTIN_ITEMS.indexOf(item.id) != -1) { // Base case - we clicked directly on one of our built-in items, // and we can go ahead and register that click. - this._countMouseUpEvent("click-builtin-item", item.id, aEvent); + this._countMouseUpEvent("click-builtin-item", item.id, aEvent.button); } }, From e5b223c2ef1f67ad85eeec097ee8c23e35cbc9c6 Mon Sep 17 00:00:00 2001 From: Jared Wein Date: Tue, 10 Dec 2013 17:50:08 -0500 Subject: [PATCH 15/58] Bug 901207 - [Australis] Cmd/Ctrl+K should open menu panel when search bar was moved there. r=Gijs DONTBUILD --- browser/base/content/browser.js | 34 +++++- .../customizableui/content/panelUI.js | 15 ++- .../customizableui/src/CustomizableUI.jsm | 22 ++++ .../customizableui/test/browser.ini | 1 + .../test/browser_901207_searchbar_in_panel.js | 100 ++++++++++++++++++ .../components/customizableui/test/head.js | 26 ++++- 6 files changed, 187 insertions(+), 11 deletions(-) create mode 100644 browser/components/customizableui/test/browser_901207_searchbar_in_panel.js diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 95e950a271e9..649bb5091852 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2929,13 +2929,37 @@ const BrowserSearch = { return; } #endif - var searchBar = this.searchBar; - if (searchBar && window.fullScreen) + let openSearchPageIfFieldIsNotActive = function(aSearchBar) { + if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) + openUILinkIn(Services.search.defaultEngine.searchForm, "current"); + }; + + let searchBar = this.searchBar; + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + if (placement && placement.area == CustomizableUI.AREA_PANEL) { + PanelUI.show().then(() => { + // The panel is not constructed until the first time it is shown. + searchBar = this.searchBar; + searchBar.select(); + openSearchPageIfFieldIsNotActive(searchBar); + }); + return; + } else if (placement.area == CustomizableUI.AREA_NAVBAR && searchBar && + searchBar.parentNode.classList.contains("overflowedItem")) { + let navBar = document.getElementById(CustomizableUI.AREA_NAVBAR); + navBar.overflowable.show().then(() => { + // The searchBar gets moved when the overflow panel opens. + searchBar = this.searchBar; + searchBar.select(); + openSearchPageIfFieldIsNotActive(searchBar); + }); + return; + } + if (searchBar && window.fullScreen) { FullScreen.mouseoverToggle(true); - if (searchBar) searchBar.select(); - if (!searchBar || document.activeElement != searchBar.textbox.inputField) - openUILinkIn(Services.search.defaultEngine.searchForm, "current"); + } + openSearchPageIfFieldIsNotActive(searchBar); }, /** diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js index 89122282d19f..70f020e86d1f 100644 --- a/browser/components/customizableui/content/panelUI.js +++ b/browser/components/customizableui/content/panelUI.js @@ -6,6 +6,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler", "resource:///modules/ScrollbarSampler.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); /** * Maintains the state and dispatches events for the main menu panel. */ @@ -116,9 +118,11 @@ const PanelUI = { * @param aEvent the event (if any) that triggers showing the menu. */ show: function(aEvent) { - if (this.panel.state == "open" || this.panel.state == "showing" || + let deferred = Promise.defer(); + if (this.panel.state == "open" || document.documentElement.hasAttribute("customizing")) { - return; + deferred.resolve(); + return deferred.promise; } this.ensureReady().then(() => { @@ -144,7 +148,14 @@ const PanelUI = { aEvent.sourceEvent.target.localName == "key"; this.panel.setAttribute("noautofocus", !keyboardOpened); this.panel.openPopup(iconAnchor || anchor, "bottomcenter topright"); + + this.panel.addEventListener("popupshown", function onPopupShown() { + this.removeEventListener("popupshown", onPopupShown); + deferred.resolve(); + }); }); + + return deferred.promise; }, /** diff --git a/browser/components/customizableui/src/CustomizableUI.jsm b/browser/components/customizableui/src/CustomizableUI.jsm index 43d6e7911747..de01bb7eb0ec 100644 --- a/browser/components/customizableui/src/CustomizableUI.jsm +++ b/browser/components/customizableui/src/CustomizableUI.jsm @@ -18,6 +18,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Promise", + "resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() { const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties"; return Services.strings.createBundle(kUrl); @@ -3108,6 +3110,26 @@ OverflowableToolbar.prototype = { } }, + show: function() { + let deferred = Promise.defer(); + if (this._panel.state == "open") { + deferred.resolve(); + return deferred.promise; + } + let doc = this._panel.ownerDocument; + this._panel.hidden = false; + let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon"); + this._panel.openPopup(anchor || this._chevron, "bottomcenter topright"); + this._chevron.open = true; + + this._panel.addEventListener("popupshown", function onPopupShown() { + this.removeEventListener("popupshown", onPopupShown); + deferred.resolve(); + }); + + return deferred.promise; + }, + _onClickChevron: function(aEvent) { if (this._chevron.open) this._panel.hidePopup(); diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini index b40fab717906..15bb529953f6 100644 --- a/browser/components/customizableui/test/browser.ini +++ b/browser/components/customizableui/test/browser.ini @@ -20,6 +20,7 @@ support-files = [browser_892955_isWidgetRemovable_for_removed_widgets.js] [browser_892956_destroyWidget_defaultPlacements.js] [browser_909779_overflow_toolbars_new_window.js] +[browser_901207_searchbar_in_panel.js] [browser_913972_currentset_overflow.js] [browser_914138_widget_API_overflowable_toolbar.js] diff --git a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js new file mode 100644 index 000000000000..ff5a358855bb --- /dev/null +++ b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js @@ -0,0 +1,100 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +let gTests = [ + { + desc: "Ctrl+K should open the menu panel and focus the search bar if the search bar is in the panel.", + run: function() { + let searchbar = document.getElementById("searchbar"); + gCustomizeMode.addToPanel(searchbar); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); + + let shownPanelPromise = promisePanelShown(window); + EventUtils.synthesizeKey("k", { ctrlKey: true }); + yield shownPanelPromise; + + logActiveElement(); + is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); + + let hiddenPanelPromise = promisePanelHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + }, + teardown: function() { + CustomizableUI.reset(); + } + }, + { + desc: "Ctrl+K should give focus to the searchbar when the searchbar is in the menupanel and the panel is already opened.", + run: function() { + let searchbar = document.getElementById("searchbar"); + gCustomizeMode.addToPanel(searchbar); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); + + let shownPanelPromise = promisePanelShown(window); + PanelUI.toggle({type: "command"}); + yield shownPanelPromise; + + EventUtils.synthesizeKey("k", { ctrlKey: true }); + logActiveElement(); + is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); + + let hiddenPanelPromise = promisePanelHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + }, + teardown: function() { + CustomizableUI.reset(); + } + }, + { + desc: "Ctrl+K should open the overflow panel and focus the search bar if the search bar is overflowed.", + setup: function() { + this.originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + + window.resizeTo(480, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(!navbar.querySelector("#search-container"), "Search container should be overflowing"); + }, + run: function() { + let searchbar = document.getElementById("searchbar"); + + let shownPanelPromise = promiseOverflowShown(window); + EventUtils.synthesizeKey("k", { ctrlKey: true }); + yield shownPanelPromise; + + let chevron = document.getElementById("nav-bar-overflow-button"); + yield waitForCondition(function() chevron.open); + logActiveElement(); + is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); + + let hiddenPanelPromise = promiseOverflowHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + }, + teardown: function() { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + window.resizeTo(this.originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar."); + } + }, +]; + +function test() { + waitForExplicitFinish(); + runTests(gTests); +} + +function logActiveElement() { + let element = document.activeElement; + info("Active element: " + element ? + element + " (" + element.localName + "#" + element.id + "." + [...element.classList].join(".") + ")" : + "null"); +} diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js index 1de61e07fc21..63be435bd30a 100644 --- a/browser/components/customizableui/test/head.js +++ b/browser/components/customizableui/test/head.js @@ -196,31 +196,49 @@ function openAndLoadWindow(aOptions, aWaitForDelayedStartup=false) { function promisePanelShown(win) { let panelEl = win.PanelUI.panel; + return promisePanelElementShown(win, panelEl); +} + +function promiseOverflowShown(win) { + let panelEl = win.document.getElementById("widget-overflow"); + return promisePanelElementShown(win, panelEl); +} + +function promisePanelElementShown(win, aPanel) { let deferred = Promise.defer(); let timeoutId = win.setTimeout(() => { deferred.reject("Panel did not show within 20 seconds."); }, 20000); function onPanelOpen(e) { - panelEl.removeEventListener("popupshown", onPanelOpen); + aPanel.removeEventListener("popupshown", onPanelOpen); win.clearTimeout(timeoutId); deferred.resolve(); }; - panelEl.addEventListener("popupshown", onPanelOpen); + aPanel.addEventListener("popupshown", onPanelOpen); return deferred.promise; } function promisePanelHidden(win) { let panelEl = win.PanelUI.panel; + return promisePanelElementHidden(win, panelEl); +} + +function promiseOverflowHidden(win) { + let panelEl = document.getElementById("widget-overflow"); + return promisePanelElementHidden(win, panelEl); +} + +function promisePanelElementHidden(win, aPanel) { let deferred = Promise.defer(); let timeoutId = win.setTimeout(() => { deferred.reject("Panel did not hide within 20 seconds."); }, 20000); function onPanelClose(e) { - panelEl.removeEventListener("popuphidden", onPanelClose); + aPanel.removeEventListener("popuphidden", onPanelClose); win.clearTimeout(timeoutId); deferred.resolve(); } - panelEl.addEventListener("popuphidden", onPanelClose); + aPanel.addEventListener("popuphidden", onPanelClose); return deferred.promise; } From 7db3725419fbb0d20d77e9762af0b8da2a38d078 Mon Sep 17 00:00:00 2001 From: Wes Kocher Date: Tue, 10 Dec 2013 17:07:39 -0800 Subject: [PATCH 16/58] Backed out changeset 371cd4b623de (bug 901207) for m-bc orange --- browser/base/content/browser.js | 34 +----- .../customizableui/content/panelUI.js | 15 +-- .../customizableui/src/CustomizableUI.jsm | 22 ---- .../customizableui/test/browser.ini | 1 - .../test/browser_901207_searchbar_in_panel.js | 100 ------------------ .../components/customizableui/test/head.js | 26 +---- 6 files changed, 11 insertions(+), 187 deletions(-) delete mode 100644 browser/components/customizableui/test/browser_901207_searchbar_in_panel.js diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 649bb5091852..95e950a271e9 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2929,37 +2929,13 @@ const BrowserSearch = { return; } #endif - let openSearchPageIfFieldIsNotActive = function(aSearchBar) { - if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) - openUILinkIn(Services.search.defaultEngine.searchForm, "current"); - }; - - let searchBar = this.searchBar; - let placement = CustomizableUI.getPlacementOfWidget("search-container"); - if (placement && placement.area == CustomizableUI.AREA_PANEL) { - PanelUI.show().then(() => { - // The panel is not constructed until the first time it is shown. - searchBar = this.searchBar; - searchBar.select(); - openSearchPageIfFieldIsNotActive(searchBar); - }); - return; - } else if (placement.area == CustomizableUI.AREA_NAVBAR && searchBar && - searchBar.parentNode.classList.contains("overflowedItem")) { - let navBar = document.getElementById(CustomizableUI.AREA_NAVBAR); - navBar.overflowable.show().then(() => { - // The searchBar gets moved when the overflow panel opens. - searchBar = this.searchBar; - searchBar.select(); - openSearchPageIfFieldIsNotActive(searchBar); - }); - return; - } - if (searchBar && window.fullScreen) { + var searchBar = this.searchBar; + if (searchBar && window.fullScreen) FullScreen.mouseoverToggle(true); + if (searchBar) searchBar.select(); - } - openSearchPageIfFieldIsNotActive(searchBar); + if (!searchBar || document.activeElement != searchBar.textbox.inputField) + openUILinkIn(Services.search.defaultEngine.searchForm, "current"); }, /** diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js index 70f020e86d1f..89122282d19f 100644 --- a/browser/components/customizableui/content/panelUI.js +++ b/browser/components/customizableui/content/panelUI.js @@ -6,8 +6,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", "resource:///modules/CustomizableUI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler", "resource:///modules/ScrollbarSampler.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); /** * Maintains the state and dispatches events for the main menu panel. */ @@ -118,11 +116,9 @@ const PanelUI = { * @param aEvent the event (if any) that triggers showing the menu. */ show: function(aEvent) { - let deferred = Promise.defer(); - if (this.panel.state == "open" || + if (this.panel.state == "open" || this.panel.state == "showing" || document.documentElement.hasAttribute("customizing")) { - deferred.resolve(); - return deferred.promise; + return; } this.ensureReady().then(() => { @@ -148,14 +144,7 @@ const PanelUI = { aEvent.sourceEvent.target.localName == "key"; this.panel.setAttribute("noautofocus", !keyboardOpened); this.panel.openPopup(iconAnchor || anchor, "bottomcenter topright"); - - this.panel.addEventListener("popupshown", function onPopupShown() { - this.removeEventListener("popupshown", onPopupShown); - deferred.resolve(); - }); }); - - return deferred.promise; }, /** diff --git a/browser/components/customizableui/src/CustomizableUI.jsm b/browser/components/customizableui/src/CustomizableUI.jsm index de01bb7eb0ec..43d6e7911747 100644 --- a/browser/components/customizableui/src/CustomizableUI.jsm +++ b/browser/components/customizableui/src/CustomizableUI.jsm @@ -18,8 +18,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "Promise", - "resource://gre/modules/Promise.jsm"); XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() { const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties"; return Services.strings.createBundle(kUrl); @@ -3110,26 +3108,6 @@ OverflowableToolbar.prototype = { } }, - show: function() { - let deferred = Promise.defer(); - if (this._panel.state == "open") { - deferred.resolve(); - return deferred.promise; - } - let doc = this._panel.ownerDocument; - this._panel.hidden = false; - let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon"); - this._panel.openPopup(anchor || this._chevron, "bottomcenter topright"); - this._chevron.open = true; - - this._panel.addEventListener("popupshown", function onPopupShown() { - this.removeEventListener("popupshown", onPopupShown); - deferred.resolve(); - }); - - return deferred.promise; - }, - _onClickChevron: function(aEvent) { if (this._chevron.open) this._panel.hidePopup(); diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini index 15bb529953f6..b40fab717906 100644 --- a/browser/components/customizableui/test/browser.ini +++ b/browser/components/customizableui/test/browser.ini @@ -20,7 +20,6 @@ support-files = [browser_892955_isWidgetRemovable_for_removed_widgets.js] [browser_892956_destroyWidget_defaultPlacements.js] [browser_909779_overflow_toolbars_new_window.js] -[browser_901207_searchbar_in_panel.js] [browser_913972_currentset_overflow.js] [browser_914138_widget_API_overflowable_toolbar.js] diff --git a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js deleted file mode 100644 index ff5a358855bb..000000000000 --- a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js +++ /dev/null @@ -1,100 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * 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/. */ - -let gTests = [ - { - desc: "Ctrl+K should open the menu panel and focus the search bar if the search bar is in the panel.", - run: function() { - let searchbar = document.getElementById("searchbar"); - gCustomizeMode.addToPanel(searchbar); - let placement = CustomizableUI.getPlacementOfWidget("search-container"); - is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); - - let shownPanelPromise = promisePanelShown(window); - EventUtils.synthesizeKey("k", { ctrlKey: true }); - yield shownPanelPromise; - - logActiveElement(); - is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); - - let hiddenPanelPromise = promisePanelHidden(window); - EventUtils.synthesizeKey("VK_ESCAPE", {}); - yield hiddenPanelPromise; - }, - teardown: function() { - CustomizableUI.reset(); - } - }, - { - desc: "Ctrl+K should give focus to the searchbar when the searchbar is in the menupanel and the panel is already opened.", - run: function() { - let searchbar = document.getElementById("searchbar"); - gCustomizeMode.addToPanel(searchbar); - let placement = CustomizableUI.getPlacementOfWidget("search-container"); - is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); - - let shownPanelPromise = promisePanelShown(window); - PanelUI.toggle({type: "command"}); - yield shownPanelPromise; - - EventUtils.synthesizeKey("k", { ctrlKey: true }); - logActiveElement(); - is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); - - let hiddenPanelPromise = promisePanelHidden(window); - EventUtils.synthesizeKey("VK_ESCAPE", {}); - yield hiddenPanelPromise; - }, - teardown: function() { - CustomizableUI.reset(); - } - }, - { - desc: "Ctrl+K should open the overflow panel and focus the search bar if the search bar is overflowed.", - setup: function() { - this.originalWindowWidth = window.outerWidth; - let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); - ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); - ok(CustomizableUI.inDefaultState, "Should start in default state."); - - window.resizeTo(480, window.outerHeight); - yield waitForCondition(() => navbar.hasAttribute("overflowing")); - ok(!navbar.querySelector("#search-container"), "Search container should be overflowing"); - }, - run: function() { - let searchbar = document.getElementById("searchbar"); - - let shownPanelPromise = promiseOverflowShown(window); - EventUtils.synthesizeKey("k", { ctrlKey: true }); - yield shownPanelPromise; - - let chevron = document.getElementById("nav-bar-overflow-button"); - yield waitForCondition(function() chevron.open); - logActiveElement(); - is(document.activeElement, searchbar.textbox.inputField, "The searchbar should be focused"); - - let hiddenPanelPromise = promiseOverflowHidden(window); - EventUtils.synthesizeKey("VK_ESCAPE", {}); - yield hiddenPanelPromise; - }, - teardown: function() { - let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); - window.resizeTo(this.originalWindowWidth, window.outerHeight); - yield waitForCondition(() => !navbar.hasAttribute("overflowing")); - ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar."); - } - }, -]; - -function test() { - waitForExplicitFinish(); - runTests(gTests); -} - -function logActiveElement() { - let element = document.activeElement; - info("Active element: " + element ? - element + " (" + element.localName + "#" + element.id + "." + [...element.classList].join(".") + ")" : - "null"); -} diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js index 63be435bd30a..1de61e07fc21 100644 --- a/browser/components/customizableui/test/head.js +++ b/browser/components/customizableui/test/head.js @@ -196,49 +196,31 @@ function openAndLoadWindow(aOptions, aWaitForDelayedStartup=false) { function promisePanelShown(win) { let panelEl = win.PanelUI.panel; - return promisePanelElementShown(win, panelEl); -} - -function promiseOverflowShown(win) { - let panelEl = win.document.getElementById("widget-overflow"); - return promisePanelElementShown(win, panelEl); -} - -function promisePanelElementShown(win, aPanel) { let deferred = Promise.defer(); let timeoutId = win.setTimeout(() => { deferred.reject("Panel did not show within 20 seconds."); }, 20000); function onPanelOpen(e) { - aPanel.removeEventListener("popupshown", onPanelOpen); + panelEl.removeEventListener("popupshown", onPanelOpen); win.clearTimeout(timeoutId); deferred.resolve(); }; - aPanel.addEventListener("popupshown", onPanelOpen); + panelEl.addEventListener("popupshown", onPanelOpen); return deferred.promise; } function promisePanelHidden(win) { let panelEl = win.PanelUI.panel; - return promisePanelElementHidden(win, panelEl); -} - -function promiseOverflowHidden(win) { - let panelEl = document.getElementById("widget-overflow"); - return promisePanelElementHidden(win, panelEl); -} - -function promisePanelElementHidden(win, aPanel) { let deferred = Promise.defer(); let timeoutId = win.setTimeout(() => { deferred.reject("Panel did not hide within 20 seconds."); }, 20000); function onPanelClose(e) { - aPanel.removeEventListener("popuphidden", onPanelClose); + panelEl.removeEventListener("popuphidden", onPanelClose); win.clearTimeout(timeoutId); deferred.resolve(); } - aPanel.addEventListener("popuphidden", onPanelClose); + panelEl.addEventListener("popuphidden", onPanelClose); return deferred.promise; } From 1d65b9038a7a4ac6ac00b16369b9d881fa25ee06 Mon Sep 17 00:00:00 2001 From: Heather Arthur Date: Fri, 6 Dec 2013 23:52:32 -0800 Subject: [PATCH 17/58] Bug 926014 - Support CSS source maps; r=dcamp --HG-- rename : browser/devtools/styleeditor/StyleEditorPanel.jsm => browser/devtools/styleeditor/styleeditor-panel.js --- browser/app/profile/firefox.js | 2 +- browser/devtools/main.js | 2 +- browser/devtools/styleeditor/Makefile.in | 4 +- .../styleeditor/StyleEditorDebuggee.jsm | 346 ------ .../devtools/styleeditor/StyleEditorUI.jsm | 240 ++-- .../devtools/styleeditor/StyleSheetEditor.jsm | 83 +- ...leEditorPanel.jsm => styleeditor-panel.js} | 23 +- browser/devtools/styleeditor/test/browser.ini | 5 + ...ser_styleeditor_bug_851132_middle_click.js | 7 +- .../test/browser_styleeditor_bug_870339.js | 8 +- .../test/browser_styleeditor_new.js | 2 +- .../test/browser_styleeditor_nostyle.js | 2 +- ...browser_styleeditor_private_perwindowpb.js | 7 +- .../test/browser_styleeditor_reload.js | 3 + .../test/browser_styleeditor_sourcemaps.js | 86 ++ browser/devtools/styleeditor/test/head.js | 36 - .../devtools/styleeditor/test/sourcemaps.css | 7 + .../styleeditor/test/sourcemaps.css.map | 7 + .../devtools/styleeditor/test/sourcemaps.html | 11 + .../devtools/styleeditor/test/sourcemaps.scss | 10 + .../devtools/styleinspector/computed-view.js | 62 +- .../styleinspector/computedview.xhtml | 1 + browser/devtools/styleinspector/rule-view.js | 37 + .../styleinspector/style-inspector.js | 42 +- .../devtools/styleinspector/test/browser.ini | 7 + ...owser_computedview_original_source_link.js | 119 ++ .../browser_ruleview_original_source_link.js | 112 ++ browser/devtools/styleinspector/test/head.js | 50 + .../styleinspector/test/sourcemaps.css | 7 + .../styleinspector/test/sourcemaps.css.map | 7 + .../styleinspector/test/sourcemaps.html | 11 + .../styleinspector/test/sourcemaps.scss | 10 + toolkit/devtools/server/actors/inspector.js | 2 +- toolkit/devtools/server/actors/styleeditor.js | 1047 +++++++++++------ toolkit/devtools/server/actors/styles.js | 112 +- toolkit/devtools/server/main.js | 4 +- 36 files changed, 1475 insertions(+), 1046 deletions(-) delete mode 100644 browser/devtools/styleeditor/StyleEditorDebuggee.jsm rename browser/devtools/styleeditor/{StyleEditorPanel.jsm => styleeditor-panel.js} (82%) create mode 100644 browser/devtools/styleeditor/test/browser_styleeditor_sourcemaps.js create mode 100644 browser/devtools/styleeditor/test/sourcemaps.css create mode 100644 browser/devtools/styleeditor/test/sourcemaps.css.map create mode 100644 browser/devtools/styleeditor/test/sourcemaps.html create mode 100644 browser/devtools/styleeditor/test/sourcemaps.scss create mode 100644 browser/devtools/styleinspector/test/browser_computedview_original_source_link.js create mode 100644 browser/devtools/styleinspector/test/browser_ruleview_original_source_link.js create mode 100644 browser/devtools/styleinspector/test/sourcemaps.css create mode 100644 browser/devtools/styleinspector/test/sourcemaps.css.map create mode 100644 browser/devtools/styleinspector/test/sourcemaps.html create mode 100644 browser/devtools/styleinspector/test/sourcemaps.scss diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index c734407b1072..b1168b6dc814 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1155,7 +1155,7 @@ pref("devtools.scratchpad.recentFilesMax", 10); // Enable the Style Editor. pref("devtools.styleeditor.enabled", true); -pref("devtools.styleeditor.transitions", true); +pref("devtools.styleeditor.source-maps-enabled", false); // Enable the Shader Editor. pref("devtools.shadereditor.enabled", false); diff --git a/browser/devtools/main.js b/browser/devtools/main.js index 02e37b54d9a5..e7925acf42b1 100644 --- a/browser/devtools/main.js +++ b/browser/devtools/main.js @@ -26,7 +26,7 @@ loader.lazyGetter(this, "OptionsPanel", () => require("devtools/framework/toolbo loader.lazyGetter(this, "InspectorPanel", () => require("devtools/inspector/inspector-panel").InspectorPanel); loader.lazyGetter(this, "WebConsolePanel", () => require("devtools/webconsole/panel").WebConsolePanel); loader.lazyGetter(this, "DebuggerPanel", () => require("devtools/debugger/panel").DebuggerPanel); -loader.lazyImporter(this, "StyleEditorPanel", "resource:///modules/devtools/StyleEditorPanel.jsm"); +loader.lazyGetter(this, "StyleEditorPanel", () => require("devtools/styleeditor/styleeditor-panel").StyleEditorPanel); loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shadereditor/panel").ShaderEditorPanel); loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel")); loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel); diff --git a/browser/devtools/styleeditor/Makefile.in b/browser/devtools/styleeditor/Makefile.in index f75d4ac50e03..f7c48461d2fa 100644 --- a/browser/devtools/styleeditor/Makefile.in +++ b/browser/devtools/styleeditor/Makefile.in @@ -5,4 +5,6 @@ include $(topsrcdir)/config/rules.mk libs:: - $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools + $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools/ + $(NSINSTALL) $(srcdir)/*.js $(FINAL_TARGET)/modules/devtools/styleeditor + diff --git a/browser/devtools/styleeditor/StyleEditorDebuggee.jsm b/browser/devtools/styleeditor/StyleEditorDebuggee.jsm deleted file mode 100644 index 272f3322b175..000000000000 --- a/browser/devtools/styleeditor/StyleEditorDebuggee.jsm +++ /dev/null @@ -1,346 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * 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/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["StyleEditorDebuggee", "StyleSheet"]; - -const Cc = Components.classes; -const Ci = Components.interfaces; -const Cu = Components.utils; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource:///modules/devtools/shared/event-emitter.js"); - -XPCOMUtils.defineLazyModuleGetter(this, "promise", - "resource://gre/modules/commonjs/sdk/core/promise.js", "Promise"); - -/** - * A StyleEditorDebuggee represents the document the style editor is debugging. - * It maintains a list of StyleSheet objects that represent the stylesheets in - * the target's document. It wraps remote debugging protocol comunications. - * - * It emits these events: - * 'document-load': debuggee's document is loaded, style sheets are argument - * 'stylesheets-cleared': The debuggee's stylesheets have been reset (e.g. the - * page navigated) - * - * @param {Target} target - * The target the debuggee is listening to - */ -let StyleEditorDebuggee = function(target) { - EventEmitter.decorate(this); - - this.styleSheets = []; - - this.clear = this.clear.bind(this); - this._onNewDocument = this._onNewDocument.bind(this); - this._onDocumentLoad = this._onDocumentLoad.bind(this); - - this._target = target; - this._actor = this.target.form.styleEditorActor; - - this.client.addListener("documentLoad", this._onDocumentLoad); - this._target.on("navigate", this._onNewDocument); - - this._onNewDocument(); -} - -StyleEditorDebuggee.prototype = { - /** - * list of StyleSheet objects for this target - */ - styleSheets: null, - - /** - * baseURIObject for the current document - */ - baseURI: null, - - /** - * The target we're debugging - */ - get target() { - return this._target; - }, - - /** - * Client for communicating with server with remote debug protocol. - */ - get client() { - return this._target.client; - }, - - /** - * Get the StyleSheet object with the given href. - * - * @param {string} href - * Url of the stylesheet to find - * @return {StyleSheet} - * StyleSheet with the matching href - */ - styleSheetFromHref: function(href) { - for (let sheet of this.styleSheets) { - if (sheet.href == href) { - return sheet; - } - } - return null; - }, - - /** - * Clear stylesheets and state. - */ - clear: function() { - this.baseURI = null; - this.clearStyleSheets(); - }, - - /** - * Clear stylesheets. - */ - clearStyleSheets: function() { - for (let stylesheet of this.styleSheets) { - stylesheet.destroy(); - } - this.styleSheets = []; - this.emit("stylesheets-cleared"); - }, - - /** - * Called when target is created or has navigated. - * Clear previous sheets and request new document's - */ - _onNewDocument: function() { - this.clear(); - - this._getBaseURI(); - - let message = { type: "newDocument" }; - this._sendRequest(message); - }, - - /** - * request baseURIObject information from the document - */ - _getBaseURI: function() { - let message = { type: "getBaseURI" }; - this._sendRequest(message, (response) => { - this.baseURI = Services.io.newURI(response.baseURI, null, null); - }); - }, - - /** - * Handler for document load, forward event with - * all the stylesheets available on load. - * - * @param {string} type - * Event type - * @param {object} request - * Object with 'styleSheets' array of actor forms - */ - _onDocumentLoad: function(type, request) { - if (this.styleSheets.length > 0) { - this.clearStyleSheets(); - } - let sheets = []; - for (let form of request.styleSheets) { - let sheet = this._addStyleSheet(form); - sheets.push(sheet); - } - this.emit("document-load", sheets); - }, - - /** - * Create a new StyleSheet object from the form - * and add to our stylesheet list. - * - * @param {object} form - * Initial properties of the stylesheet - */ - _addStyleSheet: function(form) { - let sheet = new StyleSheet(form, this); - this.styleSheets.push(sheet); - return sheet; - }, - - /** - * Create a new stylesheet with the given text - * and attach it to the document. - * - * @param {string} text - * Initial text of the stylesheet - * @param {function} callback - * Function to call when the stylesheet has been added to the document - */ - createStyleSheet: function(text, callback) { - let message = { type: "newStyleSheet", text: text }; - this._sendRequest(message, (response) => { - let sheet = this._addStyleSheet(response.styleSheet); - callback(sheet); - }); - }, - - /** - * Send a request to our actor on the server - * - * @param {object} message - * Message to send to the actor - * @param {function} callback - * Function to call with reponse from actor - */ - _sendRequest: function(message, callback) { - message.to = this._actor; - this.client.request(message, callback); - }, - - /** - * Clean up and remove listeners - */ - destroy: function() { - this.clear(); - - this._target.off("navigate", this._onNewDocument); - } -} - -/** - * A StyleSheet object represents a stylesheet on the debuggee. It wraps - * communication with a complimentary StyleSheetActor on the server. - * - * It emits these events: - * 'source-load' - The full text source of the stylesheet has been fetched - * 'property-change' - Any property (e.g 'disabled') has changed - * 'style-applied' - A change has been applied to the live stylesheet on the server - * 'error' - An error occured when loading or saving stylesheet - * - * @param {object} form - * Initial properties of the stylesheet - * @param {StyleEditorDebuggee} debuggee - * Owner of the stylesheet - */ -let StyleSheet = function(form, debuggee) { - EventEmitter.decorate(this); - - this.debuggee = debuggee; - this._client = debuggee.client; - this._actor = form.actor; - - this._onSourceLoad = this._onSourceLoad.bind(this); - this._onPropertyChange = this._onPropertyChange.bind(this); - this._onStyleApplied = this._onStyleApplied.bind(this); - - this._client.addListener("sourceLoad", this._onSourceLoad); - this._client.addListener("propertyChange", this._onPropertyChange); - this._client.addListener("styleApplied", this._onStyleApplied); - - // Backwards compatibility - this._client.addListener("sourceLoad-" + this._actor, this._onSourceLoad); - this._client.addListener("propertyChange-" + this._actor, this._onPropertyChange); - this._client.addListener("styleApplied-" + this._actor, this._onStyleApplied); - - - // set initial property values - for (let attr in form) { - this[attr] = form[attr]; - } -} - -StyleSheet.prototype = { - /** - * Toggle the disabled attribute of the stylesheet - */ - toggleDisabled: function() { - let message = { type: "toggleDisabled" }; - this._sendRequest(message); - }, - - /** - * Request that the source of the stylesheet be fetched. - * 'source-load' event will be fired when it's been fetched. - */ - fetchSource: function() { - let message = { type: "fetchSource" }; - this._sendRequest(message); - }, - - /** - * Update the stylesheet in place with the given full source. - * - * @param {string} sheetText - * Full text to update the stylesheet with - */ - update: function(sheetText) { - let message = { type: "update", text: sheetText, transition: true }; - this._sendRequest(message); - }, - - /** - * Handle source load event from the client. - * - * @param {string} type - * Event type - * @param {object} request - * Event details - */ - _onSourceLoad: function(type, request) { - if (request.from == this._actor) { - if (request.error) { - return this.emit("error", request.error); - } - this.emit("source-load", request.source); - } - }, - - /** - * Handle a property change on the stylesheet - * - * @param {string} type - * Event type - * @param {object} request - * Event details - */ - _onPropertyChange: function(type, request) { - if (request.from == this._actor) { - this[request.property] = request.value; - this.emit("property-change", request.property); - } - }, - - /** - * Handle event when update has been successfully applied and propogate it. - */ - _onStyleApplied: function(type, request) { - if (request.from == this._actor) { - this.emit("style-applied"); - } - }, - - /** - * Send a request to our actor on the server - * - * @param {object} message - * Message to send to the actor - * @param {function} callback - * Function to call with reponse from actor - */ - _sendRequest: function(message, callback) { - message.to = this._actor; - this._client.request(message, callback); - }, - - /** - * Clean up and remove event listeners - */ - destroy: function() { - this._client.removeListener("sourceLoad", this._onSourceLoad); - this._client.removeListener("propertyChange", this._onPropertyChange); - this._client.removeListener("styleApplied", this._onStyleApplied); - - this._client.removeListener("sourceLoad-" + this._actor, this._onSourceLoad); - this._client.removeListener("propertyChange-" + this._actor, this._onPropertyChange); - this._client.removeListener("styleApplied-" + this._actor, this._onStyleApplied); - } -} diff --git a/browser/devtools/styleeditor/StyleEditorUI.jsm b/browser/devtools/styleeditor/StyleEditorUI.jsm index 59cf01628f95..157bf18f88e5 100644 --- a/browser/devtools/styleeditor/StyleEditorUI.jsm +++ b/browser/devtools/styleeditor/StyleEditorUI.jsm @@ -25,6 +25,8 @@ const LOAD_ERROR = "error-load"; const STYLE_EDITOR_TEMPLATE = "stylesheet"; +const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled"; + /** * StyleEditorUI is controls and builds the UI of the Style Editor, including * maintaining a list of editors for each stylesheet on a debuggee. @@ -34,15 +36,18 @@ const STYLE_EDITOR_TEMPLATE = "stylesheet"; * 'editor-selected': An editor was selected * 'error': An error occured * - * @param {StyleEditorDebuggee} debuggee - * Debuggee of whose stylesheets should be shown in the UI + * @param {StyleEditorFront} debuggee + * Client-side front for interacting with the page's stylesheets + * @param {Target} target + * Interface for the page we're debugging * @param {Document} panelDoc * Document of the toolbox panel to populate UI in. */ -function StyleEditorUI(debuggee, panelDoc) { +function StyleEditorUI(debuggee, target, panelDoc) { EventEmitter.decorate(this); this._debuggee = debuggee; + this._target = target; this._panelDoc = panelDoc; this._window = this._panelDoc.defaultView; this._root = this._panelDoc.getElementById("style-editor-chrome"); @@ -51,14 +56,18 @@ function StyleEditorUI(debuggee, panelDoc) { this.selectedEditor = null; this._onStyleSheetCreated = this._onStyleSheetCreated.bind(this); - this._onStyleSheetsCleared = this._onStyleSheetsCleared.bind(this); - this._onDocumentLoad = this._onDocumentLoad.bind(this); + this._onNewDocument = this._onNewDocument.bind(this); + this._clear = this._clear.bind(this); this._onError = this._onError.bind(this); - debuggee.on("document-load", this._onDocumentLoad); - debuggee.on("stylesheets-cleared", this._onStyleSheetsCleared); - this.createUI(); + + this._debuggee.getStyleSheets().then((styleSheets) => { + this._resetStyleSheetList(styleSheets); + + this._target.on("will-navigate", this._clear); + this._target.on("navigate", this._onNewDocument); + }) } StyleEditorUI.prototype = { @@ -101,7 +110,7 @@ StyleEditorUI.prototype = { this._view = new SplitView(viewRoot); wire(this._view.rootElement, ".style-editor-newButton", function onNew() { - this._debuggee.createStyleSheet(null, this._onStyleSheetCreated); + this._debuggee.addStyleSheet(null).then(this._onStyleSheetCreated); }.bind(this)); wire(this._view.rootElement, ".style-editor-importButton", function onImport() { @@ -109,6 +118,112 @@ StyleEditorUI.prototype = { }.bind(this)); }, + /** + * Refresh editors to reflect the stylesheets in the document. + * + * @param {string} event + * Event name + * @param {StyleSheet} styleSheet + * StyleSheet object for new sheet + */ + _onNewDocument: function() { + this._debuggee.getStyleSheets().then((styleSheets) => { + this._resetStyleSheetList(styleSheets); + }) + }, + + /** + * Remove all editors and add loading indicator. + */ + _clear: function() { + // remember selected sheet and line number for next load + if (this.selectedEditor && this.selectedEditor.sourceEditor) { + let href = this.selectedEditor.styleSheet.href; + let {line, ch} = this.selectedEditor.sourceEditor.getCursor(); + + this._styleSheetToSelect = { + href: href, + line: line, + col: ch + }; + } + + this._clearStyleSheetEditors(); + this._view.removeAll(); + + this.selectedEditor = null; + + this._root.classList.add("loading"); + }, + + /** + * Add editors for all the given stylesheets to the UI. + * + * @param {array} styleSheets + * Array of StyleSheetFront + */ + _resetStyleSheetList: function(styleSheets) { + this._clear(); + + for (let sheet of styleSheets) { + this._addStyleSheet(sheet); + } + + this._root.classList.remove("loading"); + + this.emit("stylesheets-reset"); + }, + + /** + * Add an editor for this stylesheet. Add editors for its original sources + * instead (e.g. Sass sources), if applicable. + * + * @param {StyleSheetFront} styleSheet + * Style sheet to add to style editor + */ + _addStyleSheet: function(styleSheet) { + let editor = this._addStyleSheetEditor(styleSheet); + + if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { + return; + } + + styleSheet.getOriginalSources().then((sources) => { + if (sources && sources.length) { + this._removeStyleSheetEditor(editor); + sources.forEach((source) => { + // set so the first sheet will be selected, even if it's a source + source.styleSheetIndex = styleSheet.styleSheetIndex; + + this._addStyleSheetEditor(source); + }); + } + }); + }, + + /** + * Add a new editor to the UI for a source. + * + * @param {StyleSheet} styleSheet + * Object representing stylesheet + * @param {nsIfile} file + * Optional file object that sheet was imported from + * @param {Boolean} isNew + * Optional if stylesheet is a new sheet created by user + */ + _addStyleSheetEditor: function(styleSheet, file, isNew) { + let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew); + + editor.on("property-change", this._summaryChange.bind(this, editor)); + editor.on("style-applied", this._summaryChange.bind(this, editor)); + editor.on("error", this._onError); + + this.editors.push(editor); + + editor.fetchSource(this._sourceLoaded.bind(this, editor)); + return editor; + }, + /** * Import a style sheet from file and asynchronously create a * new stylesheet on the debuggee for it. @@ -134,7 +249,7 @@ StyleEditorUI.prototype = { let source = NetUtil.readInputStreamToString(stream, stream.available()); stream.close(); - this._debuggee.createStyleSheet(source, (styleSheet) => { + this._debuggee.addStyleSheet(source).then((styleSheet) => { this._onStyleSheetCreated(styleSheet, file); }); }); @@ -144,24 +259,6 @@ StyleEditorUI.prototype = { showFilePicker(file, false, parentWindow, onFileSelected); }, - /** - * Handler for debuggee's 'stylesheets-cleared' event. Remove all editors. - */ - _onStyleSheetsCleared: function() { - // remember selected sheet and line number for next load - if (this.selectedEditor && this.selectedEditor.sourceEditor) { - let href = this.selectedEditor.styleSheet.href; - let {line, ch} = this.selectedEditor.sourceEditor.getCursor(); - this.selectStyleSheet(href, line, ch); - } - - this._clearStyleSheetEditors(); - this._view.removeAll(); - - this.selectedEditor = null; - - this._root.classList.add("loading"); - }, /** * When a new or imported stylesheet has been added to the document. @@ -171,35 +268,6 @@ StyleEditorUI.prototype = { this._addStyleSheetEditor(styleSheet, file, true); }, - /** - * Handler for debuggee's 'document-load' event. Add editors - * for all style sheets in the document - * - * @param {string} event - * Event name - * @param {StyleSheet} styleSheet - * StyleSheet object for new sheet - */ - _onDocumentLoad: function(event, styleSheets) { - if (this._styleSheetToSelect) { - // if selected stylesheet from previous load isn't here, - // just set first stylesheet to be selected instead - let selectedExists = styleSheets.some((sheet) => { - return this._styleSheetToSelect.href == sheet.href; - }) - if (!selectedExists) { - this._styleSheetToSelect = null; - } - } - for (let sheet of styleSheets) { - this._addStyleSheetEditor(sheet); - } - - this._root.classList.remove("loading"); - - this.emit("document-load"); - }, - /** * Forward any error from a stylesheet. * @@ -207,34 +275,35 @@ StyleEditorUI.prototype = { * Event name * @param {string} errorCode * Code represeting type of error + * @param {string} message + * The full error message */ - _onError: function(event, errorCode) { - this.emit("error", errorCode); + _onError: function(event, errorCode, message) { + this.emit("error", errorCode, message); }, /** - * Add a new editor to the UI for a stylesheet. + * Remove a particular stylesheet editor from the UI * - * @param {StyleSheet} styleSheet - * Object representing stylesheet - * @param {nsIfile} file - * Optional file object that sheet was imported from - * @param {Boolean} isNew - * Optional if stylesheet is a new sheet created by user + * @param {StyleSheetEditor} editor + * The editor to remove. */ - _addStyleSheetEditor: function(styleSheet, file, isNew) { - let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew); + _removeStyleSheetEditor: function(editor) { + if (editor.summary) { + this._view.removeItem(editor.summary); + } + else { + let self = this; + this.on("editor-added", function onAdd(event, added) { + if (editor == added) { + self.off("editor-added", onAdd); + self._view.removeItem(editor.summary); + } + }) + } - editor.once("source-load", this._sourceLoaded.bind(this, editor)); - editor.on("property-change", this._summaryChange.bind(this, editor)); - editor.on("style-applied", this._summaryChange.bind(this, editor)); - editor.on("error", this._onError); - - this.editors.push(editor); - - // Queue editor loading. This helps responsivity during loading when - // there are many heavy stylesheets. - this._window.setTimeout(editor.fetchSource.bind(editor), 0); + editor.destroy(); + this.editors.splice(this.editors.indexOf(editor), 1); }, /** @@ -248,8 +317,8 @@ StyleEditorUI.prototype = { }, /** - * Handler for an StyleSheetEditor's 'source-load' event. - * Create a summary UI for the editor. + * Called when a StyleSheetEditor's source has been fetched. Create a + * summary UI for the editor. * * @param {StyleSheetEditor} editor * Editor to create UI for. @@ -310,8 +379,7 @@ StyleEditorUI.prototype = { } // If this is the first stylesheet, select it - if (this.selectedStyleSheetIndex == -1 - && !this._styleSheetToSelect + if (!this.selectedEditor && editor.styleSheet.styleSheetIndex == 0) { this._selectEditor(editor); } @@ -322,7 +390,6 @@ StyleEditorUI.prototype = { onShow: function(summary, details, data) { let editor = data.editor; this.selectedEditor = editor; - this._styleSheetToSelect = null; if (!editor.sourceEditor) { // only initialize source editor when we switch to this view @@ -345,7 +412,8 @@ StyleEditorUI.prototype = { for each (let editor in this.editors) { if (editor.styleSheet.href == sheet.href) { this._selectEditor(editor, sheet.line, sheet.col); - break; + this._styleSheetToSelect = null; + return; } } }, @@ -367,7 +435,6 @@ StyleEditorUI.prototype = { editor.getSourceEditor().then(() => { editor.sourceEditor.setCursor({line: line, ch: col}); }); - this._view.activeSummary = editor.summary; }, @@ -466,8 +533,5 @@ StyleEditorUI.prototype = { destroy: function() { this._clearStyleSheetEditors(); - - this._debuggee.off("document-load", this._onDocumentLoad); - this._debuggee.off("stylesheets-cleared", this._onStyleSheetsCleared); } } diff --git a/browser/devtools/styleeditor/StyleSheetEditor.jsm b/browser/devtools/styleeditor/StyleSheetEditor.jsm index 4a36dccda073..bc307b1afea2 100644 --- a/browser/devtools/styleeditor/StyleSheetEditor.jsm +++ b/browser/devtools/styleeditor/StyleSheetEditor.jsm @@ -14,6 +14,7 @@ const Cu = Components.utils; const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; const Editor = require("devtools/sourceeditor/editor"); const promise = require("sdk/core/promise"); +const {CssLogic} = require("devtools/styleinspector/css-logic"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); @@ -21,6 +22,7 @@ Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource:///modules/devtools/shared/event-emitter.js"); Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm"); +const LOAD_ERROR = "error-load"; const SAVE_ERROR = "error-save"; // max update frequency in ms (avoid potential typing lag and/or flicker) @@ -32,12 +34,12 @@ const UPDATE_STYLESHEET_THROTTLE_DELAY = 500; * object. * * Emits events: - * 'source-load': The source of the stylesheet has been fetched * 'property-change': A property on the underlying stylesheet has changed * 'source-editor-load': The source editor for this editor has been loaded * 'error': An error has occured * - * @param {StyleSheet} styleSheet + * @param {StyleSheet|OriginalSource} styleSheet + * Stylesheet or original source to show * @param {DOMWindow} win * panel window for style editor * @param {nsIFile} file @@ -57,13 +59,19 @@ function StyleSheetEditor(styleSheet, win, file, isNew) { this.errorMessage = null; + let readOnly = false; + if (styleSheet.isOriginalSource) { + // live-preview won't work with sources that need compilation + readOnly = true; + } + this._state = { // state to use when inputElement attaches text: "", selection: { start: {line: 0, ch: 0}, end: {line: 0, ch: 0} }, - readOnly: false, + readOnly: readOnly, topIndex: 0, // the first visible line }; @@ -73,30 +81,21 @@ function StyleSheetEditor(styleSheet, win, file, isNew) { this._styleSheetFilePath = this.styleSheet.href; } - this._onSourceLoad = this._onSourceLoad.bind(this); this._onPropertyChange = this._onPropertyChange.bind(this); this._onError = this._onError.bind(this); this._focusOnSourceEditorReady = false; - this.styleSheet.once("source-load", this._onSourceLoad); this.styleSheet.on("property-change", this._onPropertyChange); this.styleSheet.on("error", this._onError); } StyleSheetEditor.prototype = { - /** - * This editor's source editor - */ - get sourceEditor() { - return this._sourceEditor; - }, - /** * Whether there are unsaved changes in the editor */ get unsaved() { - return this._sourceEditor && !this._sourceEditor.isClean(); + return this.sourceEditor && !this.sourceEditor.isClean(); }, /** @@ -113,37 +112,23 @@ StyleSheetEditor.prototype = { * @return string */ get friendlyName() { - if (this.savedFile) { // reuse the saved filename if any + if (this.savedFile) { return this.savedFile.leafName; } if (this._isNew) { - let index = this.styleSheet.styleSheetIndex + 1; // 0-indexing only works for devs + let index = this.styleSheet.styleSheetIndex + 1; return _("newStyleSheet", index); } if (!this.styleSheet.href) { - let index = this.styleSheet.styleSheetIndex + 1; // 0-indexing only works for devs + let index = this.styleSheet.styleSheetIndex + 1; return _("inlineStyleSheet", index); } if (!this._friendlyName) { let sheetURI = this.styleSheet.href; - let contentURI = this.styleSheet.debuggee.baseURI; - let contentURIScheme = contentURI.scheme; - let contentURILeafIndex = contentURI.specIgnoringRef.lastIndexOf("/"); - contentURI = contentURI.specIgnoringRef; - - // get content base URI without leaf name (if any) - if (contentURILeafIndex > contentURIScheme.length) { - contentURI = contentURI.substring(0, contentURILeafIndex + 1); - } - - // avoid verbose repetition of absolute URI when the style sheet URI - // is relative to the content URI - this._friendlyName = (sheetURI.indexOf(contentURI) == 0) - ? sheetURI.substring(contentURI.length) - : sheetURI; + this._friendlyName = CssLogic.shortSource({ href: sheetURI }); try { this._friendlyName = decodeURI(this._friendlyName); } catch (ex) { @@ -155,22 +140,17 @@ StyleSheetEditor.prototype = { /** * Start fetching the full text source for this editor's sheet. */ - fetchSource: function() { - this.styleSheet.fetchSource(); - }, + fetchSource: function(callback) { + this.styleSheet.getText().then((longStr) => { + longStr.string().then((source) => { + this._state.text = prettifyCSS(source); + this.sourceLoaded = true; - /** - * Handle source fetched event. Forward source-load event. - * - * @param {string} event - * Event type - * @param {string} source - * Full-text source of the stylesheet - */ - _onSourceLoad: function(event, source) { - this._state.text = prettifyCSS(source); - this.sourceLoaded = true; - this.emit("source-load"); + callback(source); + }); + }, e => { + this.emit("error", LOAD_ERROR, this.styleSheet.href); + }) }, /** @@ -181,8 +161,8 @@ StyleSheetEditor.prototype = { * @param {string} property * Property that has changed on sheet */ - _onPropertyChange: function(event, property) { - this.emit("property-change", property); + _onPropertyChange: function(property, value) { + this.emit("property-change", property, value); }, /** @@ -220,7 +200,7 @@ StyleSheetEditor.prototype = { this.updateStyleSheet(); }); - this._sourceEditor = sourceEditor; + this.sourceEditor = sourceEditor; if (this._focusOnSourceEditorReady) { this._focusOnSourceEditorReady = false; @@ -320,7 +300,7 @@ StyleSheetEditor.prototype = { this._state.text = this.sourceEditor.getText(); } - this.styleSheet.update(this._state.text); + this.styleSheet.update(this._state.text, true); }, /** @@ -374,6 +354,8 @@ StyleSheetEditor.prototype = { callback(returnFile); } this.sourceEditor.setClean(); + + this.emit("property-change"); }.bind(this)); }; @@ -404,7 +386,6 @@ StyleSheetEditor.prototype = { * Clean up for this editor. */ destroy: function() { - this.styleSheet.off("source-load", this._onSourceLoad); this.styleSheet.off("property-change", this._onPropertyChange); this.styleSheet.off("error", this._onError); } diff --git a/browser/devtools/styleeditor/StyleEditorPanel.jsm b/browser/devtools/styleeditor/styleeditor-panel.js similarity index 82% rename from browser/devtools/styleeditor/StyleEditorPanel.jsm rename to browser/devtools/styleeditor/styleeditor-panel.js index ac58b5c26c30..ef55e84761ee 100644 --- a/browser/devtools/styleeditor/StyleEditorPanel.jsm +++ b/browser/devtools/styleeditor/styleeditor-panel.js @@ -4,21 +4,19 @@ * 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 { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -this.EXPORTED_SYMBOLS = ["StyleEditorPanel"]; +const {Cc, Ci, Cu, Cr} = require("chrome"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); -let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise; -Cu.import("resource:///modules/devtools/shared/event-emitter.js"); -Cu.import("resource:///modules/devtools/StyleEditorDebuggee.jsm"); + +let promise = require("sdk/core/promise"); +let EventEmitter = require("devtools/shared/event-emitter"); + Cu.import("resource:///modules/devtools/StyleEditorUI.jsm"); Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "StyleEditorChrome", - "resource:///modules/devtools/StyleEditorChrome.jsm"); +loader.lazyGetter(this, "StyleSheetsFront", + () => require("devtools/server/actors/styleeditor").StyleSheetsFront); this.StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox) { EventEmitter.decorate(this); @@ -32,6 +30,8 @@ this.StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox) { this._showError = this._showError.bind(this); } +exports.StyleEditorPanel = StyleEditorPanel; + StyleEditorPanel.prototype = { get target() this._toolbox.target, @@ -54,9 +54,9 @@ StyleEditorPanel.prototype = { targetPromise.then(() => { this.target.on("close", this.destroy); - this._debuggee = new StyleEditorDebuggee(this.target); + this._debuggee = StyleSheetsFront(this.target.client, this.target.form); - this.UI = new StyleEditorUI(this._debuggee, this._panelDoc); + this.UI = new StyleEditorUI(this._debuggee, this.target, this._panelDoc); this.UI.on("error", this._showError); this.isReady = true; @@ -99,7 +99,6 @@ StyleEditorPanel.prototype = { if (!this._debuggee || !this.UI) { return; } - let stylesheet = this._debuggee.styleSheetFromHref(href); this.UI.selectStyleSheet(href, line - 1, col ? col - 1 : 0); }, diff --git a/browser/devtools/styleeditor/test/browser.ini b/browser/devtools/styleeditor/test/browser.ini index 251ddc120177..68a1583ef012 100644 --- a/browser/devtools/styleeditor/test/browser.ini +++ b/browser/devtools/styleeditor/test/browser.ini @@ -19,6 +19,10 @@ support-files = simple.css.gz^headers^ simple.gz.html simple.html + sourcemaps.css + sourcemaps.css.map + sourcemaps.scss + sourcemaps.html test_private.css test_private.html @@ -42,3 +46,4 @@ skip-if = true [browser_styleeditor_sv_keynav.js] [browser_styleeditor_sv_resize.js] [browser_styleeditor_selectstylesheet.js] +[browser_styleeditor_sourcemaps.js] diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js b/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js index b196339a2111..189322a52ba9 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js @@ -13,7 +13,8 @@ function test() { gUI = panel.UI; gUI.on("editor-added", function(event, editor) { count++; - if (count == 2) { + if (count == 4) { + info("all editors added"); runTests(); } }) @@ -35,6 +36,8 @@ function getStylesheetNameLinkFor(aEditor) { } function onEditor0Attach(aEditor) { + info("first editor selected"); + waitForFocus(function () { // left mouse click should focus editor 1 EventUtils.synthesizeMouseAtCenter( @@ -45,6 +48,8 @@ function onEditor0Attach(aEditor) { } function onEditor1Attach(aEditor) { + info("second editor selected"); + ok(aEditor.sourceEditor.hasFocus(), "left mouse click has given editor 1 focus"); diff --git a/browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js b/browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js index 6daa0f4f8b99..18f273654a8e 100644 --- a/browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js +++ b/browser/devtools/styleeditor/test/browser_styleeditor_bug_870339.js @@ -19,25 +19,25 @@ function test() waitForExplicitFinish(); addTabAndOpenStyleEditor(function (aPanel) { - let debuggee = aPanel._debuggee; + let UI = aPanel.UI; // Spam the _onNewDocument callback multiple times before the // StyleEditorActor has a chance to respond to the first one. const SPAM_COUNT = 2; for (let i=0; i { + if (++count >= 3) { + // wait for 3 editors - 1 for first style sheet, 1 for the + // generated style sheet, and 1 for original source after it + // loads and replaces the generated style sheet. + runTests(UI); + } + }) + }); + + content.location = TESTCASE_URI; +} + +function runTests(UI) +{ + is(UI.editors.length, 2); + + let firstEditor = UI.editors[0]; + testFirstEditor(firstEditor); + + let ScssEditor = UI.editors[1]; + + let link = getStylesheetNameLinkFor(ScssEditor); + link.click(); + + ScssEditor.getSourceEditor().then(() => { + testScssEditor(ScssEditor); + + finishUp(); + }); +} + +function testFirstEditor(editor) { + let name = getStylesheetNameFor(editor); + is(name, "simple.css", "First style sheet display name is correct"); +} + +function testScssEditor(editor) { + let name = getStylesheetNameFor(editor); + is(name, "sourcemaps.scss", "Original source display name is correct"); + + let text = editor.sourceEditor.getText(); + + is(text, "\n\ +$paulrougetpink: #f06;\n\ +\n\ +div {\n\ + color: $paulrougetpink;\n\ +}\n\ +\n\ +span {\n\ + background-color: #EEE;\n\ +}", "Original source text is correct"); +} + +/* Helpers */ +function getStylesheetNameLinkFor(editor) { + return editor.summary.querySelector(".stylesheet-name"); +} + +function getStylesheetNameFor(editor) { + return editor.summary.querySelector(".stylesheet-name > label") + .getAttribute("value") +} + +function finishUp() { + Services.prefs.clearUserPref(PREF); + finish(); +} \ No newline at end of file diff --git a/browser/devtools/styleeditor/test/head.js b/browser/devtools/styleeditor/test/head.js index e9a44351edf0..087286f05d0b 100644 --- a/browser/devtools/styleeditor/test/head.js +++ b/browser/devtools/styleeditor/test/head.js @@ -46,46 +46,10 @@ function openStyleEditorInWindow(win, callback) { gPanelWindow = panel._panelWin; panel.UI._alwaysDisableAnimations = true; - - /* - if (aSheet) { - panel.selectStyleSheet(aSheet, aLine, aCol); - } */ - callback(panel); }); } -/* -function launchStyleEditorChrome(aCallback, aSheet, aLine, aCol) -{ - launchStyleEditorChromeFromWindow(window, aCallback, aSheet, aLine, aCol); -} - -function launchStyleEditorChromeFromWindow(aWindow, aCallback, aSheet, aLine, aCol) -{ - let target = TargetFactory.forTab(aWindow.gBrowser.selectedTab); - gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) { - let panel = toolbox.getCurrentPanel(); - gPanelWindow = panel._panelWin; - gPanelWindow.styleEditorChrome._alwaysDisableAnimations = true; - if (aSheet) { - panel.selectStyleSheet(aSheet, aLine, aCol); - } - aCallback(gPanelWindow.styleEditorChrome); - }); -} - -function addTabAndLaunchStyleEditorChromeWhenLoaded(aCallback, aSheet, aLine, aCol) -{ - gBrowser.selectedTab = gBrowser.addTab(); - gBrowser.selectedBrowser.addEventListener("load", function onLoad() { - gBrowser.selectedBrowser.removeEventListener("load", onLoad, true); - launchStyleEditorChrome(aCallback, aSheet, aLine, aCol); - }, true); -} -*/ - function checkDiskCacheFor(host, done) { let foundPrivateData = false; diff --git a/browser/devtools/styleeditor/test/sourcemaps.css b/browser/devtools/styleeditor/test/sourcemaps.css new file mode 100644 index 000000000000..7246a9082aa1 --- /dev/null +++ b/browser/devtools/styleeditor/test/sourcemaps.css @@ -0,0 +1,7 @@ +div { + color: #ff0066; } + +span { + background-color: #EEE; } + +/*# sourceMappingURL=sourcemaps.css.map */ \ No newline at end of file diff --git a/browser/devtools/styleeditor/test/sourcemaps.css.map b/browser/devtools/styleeditor/test/sourcemaps.css.map new file mode 100644 index 000000000000..d280979c6958 --- /dev/null +++ b/browser/devtools/styleeditor/test/sourcemaps.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI", +"sources": ["sourcemaps.scss"], +"names": [], +"file": "sourcemaps.css" +} diff --git a/browser/devtools/styleeditor/test/sourcemaps.html b/browser/devtools/styleeditor/test/sourcemaps.html new file mode 100644 index 000000000000..2b700ac40a4b --- /dev/null +++ b/browser/devtools/styleeditor/test/sourcemaps.html @@ -0,0 +1,11 @@ + + + + testcase for testing CSS source maps + + + + +
source maps testcase
+ + diff --git a/browser/devtools/styleeditor/test/sourcemaps.scss b/browser/devtools/styleeditor/test/sourcemaps.scss new file mode 100644 index 000000000000..0ff6c471bbe0 --- /dev/null +++ b/browser/devtools/styleeditor/test/sourcemaps.scss @@ -0,0 +1,10 @@ + +$paulrougetpink: #f06; + +div { + color: $paulrougetpink; +} + +span { + background-color: #EEE; +} \ No newline at end of file diff --git a/browser/devtools/styleinspector/computed-view.js b/browser/devtools/styleinspector/computed-view.js index d619d02205f5..04c3a3cd18ea 100644 --- a/browser/devtools/styleinspector/computed-view.js +++ b/browser/devtools/styleinspector/computed-view.js @@ -26,6 +26,8 @@ const FILTER_CHANGED_TIMEOUT = 300; const HTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled"; + /** * Helper for long-running processes that should yield occasionally to * the mainloop. @@ -1096,6 +1098,25 @@ function SelectorView(aTree, aSelectorInfo) if (rule && rule.parentStyleSheet) { this.sheet = rule.parentStyleSheet; this.source = CssLogic.shortSource(this.sheet) + ":" + rule.line; + + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + if (showOrig && rule.type != ELEMENT_STYLE) { + rule.getOriginalLocation().then(({href, line, column}) => { + let newSource = CssLogic.shortSource({href: href}) + ":" + line; + + // Really hacky. Setting the 'source' property won't change the + // link's text if the link's already been loaded via template, so we + // have to retroactively mutate the DOM. + if (newSource != this.source && this.tree.propertyContainer) { + let selector = '[sourcelocation="' + this.source + '"]'; + let link = this.tree.propertyContainer.querySelector(selector); + if (link) { + link.textContent = newSource; + } + } + this.source = newSource; + }); + } } else { this.source = CssLogic.l10n("rule.sourceElement"); this.href = "#"; @@ -1217,37 +1238,42 @@ SelectorView.prototype = { { let inspector = this.tree.styleInspector.inspector; let rule = this.selectorInfo.rule; - let line = rule.line || 0; // The style editor can only display stylesheets coming from content because // chrome stylesheets are not listed in the editor's stylesheet selector. // // If the stylesheet is a content stylesheet we send it to the style // editor else we display it in the view source window. - // - - let href = rule.href; let sheet = rule.parentStyleSheet; - if (sheet && href && !sheet.isSystem) { + if (!sheet || sheet.isSystem) { + let contentDoc = null; + if (this.tree.viewedElement.isLocal_toBeDeprecated()) { + let rawNode = this.tree.viewedElement.rawNode(); + if (rawNode) { + contentDoc = rawNode.ownerDocument; + } + } + let viewSourceUtils = inspector.viewSourceUtils; + viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line); + return; + } + + let location = promise.resolve({ + href: rule.href, + line: rule.line + }); + if (rule.href && Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { + location = rule.getOriginalLocation(); + } + + location.then(({href, line}) => { let target = inspector.target; if (ToolDefinitions.styleEditor.isTargetSupported(target)) { gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) { toolbox.getCurrentPanel().selectStyleSheet(href, line); }); } - return; - } - - let contentDoc = null; - if (this.tree.viewedElement.isLocal_toBeDeprecated()) { - let rawNode = this.tree.viewedElement.rawNode(); - if (rawNode) { - contentDoc = rawNode.ownerDocument; - } - } - - let viewSourceUtils = inspector.viewSourceUtils; - viewSourceUtils.viewSource(href, null, contentDoc, line); + }); } }; diff --git a/browser/devtools/styleinspector/computedview.xhtml b/browser/devtools/styleinspector/computedview.xhtml index 8ec8954e642f..3f1c8062e804 100644 --- a/browser/devtools/styleinspector/computedview.xhtml +++ b/browser/devtools/styleinspector/computedview.xhtml @@ -99,6 +99,7 @@ onclick="${selector.openStyleEditor}" onkeydown="${selector.maybeOpenStyleEditor}" title="${selector.href}" + sourcelocation="${selector.source}" tabindex="0">${selector.source} diff --git a/browser/devtools/styleinspector/rule-view.js b/browser/devtools/styleinspector/rule-view.js index 01e4879c4ef1..0499c72193d8 100644 --- a/browser/devtools/styleinspector/rule-view.js +++ b/browser/devtools/styleinspector/rule-view.js @@ -22,6 +22,8 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm"); const HTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled"; + /** * These regular expressions are adapted from firebug's css.js, and are * used to parse CSSStyleDeclaration's cssText attribute. @@ -487,6 +489,33 @@ Rule.prototype = { return this.domRule ? this.domRule.line : null; }, + /** + * The rule's column within a stylesheet + */ + get ruleColumn() + { + return this.domRule ? this.domRule.column : null; + }, + + /** + * Get display name for this rule based on the original source + * for this rule's style sheet. + * + * @return {Promise} + * Promise which resolves with location as a string. + */ + getOriginalSourceString: function Rule_getOriginalSourceString() + { + if (this._originalSourceString) { + return promise.resolve(this._originalSourceString); + } + return this.domRule.getOriginalLocation().then(({href, line}) => { + let string = CssLogic.shortSource({href: href}) + ":" + line; + this._originalSourceString = string; + return string; + }); + }, + /** * Returns true if the rule matches the creation options * specified. @@ -1586,6 +1615,14 @@ RuleEditor.prototype = { sourceLabel.setAttribute("tooltiptext", this.rule.title); source.appendChild(sourceLabel); + let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES); + if (showOrig && this.rule.domRule.type != ELEMENT_STYLE) { + this.rule.getOriginalSourceString().then((string) => { + sourceLabel.setAttribute("value", string); + sourceLabel.setAttribute("tooltiptext", string); + }) + } + let code = createChild(this.element, "div", { class: "ruleview-code" }); diff --git a/browser/devtools/styleinspector/style-inspector.js b/browser/devtools/styleinspector/style-inspector.js index 9b91c1fe712a..c154a6c40e92 100644 --- a/browser/devtools/styleinspector/style-inspector.js +++ b/browser/devtools/styleinspector/style-inspector.js @@ -5,6 +5,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ const {Cc, Cu, Ci} = require("chrome"); +const promise = require("sdk/core/promise"); let ToolDefinitions = require("main").Tools; @@ -16,6 +17,8 @@ loader.lazyGetter(this, "ComputedView", () => require("devtools/styleinspector/c loader.lazyGetter(this, "_strings", () => Services.strings .createBundle("chrome://global/locale/devtools/styleinspector.properties")); +const PREF_ORIG_SOURCES = "devtools.styleeditor.source-maps-enabled"; + // This module doesn't currently export any symbols directly, it only // registers inspector tools. @@ -42,29 +45,30 @@ function RuleViewTool(aInspector, aWindow, aIFrame) this._cssLinkHandler = (aEvent) => { let rule = aEvent.detail.rule; - let line = rule.line || 0; - - // The style editor can only display stylesheets coming from content because - // chrome stylesheets are not listed in the editor's stylesheet selector. - // - // If the stylesheet is a content stylesheet we send it to the style - // editor else we display it in the view source window. - // - let href = rule.href; let sheet = rule.parentStyleSheet; - if (sheet && href && !sheet.isSystem) { - let target = this.inspector.target; - if (ToolDefinitions.styleEditor.isTargetSupported(target)) { - gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) { - toolbox.getCurrentPanel().selectStyleSheet(href, line); - }); - } + + // Chrome stylesheets are not listed in the style editor, so show + // these sheets in the view source window instead. + if (!sheet || !rule.href || sheet.isSystem) { + let contentDoc = this.inspector.selection.document; + let viewSourceUtils = this.inspector.viewSourceUtils; + viewSourceUtils.viewSource(rule.href, null, contentDoc, rule.line || 0); return; } - let contentDoc = this.inspector.selection.document; - let viewSourceUtils = this.inspector.viewSourceUtils; - viewSourceUtils.viewSource(href, null, contentDoc, line); + let location = promise.resolve(rule.location); + if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) { + location = rule.getOriginalLocation(); + } + location.then(({ href, line, column }) => { + let target = this.inspector.target; + if (ToolDefinitions.styleEditor.isTargetSupported(target)) { + gDevTools.showToolbox(target, "styleeditor").then(function(toolbox) { + toolbox.getCurrentPanel().selectStyleSheet(href, line, column); + }); + } + return; + }) } this.view.element.addEventListener("CssRuleViewCSSLinkClicked", diff --git a/browser/devtools/styleinspector/test/browser.ini b/browser/devtools/styleinspector/test/browser.ini index d0e21683383c..780e0b740a65 100644 --- a/browser/devtools/styleinspector/test/browser.ini +++ b/browser/devtools/styleinspector/test/browser.ini @@ -54,3 +54,10 @@ support-files = browser_ruleview_pseudoelement.html [browser_bug765105_background_image_tooltip.js] [browser_bug889638_rule_view_color_picker.js] [browser_bug940500_rule_view_pick_gradient_color.js] +[browser_ruleview_original_source_link.js] +support-files = + sourcemaps.html + sourcemaps.css + sourcemaps.css.map + sourcemaps.scss +[browser_computedview_original_source_link.js] diff --git a/browser/devtools/styleinspector/test/browser_computedview_original_source_link.js b/browser/devtools/styleinspector/test/browser_computedview_original_source_link.js new file mode 100644 index 000000000000..044cd8ba503d --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_computedview_original_source_link.js @@ -0,0 +1,119 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let win; +let doc; +let inspector; +let computedView; +let toolbox; + +const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps.html"; +const PREF = "devtools.styleeditor.source-maps-enabled"; + +function test() +{ + waitForExplicitFinish(); + + Services.prefs.setBoolPref(PREF, true); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function(evt) { + gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, + true); + doc = content.document; + waitForFocus(function () { openComputedView(highlightNode); }, content); + }, true); + + content.location = TESTCASE_URI; +} + +function highlightNode(aInspector, aComputedView) +{ + inspector = aInspector; + computedView = aComputedView; + + // Highlight a node. + let div = content.document.getElementsByTagName("div")[0]; + ok(div, "div to select exists") + + inspector.selection.setNode(div); + inspector.once("inspector-updated", () => { + is(inspector.selection.node, div, "selection matches the div element"); + + expandProperty(0, testComputedViewLink); + }).then(null, console.error); +} + +function testComputedViewLink() { + let link = getLinkByIndex(0); + waitForSuccess({ + name: "link text changed to display original source location", + validatorFn: function() + { + return link.textContent == "sourcemaps.scss:4"; + }, + successFn: linkChanged, + failureFn: linkChanged, + }); +} + +function linkChanged() { + let target = TargetFactory.forTab(gBrowser.selectedTab); + let toolbox = gDevTools.getToolbox(target); + + toolbox.once("styleeditor-ready", function(id, aToolbox) { + let panel = toolbox.getCurrentPanel(); + panel.UI.on("editor-selected", (event, editor) => { + // The style editor selects the first sheet at first load before + // selecting the desired sheet. + if (editor.styleSheet.href.endsWith("scss")) { + info("original source editor selected"); + editor.getSourceEditor().then(editorSelected); + } + }); + }); + + let link = getLinkByIndex(0); + + info("clicking rule view link"); + link.scrollIntoView(); + link.click(); +} + +function editorSelected(editor) { + let href = editor.styleSheet.href; + ok(href.endsWith("sourcemaps.scss"), "selected stylesheet is correct one"); + + let {line, col} = editor.sourceEditor.getCursor(); + is(line, 3, "cursor is at correct line number in original source"); + + finishUp(); +} + +/* Helpers */ +function expandProperty(aIndex, aCallback) +{ + info("expanding property " + aIndex); + let contentDoc = computedView.styleDocument; + let expando = contentDoc.querySelectorAll(".expandable")[aIndex]; + + expando.click(); + + inspector.once("computed-view-property-expanded", aCallback); +} + +function getLinkByIndex(aIndex) +{ + let contentDoc = computedView.styleDocument; + let links = contentDoc.querySelectorAll(".rule-link .link"); + return links[aIndex]; +} + +function finishUp() +{ + gBrowser.removeCurrentTab(); + doc = inspector = computedView = toolbox = win = null; + Services.prefs.clearUserPref(PREF); + finish(); +} diff --git a/browser/devtools/styleinspector/test/browser_ruleview_original_source_link.js b/browser/devtools/styleinspector/test/browser_ruleview_original_source_link.js new file mode 100644 index 000000000000..d570b46491ba --- /dev/null +++ b/browser/devtools/styleinspector/test/browser_ruleview_original_source_link.js @@ -0,0 +1,112 @@ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +let win; +let doc; +let contentWindow; +let inspector; +let toolbox; + +const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps.html"; +const PREF = "devtools.styleeditor.source-maps-enabled"; + +function test() +{ + waitForExplicitFinish(); + + Services.prefs.setBoolPref(PREF, true); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function(evt) { + gBrowser.selectedBrowser.removeEventListener(evt.type, arguments.callee, + true); + doc = content.document; + waitForFocus(openToolbox, content); + }, true); + + content.location = TESTCASE_URI; +} + +function openToolbox() { + let target = TargetFactory.forTab(gBrowser.selectedTab); + + gDevTools.showToolbox(target, "inspector").then(function(aToolbox) { + toolbox = aToolbox; + inspector = toolbox.getCurrentPanel(); + inspector.sidebar.select("ruleview"); + highlightNode(); + }); +} + +function highlightNode() +{ + // Highlight a node. + let div = content.document.getElementsByTagName("div")[0]; + + inspector.selection.setNode(div); + inspector.once("inspector-updated", () => { + is(inspector.selection.node, div, "selection matches the div element"); + testRuleViewLink(); + }).then(null, console.error); +} + +function testRuleViewLink() { + let label = getLinkByIndex(1).querySelector("label"); + + waitForSuccess({ + name: "link text changed to display original source location", + validatorFn: function() + { + return label.getAttribute("value") == "sourcemaps.scss:4"; + }, + successFn: linkChanged, + failureFn: linkChanged, + }); +} + +function linkChanged() { + toolbox.once("styleeditor-ready", function(id, aToolbox) { + let panel = toolbox.getCurrentPanel(); + panel.UI.on("editor-selected", (event, editor) => { + // The style editor selects the first sheet at first load before + // selecting the desired sheet. + if (editor.styleSheet.href.endsWith("scss")) { + info("original source editor selected"); + editor.getSourceEditor().then(editorSelected); + } + }); + }); + + let link = getLinkByIndex(1); + + info("clicking rule view link"); + link.scrollIntoView(); + link.click(); +} + +function editorSelected(editor) { + let href = editor.styleSheet.href; + ok(href.endsWith("sourcemaps.scss"), "selected stylesheet is correct one"); + + let {line, col} = editor.sourceEditor.getCursor(); + is(line, 3, "cursor is at correct line number in original source"); + + finishUp(); +} + +function getLinkByIndex(aIndex) +{ + let contentDoc = ruleView().doc; + contentWindow = contentDoc.defaultView; + let links = contentDoc.querySelectorAll(".ruleview-rule-source"); + return links[aIndex]; +} + +function finishUp() +{ + gBrowser.removeCurrentTab(); + contentWindow = doc = inspector = toolbox = win = null; + Services.prefs.clearUserPref(PREF); + finish(); +} diff --git a/browser/devtools/styleinspector/test/head.js b/browser/devtools/styleinspector/test/head.js index c794676882c6..9ec3e6736587 100644 --- a/browser/devtools/styleinspector/test/head.js +++ b/browser/devtools/styleinspector/test/head.js @@ -3,6 +3,9 @@ * 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 TEST_BASE_HTTP = "http://example.com/browser/browser/devtools/styleinspector/test/"; +const TEST_BASE_HTTPS = "https://example.com/browser/browser/devtools/styleinspector/test/"; + Services.prefs.setBoolPref("devtools.debugger.log", true); SimpleTest.registerCleanupFunction(() => { Services.prefs.clearUserPref("devtools.debugger.log"); @@ -187,6 +190,53 @@ function getComputedPropertyValue(aName) } } +/** + * Polls a given function waiting for it to become true. + * + * @param object aOptions + * Options object with the following properties: + * - validatorFn + * A validator function that returns a boolean. This is called every few + * milliseconds to check if the result is true. When it is true, succesFn + * is called and polling stops. If validatorFn never returns true, then + * polling timeouts after several tries and a failure is recorded. + * - successFn + * A function called when the validator function returns true. + * - failureFn + * A function called if the validator function timeouts - fails to return + * true in the given time. + * - name + * Name of test. This is used to generate the success and failure + * messages. + * - timeout + * Timeout for validator function, in milliseconds. Default is 5000. + */ +function waitForSuccess(aOptions) +{ + let start = Date.now(); + let timeout = aOptions.timeout || 5000; + + function wait(validatorFn, successFn, failureFn) + { + if ((Date.now() - start) > timeout) { + // Log the failure. + ok(false, "Timed out while waiting for: " + aOptions.name); + failureFn(aOptions); + return; + } + + if (validatorFn(aOptions)) { + ok(true, aOptions.name); + successFn(); + } + else { + setTimeout(function() wait(validatorFn, successFn, failureFn), 100); + } + } + + wait(aOptions.validatorFn, aOptions.successFn, aOptions.failureFn); +} + registerCleanupFunction(tearDown); waitForExplicitFinish(); diff --git a/browser/devtools/styleinspector/test/sourcemaps.css b/browser/devtools/styleinspector/test/sourcemaps.css new file mode 100644 index 000000000000..7246a9082aa1 --- /dev/null +++ b/browser/devtools/styleinspector/test/sourcemaps.css @@ -0,0 +1,7 @@ +div { + color: #ff0066; } + +span { + background-color: #EEE; } + +/*# sourceMappingURL=sourcemaps.css.map */ \ No newline at end of file diff --git a/browser/devtools/styleinspector/test/sourcemaps.css.map b/browser/devtools/styleinspector/test/sourcemaps.css.map new file mode 100644 index 000000000000..d280979c6958 --- /dev/null +++ b/browser/devtools/styleinspector/test/sourcemaps.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI", +"sources": ["sourcemaps.scss"], +"names": [], +"file": "sourcemaps.css" +} diff --git a/browser/devtools/styleinspector/test/sourcemaps.html b/browser/devtools/styleinspector/test/sourcemaps.html new file mode 100644 index 000000000000..2b700ac40a4b --- /dev/null +++ b/browser/devtools/styleinspector/test/sourcemaps.html @@ -0,0 +1,11 @@ + + + + testcase for testing CSS source maps + + + + +
source maps testcase
+ + diff --git a/browser/devtools/styleinspector/test/sourcemaps.scss b/browser/devtools/styleinspector/test/sourcemaps.scss new file mode 100644 index 000000000000..0ff6c471bbe0 --- /dev/null +++ b/browser/devtools/styleinspector/test/sourcemaps.scss @@ -0,0 +1,10 @@ + +$paulrougetpink: #f06; + +div { + color: $paulrougetpink; +} + +span { + background-color: #EEE; +} \ No newline at end of file diff --git a/toolkit/devtools/server/actors/inspector.js b/toolkit/devtools/server/actors/inspector.js index 43101038cd97..d244256e8e5b 100644 --- a/toolkit/devtools/server/actors/inspector.js +++ b/toolkit/devtools/server/actors/inspector.js @@ -722,7 +722,7 @@ var NodeListActor = exports.NodeListActor = protocol.ActorClass({ /** * Client side of a node list as returned by querySelectorAll() */ -var NodeListFront = exports.NodeLIstFront = protocol.FrontClass(NodeListActor, { +var NodeListFront = exports.NodeListFront = protocol.FrontClass(NodeListActor, { initialize: function(client, form) { protocol.Front.prototype.initialize.call(this, client, form); }, diff --git a/toolkit/devtools/server/actors/styleeditor.js b/toolkit/devtools/server/actors/styleeditor.js index 857ca9841179..dff2afb60492 100644 --- a/toolkit/devtools/server/actors/styleeditor.js +++ b/toolkit/devtools/server/actors/styleeditor.js @@ -4,14 +4,21 @@ "use strict"; -let Cc = Components.classes; -let Ci = Components.interfaces; -let Cu = Components.utils; +let { components, Cc, Ci, Cu } = require('chrome'); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); +Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); + +const promise = require("sdk/core/promise"); +const events = require("sdk/event/core"); +const protocol = require("devtools/server/protocol"); +const {Arg, Option, method, RetVal, types} = protocol; +const {LongStringActor, ShortLongString} = require("devtools/server/actors/string"); + +loader.lazyGetter(this, "CssLogic", () => require("devtools/styleinspector/css-logic").CssLogic); let TRANSITION_CLASS = "moz-styleeditor-transitioning"; let TRANSITION_DURATION_MS = 500; @@ -25,34 +32,25 @@ transition-property: all !important;\ let LOAD_ERROR = "error-load"; +exports.register = function(handle) { + handle.addTabActor(StyleSheetsActor, "styleSheetsActor"); + handle.addGlobalActor(StyleSheetsActor, "styleSheetsActor"); +}; + +exports.unregister = function(handle) { + handle.removeTabActor(StyleSheetsActor); + handle.removeGlobalActor(StyleSheetsActor); +}; + +types.addActorType("stylesheet"); +types.addActorType("originalsource"); + /** - * Creates a StyleEditorActor. StyleEditorActor provides remote access to the - * built-in style editor module. + * Creates a StyleSheetsActor. StyleSheetsActor provides remote access to the + * stylesheets of a document. */ -function StyleEditorActor(aConnection, aParentActor) -{ - this.conn = aConnection; - this._onDocumentLoaded = this._onDocumentLoaded.bind(this); - this._onSheetLoaded = this._onSheetLoaded.bind(this); - this.parentActor = aParentActor; - - // keep a map of sheets-to-actors so we don't create two actors for one sheet - this._sheets = new Map(); - - this._actorPool = new ActorPool(this.conn); - this.conn.addActorPool(this._actorPool); -} - -StyleEditorActor.prototype = { - /** - * Actor pool for all of the actors we send to the client. - */ - _actorPool: null, - - /** - * The debugger server connection instance. - */ - conn: null, +let StyleSheetsActor = protocol.ActorClass({ + typeName: "stylesheets", /** * The window we work with, taken from the parent actor. @@ -64,104 +62,72 @@ StyleEditorActor.prototype = { */ get document() this.window.document, - actorPrefix: "styleEditor", - form: function() { return { actor: this.actorID }; }, - /** - * Destroy the current StyleEditorActor instance. - */ - disconnect: function() - { - if (this._observer) { - this._observer.disconnect(); - delete this._observer; - } + initialize: function (conn, tabActor) { + protocol.Actor.prototype.initialize.call(this, null); + this.parentActor = tabActor; + + // keep a map of sheets-to-actors so we don't create two actors for one sheet + this._sheets = new Map(); + }, + + /** + * Destroy the current StyleSheetsActor instance. + */ + destroy: function() + { this._sheets.clear(); - - this.conn.removeActorPool(this._actorPool); - this._actorPool = null; - this.conn = null; }, /** - * Release an actor from our actor pool. + * Protocol method for getting a list of StyleSheetActors representing + * all the style sheets in this document. */ - releaseActor: function(actor) - { - if (this._actorPool) { - this._actorPool.removeActor(actor.actorID); - } - }, + getStyleSheets: method(function() { + let deferred = promise.defer(); - /** - * Get the BaseURI for the document. - * - * @return {object} JSON message to with BaseURI - */ - onGetBaseURI: function() { - return { baseURI: this.document.baseURIObject.spec }; - }, + let window = this.window; + var domReady = () => { + window.removeEventListener("DOMContentLoaded", domReady, true); - /** - * Called when target navigates to a new document. - * Adds load listeners to document. - */ - onNewDocument: function() { - // delete previous document's actors - this._clearStyleSheetActors(); - - // Note: listening for load won't be necessary once - // https://bugzilla.mozilla.org/show_bug.cgi?id=839103 is fixed - if (this.document.readyState == "complete") { - this._onDocumentLoaded(); - } - else { - this.window.addEventListener("load", this._onDocumentLoaded, false); - } - return {}; - }, - - /** - * Event handler for document loaded event. Add actor for each stylesheet - * and send an event notifying of the load - */ - _onDocumentLoaded: function(event) { - if (event) { - this.window.removeEventListener("load", this._onDocumentLoaded, false); - } - - let documents = [this.document]; - var forms = []; - for (let doc of documents) { - let sheetForms = this._addStyleSheets(doc.styleSheets); - forms = forms.concat(sheetForms); - // Recursively handle style sheets of the documents in iframes. - for (let iframe of doc.getElementsByTagName("iframe")) { - documents.push(iframe.contentDocument); + let documents = [this.document]; + let actors = []; + for (let doc of documents) { + let sheets = this._addStyleSheets(doc.styleSheets); + actors = actors.concat(sheets); + // Recursively handle style sheets of the documents in iframes. + for (let iframe of doc.getElementsByTagName("iframe")) { + documents.push(iframe.contentDocument); + } } + deferred.resolve(actors); + }; + + if (window.document.readyState === "loading") { + window.addEventListener("DOMContentLoaded", domReady, true); + } else { + domReady(); } - this.conn.send({ - from: this.actorID, - type: "documentLoad", - styleSheets: forms - }); - }, + return deferred.promise; + }, { + request: {}, + response: { styleSheets: RetVal("array:stylesheet") } + }), /** - * Add all the stylesheets to the map and create an actor - * for each one if not already created. Send event that there - * are new stylesheets. + * Add all the stylesheets to the map and create an actor for each one + * if not already created. Send event that there are new stylesheets. * * @param {[DOMStyleSheet]} styleSheets * Stylesheets to add * @return {[object]} - * Array of forms for each StyleSheetActor created + * Array of actors for each StyleSheetActor created */ _addStyleSheets: function(styleSheets) { @@ -174,13 +140,9 @@ StyleEditorActor.prototype = { let imports = this._getImported(styleSheet); sheets = sheets.concat(imports); } + let actors = sheets.map(this._createStyleSheetActor.bind(this)); - let forms = sheets.map((sheet) => { - let actor = this._createStyleSheetActor(sheet); - return actor.form(); - }); - - return forms; + return actors; }, /** @@ -216,22 +178,23 @@ StyleEditorActor.prototype = { }, /** - * Create a new actor for a style sheet, if it hasn't - * already been created, and return it. + * Create a new actor for a style sheet, if it hasn't already been created. * - * @param {DOMStyleSheet} aStyleSheet + * @param {DOMStyleSheet} styleSheet * The style sheet to create an actor for. * @return {StyleSheetActor} * The actor for this style sheet */ - _createStyleSheetActor: function(aStyleSheet) + _createStyleSheetActor: function(styleSheet) { - if (this._sheets.has(aStyleSheet)) { - return this._sheets.get(aStyleSheet); + if (this._sheets.has(styleSheet)) { + return this._sheets.get(styleSheet); } - let actor = new StyleSheetActor(aStyleSheet, this); - this._actorPool.addActor(actor); - this._sheets.set(aStyleSheet, actor); + let actor = new StyleSheetActor(styleSheet, this); + + this.manage(actor); + this._sheets.set(styleSheet, actor); + return actor; }, @@ -240,35 +203,11 @@ StyleEditorActor.prototype = { */ _clearStyleSheetActors: function() { for (let actor in this._sheets) { - this.releaseActor(this._sheets[actor]); + this.unmanage(this._sheets[actor]); } this._sheets.clear(); }, - /** - * Get the actors of all the stylesheets in the current document. - * - * @return {object} JSON message with the stylesheet actors' forms - */ - onGetStyleSheets: function() { - let forms = this._addStyleSheets(this.document.styleSheets); - return { "styleSheets": forms }; - }, - - /** - * Handler for style sheet loading event. Add - * a new actor for the sheet and notify. - * - * @param {Event} event - */ - _onSheetLoaded: function(event) { - let style = event.target; - style.removeEventListener("load", this._onSheetLoaded, false); - - let actor = this._createStyleSheetActor(style.sheet); - this._notifyStyleSheetsAdded([actor.form()]); - }, - /** * Create a new style sheet in the document with the given text. * Return an actor for it. @@ -278,77 +217,76 @@ StyleEditorActor.prototype = { * @return {object} * Object with 'styelSheet' property for form on new actor. */ - onNewStyleSheet: function(request) { + addStyleSheet: method(function(text) { let parent = this.document.documentElement; let style = this.document.createElementNS("http://www.w3.org/1999/xhtml", "style"); style.setAttribute("type", "text/css"); - if (request.text) { - style.appendChild(this.document.createTextNode(request.text)); + if (text) { + style.appendChild(this.document.createTextNode(text)); } parent.appendChild(style); let actor = this._createStyleSheetActor(style.sheet); - return { styleSheet: actor.form() }; - } -}; + return actor; + }, { + request: { text: Arg(0, "string") }, + response: { styleSheet: RetVal("stylesheet") } + }) +}); /** - * The request types this actor can handle. + * The corresponding Front object for the StyleSheetsActor. */ -StyleEditorActor.prototype.requestTypes = { - "getStyleSheets": StyleEditorActor.prototype.onGetStyleSheets, - "newStyleSheet": StyleEditorActor.prototype.onNewStyleSheet, - "getBaseURI": StyleEditorActor.prototype.onGetBaseURI, - "newDocument": StyleEditorActor.prototype.onNewDocument -}; +let StyleSheetsFront = protocol.FrontClass(StyleSheetsActor, { + initialize: function(client, tabForm) { + protocol.Front.prototype.initialize.call(this, client); + this.actorID = tabForm.styleSheetsActor; - -function StyleSheetActor(aStyleSheet, aParentActor) { - this.styleSheet = aStyleSheet; - this.parentActor = aParentActor; - - // text and index are unknown until source load - this.text = null; - this._styleSheetIndex = -1; - - this._transitionRefCount = 0; - - this._onSourceLoad = this._onSourceLoad.bind(this); - - // if this sheet has an @import, then it's rules are loaded async - let ownerNode = this.styleSheet.ownerNode; - if (ownerNode) { - let onSheetLoaded = function(event) { - ownerNode.removeEventListener("load", onSheetLoaded, false); - this._notifyPropertyChanged("ruleCount"); - }.bind(this); - - ownerNode.addEventListener("load", onSheetLoaded, false); + client.addActorPool(this); + this.manage(this); } -} +}); -StyleSheetActor.prototype = { - actorPrefix: "stylesheet", +/** + * A StyleSheetActor represents a stylesheet on the server. + */ +let StyleSheetActor = protocol.ActorClass({ + typeName: "stylesheet", + + events: { + "property-change" : { + type: "propertyChange", + property: Arg(0, "string"), + value: Arg(1, "json") + }, + "style-applied" : { + type: "styleApplied" + } + }, + + /* List of original sources that generated this stylesheet */ + _originalSources: null, toString: function() { return "[StyleSheetActor " + this.actorID + "]"; }, - disconnect: function() { - this.parentActor.releaseActor(this); - }, - /** * Window of target */ - get window() this.parentActor.window, + get window() this._window || this.parentActor.window, /** * Document of target. */ get document() this.window.document, + /** + * URL of underlying stylesheet. + */ + get href() this.rawSheet.href, + /** * Retrieve the index (order) of stylesheet in the document. * @@ -358,7 +296,7 @@ StyleSheetActor.prototype = { { if (this._styleSheetIndex == -1) { for (let i = 0; i < this.document.styleSheets.length; i++) { - if (this.document.styleSheets[i] == this.styleSheet) { + if (this.document.styleSheets[i] == this.rawSheet) { this._styleSheetIndex = i; break; } @@ -367,6 +305,33 @@ StyleSheetActor.prototype = { return this._styleSheetIndex; }, + initialize: function(aStyleSheet, aParentActor, aWindow) { + protocol.Actor.prototype.initialize.call(this, null); + + this.rawSheet = aStyleSheet; + this.parentActor = aParentActor; + this.conn = this.parentActor.conn; + + this._window = aWindow; + + // text and index are unknown until source load + this.text = null; + this._styleSheetIndex = -1; + + this._transitionRefCount = 0; + + // if this sheet has an @import, then it's rules are loaded async + let ownerNode = this.rawSheet.ownerNode; + if (ownerNode) { + let onSheetLoaded = function(event) { + ownerNode.removeEventListener("load", onSheetLoaded, false); + this._notifyPropertyChanged("ruleCount"); + }.bind(this); + + ownerNode.addEventListener("load", onSheetLoaded, false); + } + }, + /** * Get the current state of the actor * @@ -374,29 +339,37 @@ StyleSheetActor.prototype = { * With properties of the underlying stylesheet, plus 'text', * 'styleSheetIndex' and 'parentActor' if it's @imported */ - form: function() { - let form = { - actor: this.actorID, // actorID is set when this actor is added to a pool - href: this.styleSheet.href, - disabled: this.styleSheet.disabled, - title: this.styleSheet.title, - styleSheetIndex: this.styleSheetIndex, - text: this.text + form: function(detail) { + if (detail === "actorid") { + return this.actorID; } - // get parent actor if this sheet was @imported - let parent = this.styleSheet.parentStyleSheet; - if (parent) { - form.parentActor = this.parentActor._sheets.get(parent).form(); + let docHref; + if (this.rawSheet.ownerNode) { + if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) { + docHref = this.rawSheet.ownerNode.location.href; + } + if (this.rawSheet.ownerNode.ownerDocument) { + docHref = this.rawSheet.ownerNode.ownerDocument.location.href; + } + } + + let form = { + actor: this.actorID, // actorID is set when this actor is added to a pool + href: this.href, + nodeHref: docHref, + disabled: this.rawSheet.disabled, + title: this.rawSheet.title, + system: !CssLogic.isContentStylesheet(this.rawSheet), + styleSheetIndex: this.styleSheetIndex } try { - form.ruleCount = this.styleSheet.cssRules.length; + form.ruleCount = this.rawSheet.cssRules.length; } catch(e) { // stylesheet had an @import rule that wasn't loaded yet } - return form; }, @@ -406,12 +379,14 @@ StyleSheetActor.prototype = { * @return {object} * 'disabled' - the disabled state after toggling. */ - onToggleDisabled: function() { - this.styleSheet.disabled = !this.styleSheet.disabled; + toggleDisabled: method(function() { + this.rawSheet.disabled = !this.rawSheet.disabled; this._notifyPropertyChanged("disabled"); - return { disabled: this.styleSheet.disabled }; - }, + return this.rawSheet.disabled; + }, { + response: { disabled: RetVal("boolean")} + }), /** * Send an event notifying that a property of the stylesheet @@ -421,89 +396,225 @@ StyleSheetActor.prototype = { * Name of the changed property */ _notifyPropertyChanged: function(property) { - this.conn.send({ - from: this.actorID, - type: "propertyChange", - property: property, - value: this.form()[property] + events.emit(this, "property-change", property, this.form()[property]); + }, + + /** + * Protocol method to get the text of this stylesheet. + */ + getText: method(function() { + return this._getText().then((text) => { + return new LongStringActor(this.conn, text || ""); + }); + }, { + response: { + text: RetVal("longstring") + } + }), + + /** + * Fetch the text for this stylesheet from the cache or network. Return + * cached text if it's already been fetched. + * + * @return {Promise} + * Promise that resolves with a string text of the stylesheet. + */ + _getText: function() { + if (this.text) { + return promise.resolve(this.text); + } + + if (!this.href) { + // this is an inline