Bug 1206133 - Add popuppositioning state and popuppositioned event to improve arrow panel position handling. r=enndeakin

MozReview-Commit-ID: Dh1npORCQ6J

--HG--
extra : rebase_source : 5df6076561a746791c44d249afa31009d0e1b30a
This commit is contained in:
Kirk Steuber 2016-08-16 15:33:05 -07:00
Родитель 51b70b73b8
Коммит ad4b55af1f
13 изменённых файлов: 272 добавлений и 61 удалений

Просмотреть файл

@ -260,12 +260,26 @@ var StarUI = {
parent.setAttribute("open", "true");
}
}
this.panel.openPopup(aAnchorElement, aPosition);
let panel = this.panel;
let target = panel;
if (target.parentNode) {
// By targeting the panel's parent and using a capturing listener, we
// can have our listener called before others waiting for the panel to
// be shown (which probably expect the panel to be fully initialized)
target = target.parentNode;
}
target.addEventListener("popupshown", function shownListener(event) {
if (event.target == panel) {
target.removeEventListener("popupshown", shownListener, true);
gEditItemOverlay.initPanel({ node: aNode
, hiddenRows: ["description", "location",
"loadInSidebar", "keyword"]
, focusedElement: "preferred" });
, focusedElement: "preferred"});
}
}, true);
this.panel.openPopup(aAnchorElement, aPosition);
}),
panelShown:

Просмотреть файл

@ -177,6 +177,18 @@ var gEditItemOverlay = {
initPanel(aInfo) {
if (typeof(aInfo) != "object" || aInfo === null)
throw new Error("aInfo must be an object.");
if ("node" in aInfo) {
try {
aInfo.node.type;
} catch (e) {
// If the lazy loader for |type| generates an exception, it means that
// this bookmark could not be loaded. This sometimes happens when tests
// create a bookmark by clicking the bookmark star, then try to cleanup
// before the bookmark panel has finished opening. Either way, if we
// cannot retrieve the bookmark information, we cannot open the panel.
return;
}
}
// For sanity ensure that the implementer has uninited the panel before
// trying to init it again, or we could end up leaking due to observers.

Просмотреть файл

@ -884,6 +884,7 @@ GK_ATOM(onpointerlockchange, "onpointerlockchange")
GK_ATOM(onpointerlockerror, "onpointerlockerror")
GK_ATOM(onpopuphidden, "onpopuphidden")
GK_ATOM(onpopuphiding, "onpopuphiding")
GK_ATOM(onpopuppositioned, "onpopuppositioned")
GK_ATOM(onpopupshowing, "onpopupshowing")
GK_ATOM(onpopupshown, "onpopupshown")
GK_ATOM(onposter, "onposter")

Просмотреть файл

@ -781,6 +781,10 @@ NON_IDL_EVENT(popupshown,
eXULPopupShown,
EventNameType_XUL,
eBasicEventClass)
NON_IDL_EVENT(popuppositioned,
eXULPopupPositioned,
EventNameType_XUL,
eBasicEventClass)
NON_IDL_EVENT(popuphiding,
eXULPopupHiding,
EventNameType_XUL,

Просмотреть файл

@ -232,6 +232,7 @@ PopupBoxObject::GetPopupState(nsString& aState)
aState.AssignLiteral("open");
break;
case ePopupShowing:
case ePopupPositioning:
case ePopupOpening:
case ePopupVisible:
aState.AssignLiteral("showing");

Просмотреть файл

@ -436,7 +436,7 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu,
if (!isOpen) {
// if the popup is not open, only do layout while showing or if the menu
// is sized to the popup
shouldPosition = (mPopupState == ePopupShowing);
shouldPosition = (mPopupState == ePopupShowing || mPopupState == ePopupPositioning);
if (!shouldPosition && !aSizedToPopup) {
RemoveStateBits(NS_FRAME_FIRST_REFLOW);
return;
@ -476,7 +476,7 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu,
bool needCallback = false;
if (shouldPosition) {
SetPopupPosition(aAnchor, false, aSizedToPopup);
SetPopupPosition(aAnchor, false, aSizedToPopup, mPopupState == ePopupPositioning);
needCallback = true;
}
@ -502,7 +502,7 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu,
}
if (rePosition) {
SetPopupPosition(aAnchor, false, aSizedToPopup);
SetPopupPosition(aAnchor, false, aSizedToPopup, false);
}
nsPresContext* pc = PresContext();
@ -561,7 +561,7 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu,
bool
nsMenuPopupFrame::ReflowFinished()
{
SetPopupPosition(mReflowCallbackData.mAnchor, false, mReflowCallbackData.mSizedToPopup);
SetPopupPosition(mReflowCallbackData.mAnchor, false, mReflowCallbackData.mSizedToPopup, false);
mReflowCallbackData.Clear();
@ -865,7 +865,7 @@ nsMenuPopupFrame::ShowPopup(bool aIsContextMenu)
InvalidateFrameSubtree();
if (mPopupState == ePopupShowing) {
if (mPopupState == ePopupShowing || mPopupState == ePopupPositioning) {
mPopupState = ePopupOpening;
mIsOpenChanged = true;
@ -906,7 +906,8 @@ nsMenuPopupFrame::HidePopup(bool aDeselectMenu, nsPopupState aNewState)
ClearPopupShownDispatcher();
// don't hide the popup when it isn't open
if (mPopupState == ePopupClosed || mPopupState == ePopupShowing)
if (mPopupState == ePopupClosed || mPopupState == ePopupShowing ||
mPopupState == ePopupPositioning)
return;
// clear the trigger content if the popup is being closed. But don't clear
@ -1184,6 +1185,8 @@ nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
nscoord aOffsetForContextMenu, FlipStyle aFlip,
bool* aFlipSide)
{
*aFlipSide = false;
// all of the coordinates used here are in app units relative to the screen
nscoord popupSize = aSize;
if (aScreenPoint < aScreenBegin) {
@ -1284,7 +1287,7 @@ nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
}
nsresult
nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aSizedToPopup)
nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aSizedToPopup, bool aNotify)
{
if (!mShouldAutoPosition)
return NS_OK;
@ -1589,6 +1592,17 @@ nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aS
SetXULBounds(state, mRect);
}
// If the popup is in the positioned state or if it is shown and the position
// or size changed, dispatch a popuppositioned event if the popup wants it.
nsIntRect newRect(screenPoint.x, screenPoint.y, mRect.width, mRect.height);
if (mPopupState == ePopupPositioning ||
(mPopupState == ePopupShown && !newRect.IsEqualEdges(mUsedScreenRect))) {
mUsedScreenRect = newRect;
if (aNotify) {
nsXULPopupPositionedEvent::DispatchIfNeeded(mContent, false, false);
}
}
return NS_OK;
}
@ -2270,7 +2284,7 @@ nsMenuPopupFrame::MoveTo(const CSSIntPoint& aPos, bool aUpdateAttrs)
mScreenRect.x = aPos.x - presContext->AppUnitsToIntCSSPixels(margin.left);
mScreenRect.y = aPos.y - presContext->AppUnitsToIntCSSPixels(margin.top);
SetPopupPosition(nullptr, true, false);
SetPopupPosition(nullptr, true, false, true);
nsCOMPtr<nsIContent> popup = mContent;
if (aUpdateAttrs && (popup->HasAttr(kNameSpaceID_None, nsGkAtoms::left) ||
@ -2299,7 +2313,7 @@ nsMenuPopupFrame::MoveToAnchor(nsIContent* aAnchorContent,
mPopupState = oldstate;
// Pass false here so that flipping and adjusting to fit on the screen happen.
SetPopupPosition(nullptr, false, false);
SetPopupPosition(nullptr, false, false, true);
}
bool

Просмотреть файл

@ -48,6 +48,8 @@ enum nsPopupState {
// state from when a popup is requested to be shown to after the
// popupshowing event has been fired.
ePopupShowing,
// state while a popup is waiting to be laid out and positioned
ePopupPositioning,
// state while a popup is open but the widget is not yet visible
ePopupOpening,
// state while a popup is visible and waiting for the popupshown event
@ -251,8 +253,10 @@ public:
// (or the frame for mAnchorContent if aAnchorFrame is null), anchored at a
// rectangle, or at a specific point if a screen position is set. The popup
// will be adjusted so that it is on screen. If aIsMove is true, then the
// popup is being moved, and should not be flipped.
nsresult SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aSizedToPopup);
// popup is being moved, and should not be flipped. If aNotify is true, then
// a popuppositioned event is sent.
nsresult SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove,
bool aSizedToPopup, bool aNotify);
bool HasGeneratedChildren() { return mGeneratedChildren; }
void SetGeneratedChildren() { mGeneratedChildren = true; }
@ -425,6 +429,11 @@ public:
return false;
}
void ShowWithPositionedEvent() {
mPopupState = ePopupPositioning;
mShouldAutoPosition = true;
}
// nsIReflowCallback
virtual bool ReflowFinished() override;
virtual void ReflowCallbackCanceled() override;
@ -524,6 +533,9 @@ protected:
RefPtr<nsXULPopupShownEvent> mPopupShownDispatcher;
// The popup's screen rectangle in app units.
nsIntRect mUsedScreenRect;
// A popup's preferred size may be different than its actual size stored in
// mRect in the case where the popup was resized because it was too large
// for the screen. The preferred size mPrefSize holds the full size the popup

Просмотреть файл

@ -444,7 +444,7 @@ nsXULPopupManager::AdjustPopupsOnWindowChange(nsPIDOMWindowOuter* aWindow)
}
for (int32_t l = list.Length() - 1; l >= 0; l--) {
list[l]->SetPopupPosition(nullptr, true, false);
list[l]->SetPopupPosition(nullptr, true, false, true);
}
}
@ -500,7 +500,7 @@ nsXULPopupManager::PopupMoved(nsIFrame* aFrame, nsIntPoint aPnt)
// the specified screen coordinates.
if (menuPopupFrame->IsAnchored() &&
menuPopupFrame->PopupLevel() == ePopupLevelParent) {
menuPopupFrame->SetPopupPosition(nullptr, true, false);
menuPopupFrame->SetPopupPosition(nullptr, true, false, true);
}
else {
CSSPoint cssPos = LayoutDeviceIntPoint::FromUnknownPoint(aPnt)
@ -1038,6 +1038,24 @@ nsXULPopupManager::HidePopup(nsIContent* aPopup,
}
else if (foundPanel) {
popupToHide = aPopup;
} else {
// When the popup is in the popuppositioning state, it will not be in the
// mPopups list. We need another way to find it and make sure it does not
// continue the popup showing process.
popupFrame = do_QueryFrame(aPopup->GetPrimaryFrame());
if (popupFrame) {
if (popupFrame->PopupState() == ePopupPositioning) {
// Do basically the same thing we would have done if we had found the
// popup in the mPopups list.
deselectMenu = aDeselectMenu;
popupToHide = aPopup;
type = popupFrame->PopupType();
} else {
// The popup is not positioning. If we were supposed to have handled
// closing it, it should have been in mPopups or mNoHidePanels
popupFrame = nullptr;
}
}
}
if (popupFrame) {
@ -1494,7 +1512,19 @@ nsXULPopupManager::FirePopupShowingEvent(nsIContent* aPopup,
popupFrame->ClearTriggerContent();
}
else {
ShowPopupCallback(aPopup, popupFrame, aIsContextMenu, aSelectFirstItem);
// Now check if we need to fire the popuppositioned event. If not, call
// ShowPopupCallback directly.
// The popuppositioned event only fires on arrow panels for now.
if (popup->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
nsGkAtoms::arrow, eCaseMatters)) {
popupFrame->ShowWithPositionedEvent();
presShell->FrameNeedsReflow(popupFrame, nsIPresShell::eTreeChange,
NS_FRAME_HAS_DIRTY_CHILDREN);
}
else {
ShowPopupCallback(popup, popupFrame, aIsContextMenu, aSelectFirstItem);
}
}
}
}
@ -1724,7 +1754,8 @@ nsXULPopupManager::MayShowPopup(nsMenuPopupFrame* aPopup)
// if the popup is not in the open popup chain, then it must have a state that
// is either closed, in the process of being shown, or invisible.
NS_ASSERTION(IsPopupOpen(aPopup->GetContent()) || state == ePopupClosed ||
state == ePopupShowing || state == ePopupInvisible,
state == ePopupShowing || state == ePopupPositioning ||
state == ePopupInvisible,
"popup not in XULPopupManager open list is open");
// don't show popups unless they are closed or invisible
@ -2705,6 +2736,61 @@ nsXULPopupHidingEvent::Run()
return NS_OK;
}
bool
nsXULPopupPositionedEvent::DispatchIfNeeded(nsIContent *aPopup,
bool aIsContextMenu,
bool aSelectFirstItem)
{
// The popuppositioned event only fires on arrow panels for now.
if (aPopup->AttrValueIs(kNameSpaceID_None, nsGkAtoms::type,
nsGkAtoms::arrow, eCaseMatters)) {
nsCOMPtr<nsIRunnable> event =
new nsXULPopupPositionedEvent(aPopup, aIsContextMenu, aSelectFirstItem);
NS_DispatchToCurrentThread(event);
return true;
}
return false;
}
NS_IMETHODIMP
nsXULPopupPositionedEvent::Run()
{
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
if (pm) {
nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
if (popupFrame) {
// At this point, hidePopup may have been called but it currently has no
// way to stop this event. However, if hidePopup was called, the popup
// will now be in the hiding or closed state. If we are in the shown or
// positioning state instead, we can assume that we are still clear to
// open/move the popup
nsPopupState state = popupFrame->PopupState();
if (state != ePopupPositioning && state != ePopupShown) {
return NS_OK;
}
nsEventStatus status = nsEventStatus_eIgnore;
WidgetMouseEvent event(true, eXULPopupPositioned, nullptr,
WidgetMouseEvent::eReal);
EventDispatcher::Dispatch(mPopup, popupFrame->PresContext(),
&event, nullptr, &status);
// Get the popup frame and make sure it is still in the positioning
// state. If it isn't, someone may have tried to reshow or hide it
// during the popuppositioned event.
// Alternately, this event may have been fired in reponse to moving the
// popup rather than opening it. In that case, we are done.
nsMenuPopupFrame* popupFrame = do_QueryFrame(mPopup->GetPrimaryFrame());
if (popupFrame && popupFrame->PopupState() == ePopupPositioning) {
pm->ShowPopupCallback(mPopup, popupFrame, mIsContextMenu, mSelectFirstItem);
}
}
}
return NS_OK;
}
NS_IMETHODIMP
nsXULMenuCommandEvent::Run()
{

Просмотреть файл

@ -237,6 +237,34 @@ private:
bool mIsRollup;
};
// this class is used for dispatching popuppositioned events asynchronously.
class nsXULPopupPositionedEvent : public mozilla::Runnable
{
public:
explicit nsXULPopupPositionedEvent(nsIContent *aPopup,
bool aIsContextMenu,
bool aSelectFirstItem)
: mPopup(aPopup)
, mIsContextMenu(aIsContextMenu)
, mSelectFirstItem(aSelectFirstItem)
{
NS_ASSERTION(aPopup, "null popup supplied to nsXULPopupShowingEvent constructor");
}
NS_IMETHOD Run() override;
// Asynchronously dispatch a popuppositioned event at aPopup if this is a
// panel that should receieve such events. Return true if the event was sent.
static bool DispatchIfNeeded(nsIContent *aPopup,
bool aIsContextMenu,
bool aSelectFirstItem);
private:
nsCOMPtr<nsIContent> mPopup;
bool mIsContextMenu;
bool mSelectFirstItem;
};
// this class is used for dispatching menu command events asynchronously.
class nsXULMenuCommandEvent : public mozilla::Runnable
{
@ -287,6 +315,7 @@ class nsXULPopupManager final : public nsIDOMEventListener,
public:
friend class nsXULPopupShowingEvent;
friend class nsXULPopupHidingEvent;
friend class nsXULPopupPositionedEvent;
friend class nsXULMenuCommandEvent;
friend class TransitionEnder;

Просмотреть файл

@ -88,6 +88,15 @@ function openPopup(position, callback) {
_openPopup(position, callback);
}
function waitForPopupPositioned(actionFn, callback)
{
panel.addEventListener("popuppositioned", function listener() {
panel.removeEventListener("popuppositioned", listener, false);
callback();
}, false);
actionFn();
}
function _openPopup(position, callback) {
// this is very ugly: the panel CSS sets the arrow's list-style-image based
// on the 'side' attribute. If the setting of the 'side' attribute causes
@ -200,14 +209,22 @@ var tests = [
// and the anchor right - it can't fit with the panel on the left/arrow
// on the right, so it must flip (arrow on the left, panel on the right)
var offset = Math.floor(-anchorRight / 2);
panel.moveToAnchor(anchor, "after_end", offset, 0);
waitForPopupPositioned(
() => panel.moveToAnchor(anchor, "after_end", offset, 0),
() => {
isArrowPositionedOn("left", offset); // should have flipped and have the offset.
// resize back to original and move to a zero offset - it should flip back.
panel.sizeTo(anchorRight - 10, 100);
panel.moveToAnchor(anchor, "after_end", 0, 0);
waitForPopupPositioned(
() => panel.moveToAnchor(anchor, "after_end", 0, 0),
() => {
isArrowPositionedOn("right"); // should have flipped back and no offset
next();
});
});
});
}],
// Do a moveToAnchor that causes the panel to flip vertically
@ -219,13 +236,21 @@ var tests = [
openPopup("start_after", function() {
isArrowPositionedOn("bottom");
var offset = Math.floor(-anchorBottom / 2);
panel.moveToAnchor(anchor, "start_after", 0, offset);
waitForPopupPositioned(
() => panel.moveToAnchor(anchor, "start_after", 0, offset),
() => {
isArrowPositionedOn("top", offset);
panel.sizeTo(100, anchorBottom - 10);
panel.moveToAnchor(anchor, "start_after", 0, 0);
waitForPopupPositioned(
() => panel.moveToAnchor(anchor, "start_after", 0, 0),
() => {
isArrowPositionedOn("bottom");
next();
});
});
});
}],
['veryWidePanel-after_end', 'middle', function(next) {
@ -379,10 +404,8 @@ SimpleTest.waitForExplicitFinish();
addEventListener("load", function() {
// anchor is set by the test runner above
panel = document.getElementById("testPanel");
arrow = SpecialPowers.wrap(document).getAnonymousElementByAttribute(panel, "anonid", "arrow");
// Cancel the arrow panel slide-in transition (bug 767133) so the size and
// position are "stable" enough to test without jumping through hoops...
arrow.style.transition = "none";
runTests();
});

Просмотреть файл

@ -374,19 +374,9 @@
<body>
<![CDATA[
this.popupBoxObject.sizeTo(aWidth, aHeight);
if (this.state == "open")
this.adjustArrowPosition();
]]>
</body>
</method>
<method name="moveTo">
<parameter name="aLeft"/>
<parameter name="aTop"/>
<body>
<![CDATA[
this.popupBoxObject.moveTo(aLeft, aTop);
if (this.state == "open")
if (this.state == "open") {
this.adjustArrowPosition();
}
]]>
</body>
</method>
@ -399,8 +389,6 @@
<body>
<![CDATA[
this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride);
if (this.state == "open")
this.adjustArrowPosition();
]]>
</body>
</method>
@ -411,7 +399,6 @@
var anchor = this.anchorNode;
if (!anchor) {
arrow.hidden = true;
return;
}
@ -423,8 +410,6 @@
this.setAttribute("arrowposition", position);
// if this panel has a "sliding" arrow, we may have previously set margins...
arrowbox.style.removeProperty("transform");
if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
container.orient = "horizontal";
arrowbox.orient = "vertical";
@ -466,8 +451,6 @@
this.setAttribute("side", "top");
}
}
arrow.hidden = false;
]]>
</body>
</method>
@ -475,7 +458,13 @@
<handlers>
<handler event="popupshowing" phase="target">
<![CDATA[
var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
arrow.hidden = this.anchorNode == null;
document.getAnonymousElementByAttribute(this, "anonid", "arrowbox")
.style.removeProperty("transform");
this.adjustArrowPosition();
if (this.getAttribute("animate") != "false") {
this.setAttribute("animate", "open");
}
@ -518,6 +507,9 @@
this.removeAttribute("animate");
}
</handler>
<handler event="popuppositioned" phase="target">
this.adjustArrowPosition();
</handler>
</handlers>
</binding>

Просмотреть файл

@ -848,13 +848,35 @@ PopupNotifications.prototype = {
// the next reason will be that the user clicked elsewhere.
this.nextDismissReason = TELEMETRY_STAT_DISMISSAL_CLICK_ELSEWHERE;
this.panel.openPopup(anchorElement, "bottomcenter topleft");
let target = this.panel;
if (target.parentNode) {
// NOTIFICATION_EVENT_SHOWN should be fired for the panel before
// anyone listening for popupshown on the panel gets run. Otherwise,
// the panel will not be initialized when the popupshown event
// listeners run.
// By targeting the panel's parent and using a capturing listener, we
// can have our listener called before others waiting for the panel to
// be shown (which probably expect the panel to be fully initialized)
target = target.parentNode;
}
if (this._popupshownListener) {
target.removeEventListener("popupshown", this._popupshownListener, true);
}
this._popupshownListener = function (e) {
target.removeEventListener("popupshown", this._popupshownListener, true);
this._popupshownListener = null;
notificationsToShow.forEach(function (n) {
this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
}, this);
// This notification is used by tests to know when all the processing
// required to display the panel has happened.
this.panel.dispatchEvent(new this.window.CustomEvent("Shown"));
};
this._popupshownListener = this._popupshownListener.bind(this);
target.addEventListener("popupshown", this._popupshownListener, true);
this.panel.openPopup(anchorElement, "bottomcenter topleft");
});
},

Просмотреть файл

@ -142,6 +142,7 @@ NS_EVENT_MESSAGE_FIRST_LAST(eDragDropEvent, eDragEnter, eDragLeave)
// XUL specific events
NS_EVENT_MESSAGE(eXULPopupShowing)
NS_EVENT_MESSAGE(eXULPopupShown)
NS_EVENT_MESSAGE(eXULPopupPositioned)
NS_EVENT_MESSAGE(eXULPopupHiding)
NS_EVENT_MESSAGE(eXULPopupHidden)
NS_EVENT_MESSAGE(eXULBroadcast)