/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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/. */ #include "SimpleXULLeafFrame.h" #include "mozilla/AsyncEventDispatcher.h" #include "mozilla/ContentEvents.h" #include "mozilla/DebugOnly.h" #include "mozilla/EventDispatcher.h" #include "mozilla/gfx/2D.h" #include "mozilla/gfx/PathHelpers.h" #include "mozilla/Likely.h" #include "mozilla/LookAndFeel.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/MouseEvents.h" #include "mozilla/PresShell.h" #include "mozilla/ResultExtensions.h" #include "mozilla/Try.h" #include "mozilla/intl/Segmenter.h" #include "gfxUtils.h" #include "nsAlgorithm.h" #include "nsCOMPtr.h" #include "nsComponentManagerUtils.h" #include "nsFontMetrics.h" #include "nsITreeView.h" #include "nsPresContext.h" #include "nsNameSpaceManager.h" #include "nsTreeBodyFrame.h" #include "nsTreeSelection.h" #include "nsTreeImageListener.h" #include "nsGkAtoms.h" #include "nsCSSAnonBoxes.h" #include "gfxContext.h" #include "nsIContent.h" #include "mozilla/ComputedStyle.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/ReferrerInfo.h" #include "mozilla/intl/Segmenter.h" #include "nsCSSRendering.h" #include "nsString.h" #include "nsContainerFrame.h" #include "nsView.h" #include "nsViewManager.h" #include "nsVariant.h" #include "nsWidgetsCID.h" #include "nsIFrameInlines.h" #include "nsTreeContentView.h" #include "nsTreeUtils.h" #include "nsStyleConsts.h" #include "nsITheme.h" #include "imgIRequest.h" #include "imgIContainer.h" #include "mozilla/dom/NodeInfo.h" #include "nsContentUtils.h" #include "nsLayoutUtils.h" #include "nsIScrollableFrame.h" #include "nsDisplayList.h" #include "mozilla/dom/CustomEvent.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/dom/ToJSValue.h" #include "mozilla/dom/TreeColumnBinding.h" #include #include "ScrollbarActivity.h" #ifdef ACCESSIBILITY # include "nsAccessibilityService.h" # include "nsIWritablePropertyBag2.h" #endif #include "nsBidiUtils.h" using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::gfx; using namespace mozilla::image; using namespace mozilla::layout; enum CroppingStyle { CropNone, CropLeft, CropRight, CropCenter, CropAuto }; // FIXME: Maybe unify with MiddleCroppingBlockFrame? static void CropStringForWidth(nsAString& aText, gfxContext& aRenderingContext, nsFontMetrics& aFontMetrics, nscoord aWidth, CroppingStyle aCropType) { DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); // See if the width is even smaller than the ellipsis // If so, clear the text completely. const nsDependentString& kEllipsis = nsContentUtils::GetLocalizedEllipsis(); aFontMetrics.SetTextRunRTL(false); nscoord ellipsisWidth = nsLayoutUtils::AppUnitWidthOfString(kEllipsis, aFontMetrics, drawTarget); if (ellipsisWidth > aWidth) { aText.Truncate(0); return; } if (ellipsisWidth == aWidth) { aText.Assign(kEllipsis); return; } // We will be drawing an ellipsis, thank you very much. // Subtract out the required width of the ellipsis. // This is the total remaining width we have to play with. aWidth -= ellipsisWidth; using mozilla::intl::GraphemeClusterBreakIteratorUtf16; using mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16; // Now we crop. This is quite basic: it will not be really accurate in the // presence of complex scripts with contextual shaping, etc., as it measures // each grapheme cluster in isolation, not in its proper context. switch (aCropType) { case CropAuto: case CropNone: case CropRight: { const Span text(aText); GraphemeClusterBreakIteratorUtf16 iter(text); uint32_t pos = 0; nscoord totalWidth = 0; while (Maybe nextPos = iter.Next()) { const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( text.FromTo(pos, *nextPos), aFontMetrics, drawTarget); if (totalWidth + charWidth > aWidth) { break; } pos = *nextPos; totalWidth += charWidth; } if (pos < aText.Length()) { aText.Replace(pos, aText.Length() - pos, kEllipsis); } } break; case CropLeft: { const Span text(aText); GraphemeClusterBreakReverseIteratorUtf16 iter(text); uint32_t pos = text.Length(); nscoord totalWidth = 0; // nextPos is decreasing since we use a reverse iterator. while (Maybe nextPos = iter.Next()) { const nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( text.FromTo(*nextPos, pos), aFontMetrics, drawTarget); if (totalWidth + charWidth > aWidth) { break; } pos = *nextPos; totalWidth += charWidth; } if (pos > 0) { aText.Replace(0, pos, kEllipsis); } } break; case CropCenter: { const Span text(aText); nscoord totalWidth = 0; GraphemeClusterBreakIteratorUtf16 leftIter(text); GraphemeClusterBreakReverseIteratorUtf16 rightIter(text); uint32_t leftPos = 0; uint32_t rightPos = text.Length(); while (leftPos < rightPos) { Maybe nextPos = leftIter.Next(); nscoord charWidth = nsLayoutUtils::AppUnitWidthOfString( text.FromTo(leftPos, *nextPos), aFontMetrics, drawTarget); if (totalWidth + charWidth > aWidth) { break; } leftPos = *nextPos; totalWidth += charWidth; if (leftPos >= rightPos) { break; } nextPos = rightIter.Next(); charWidth = nsLayoutUtils::AppUnitWidthOfString( text.FromTo(*nextPos, rightPos), aFontMetrics, drawTarget); if (totalWidth + charWidth > aWidth) { break; } rightPos = *nextPos; totalWidth += charWidth; } if (leftPos < rightPos) { aText.Replace(leftPos, rightPos - leftPos, kEllipsis); } } break; } } // Function that cancels all the image requests in our cache. void nsTreeBodyFrame::CancelImageRequests() { for (nsTreeImageCacheEntry entry : mImageCache.Values()) { // If our imgIRequest object was registered with the refresh driver // then we need to deregister it. nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request, nullptr); entry.request->UnlockImage(); entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED); } } // // NS_NewTreeFrame // // Creates a new tree frame // nsIFrame* NS_NewTreeBodyFrame(PresShell* aPresShell, ComputedStyle* aStyle) { return new (aPresShell) nsTreeBodyFrame(aStyle, aPresShell->GetPresContext()); } NS_IMPL_FRAMEARENA_HELPERS(nsTreeBodyFrame) NS_QUERYFRAME_HEAD(nsTreeBodyFrame) NS_QUERYFRAME_ENTRY(nsIScrollbarMediator) NS_QUERYFRAME_ENTRY(nsTreeBodyFrame) NS_QUERYFRAME_TAIL_INHERITING(SimpleXULLeafFrame) // Constructor nsTreeBodyFrame::nsTreeBodyFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) : SimpleXULLeafFrame(aStyle, aPresContext, kClassID), mTopRowIndex(0), mPageLength(0), mHorzPosition(0), mOriginalHorzWidth(-1), mHorzWidth(0), mAdjustWidth(0), mRowHeight(0), mIndentation(0), mUpdateBatchNest(0), mRowCount(0), mMouseOverRow(-1), mFocused(false), mHasFixedRowCount(false), mVerticalOverflow(false), mHorizontalOverflow(false), mReflowCallbackPosted(false), mCheckingOverflow(false) { mColumns = new nsTreeColumns(this); } // Destructor nsTreeBodyFrame::~nsTreeBodyFrame() { CancelImageRequests(); DetachImageListeners(); } static void GetBorderPadding(ComputedStyle* aStyle, nsMargin& aMargin) { aMargin.SizeTo(0, 0, 0, 0); aStyle->StylePadding()->GetPadding(aMargin); aMargin += aStyle->StyleBorder()->GetComputedBorder(); } static void AdjustForBorderPadding(ComputedStyle* aStyle, nsRect& aRect) { nsMargin borderPadding(0, 0, 0, 0); GetBorderPadding(aStyle, borderPadding); aRect.Deflate(borderPadding); } void nsTreeBodyFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, nsIFrame* aPrevInFlow) { SimpleXULLeafFrame::Init(aContent, aParent, aPrevInFlow); mIndentation = GetIndentation(); mRowHeight = GetRowHeight(); // Call GetBaseElement so that mTree is assigned. RefPtr tree(GetBaseElement()); if (MOZ_LIKELY(tree)) { nsAutoString rows; if (tree->GetAttr(nsGkAtoms::rows, rows)) { nsresult err; mPageLength = rows.ToInteger(&err); mHasFixedRowCount = true; } } if (PresContext()->UseOverlayScrollbars()) { mScrollbarActivity = new ScrollbarActivity(static_cast(this)); } } void nsTreeBodyFrame::Destroy(DestroyContext& aContext) { if (mScrollbarActivity) { mScrollbarActivity->Destroy(); mScrollbarActivity = nullptr; } mScrollEvent.Revoke(); // Make sure we cancel any posted callbacks. if (mReflowCallbackPosted) { PresShell()->CancelReflowCallback(this); mReflowCallbackPosted = false; } if (mColumns) mColumns->SetTree(nullptr); RefPtr tree = mTree; if (nsCOMPtr view = std::move(mView)) { nsCOMPtr sel; view->GetSelection(getter_AddRefs(sel)); if (sel) { sel->SetTree(nullptr); } view->SetTree(nullptr); } // Make this call now because view->SetTree can run js which can undo this // call. if (tree) { tree->BodyDestroyed(mTopRowIndex); } if (mTree && mTree != tree) { mTree->BodyDestroyed(mTopRowIndex); } SimpleXULLeafFrame::Destroy(aContext); } void nsTreeBodyFrame::EnsureView() { if (mView) { return; } if (PresShell()->IsReflowLocked()) { if (!mReflowCallbackPosted) { mReflowCallbackPosted = true; PresShell()->PostReflowCallback(this); } return; } AutoWeakFrame weakFrame(this); RefPtr tree = GetBaseElement(); if (!tree) { return; } nsCOMPtr treeView = tree->GetView(); if (!treeView || !weakFrame.IsAlive()) { return; } int32_t rowIndex = tree->GetCachedTopVisibleRow(); // Set our view. SetView(treeView); NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); // Scroll to the given row. // XXX is this optimal if we haven't laid out yet? ScrollToRow(rowIndex); NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); } void nsTreeBodyFrame::ManageReflowCallback() { const nscoord horzWidth = CalcHorzWidth(GetScrollParts()); if (!mReflowCallbackPosted) { if (!mLastReflowRect || !mLastReflowRect->IsEqualEdges(mRect) || mHorzWidth != horzWidth) { PresShell()->PostReflowCallback(this); mReflowCallbackPosted = true; mOriginalHorzWidth = mHorzWidth; } } else if (mHorzWidth != horzWidth && mOriginalHorzWidth == horzWidth) { // FIXME(emilio): This doesn't seem sound to me, if the rect changes in the // block axis. PresShell()->CancelReflowCallback(this); mReflowCallbackPosted = false; mOriginalHorzWidth = -1; } mLastReflowRect = Some(mRect); mHorzWidth = horzWidth; } nscoord nsTreeBodyFrame::GetIntrinsicBSize() { return mHasFixedRowCount ? mRowHeight * mPageLength : 0; } void nsTreeBodyFrame::DidReflow(nsPresContext* aPresContext, const ReflowInput* aReflowInput) { ManageReflowCallback(); SimpleXULLeafFrame::DidReflow(aPresContext, aReflowInput); } bool nsTreeBodyFrame::ReflowFinished() { if (!mView) { AutoWeakFrame weakFrame(this); EnsureView(); NS_ENSURE_TRUE(weakFrame.IsAlive(), false); } if (mView) { CalcInnerBox(); ScrollParts parts = GetScrollParts(); mHorzWidth = CalcHorzWidth(parts); if (!mHasFixedRowCount) { mPageLength = (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount; } int32_t lastPageTopRow = std::max(0, mRowCount - mPageLength); if (mTopRowIndex > lastPageTopRow) ScrollToRowInternal(parts, lastPageTopRow); XULTreeElement* treeContent = GetBaseElement(); if (treeContent && treeContent->AttrValueIs( kNameSpaceID_None, nsGkAtoms::keepcurrentinview, nsGkAtoms::_true, eCaseMatters)) { // make sure that the current selected item is still // visible after the tree changes size. if (nsCOMPtr sel = GetSelection()) { int32_t currentIndex; sel->GetCurrentIndex(¤tIndex); if (currentIndex != -1) { EnsureRowIsVisibleInternal(parts, currentIndex); } } } if (!FullScrollbarsUpdate(false)) { return false; } } mReflowCallbackPosted = false; return false; } void nsTreeBodyFrame::ReflowCallbackCanceled() { mReflowCallbackPosted = false; } nsresult nsTreeBodyFrame::GetView(nsITreeView** aView) { *aView = nullptr; AutoWeakFrame weakFrame(this); EnsureView(); NS_ENSURE_STATE(weakFrame.IsAlive()); NS_IF_ADDREF(*aView = mView); return NS_OK; } nsresult nsTreeBodyFrame::SetView(nsITreeView* aView) { if (aView == mView) { return NS_OK; } // First clear out the old view. nsCOMPtr oldView = std::move(mView); if (oldView) { AutoWeakFrame weakFrame(this); nsCOMPtr sel; oldView->GetSelection(getter_AddRefs(sel)); if (sel) { sel->SetTree(nullptr); } oldView->SetTree(nullptr); NS_ENSURE_STATE(weakFrame.IsAlive()); // Only reset the top row index and delete the columns if we had an old // non-null view. mTopRowIndex = 0; } // Tree, meet the view. mView = aView; // Changing the view causes us to refetch our data. This will // necessarily entail a full invalidation of the tree. Invalidate(); RefPtr treeContent = GetBaseElement(); if (treeContent) { #ifdef ACCESSIBILITY if (nsAccessibilityService* accService = GetAccService()) { accService->TreeViewChanged(PresContext()->GetPresShell(), treeContent, mView); } #endif // #ifdef ACCESSIBILITY FireDOMEvent(u"TreeViewChanged"_ns, treeContent); } if (aView) { // Give the view a new empty selection object to play with, but only if it // doesn't have one already. nsCOMPtr sel; aView->GetSelection(getter_AddRefs(sel)); if (sel) { sel->SetTree(treeContent); } else { NS_NewTreeSelection(treeContent, getter_AddRefs(sel)); aView->SetSelection(sel); } // View, meet the tree. AutoWeakFrame weakFrame(this); aView->SetTree(treeContent); NS_ENSURE_STATE(weakFrame.IsAlive()); aView->GetRowCount(&mRowCount); if (!PresShell()->IsReflowLocked()) { // The scrollbar will need to be updated. FullScrollbarsUpdate(false); } else if (!mReflowCallbackPosted) { mReflowCallbackPosted = true; PresShell()->PostReflowCallback(this); } } return NS_OK; } already_AddRefed nsTreeBodyFrame::GetSelection() const { nsCOMPtr sel; if (nsCOMPtr view = GetExistingView()) { view->GetSelection(getter_AddRefs(sel)); } return sel.forget(); } nsresult nsTreeBodyFrame::SetFocused(bool aFocused) { if (mFocused != aFocused) { mFocused = aFocused; if (nsCOMPtr sel = GetSelection()) { sel->InvalidateSelection(); } } return NS_OK; } nsresult nsTreeBodyFrame::GetTreeBody(Element** aElement) { // NS_ASSERTION(mContent, "no content, see bug #104878"); if (!mContent) return NS_ERROR_NULL_POINTER; RefPtr element = mContent->AsElement(); element.forget(aElement); return NS_OK; } int32_t nsTreeBodyFrame::RowHeight() const { return nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); } int32_t nsTreeBodyFrame::RowWidth() { return nsPresContext::AppUnitsToIntCSSPixels(CalcHorzWidth(GetScrollParts())); } int32_t nsTreeBodyFrame::GetHorizontalPosition() const { return nsPresContext::AppUnitsToIntCSSPixels(mHorzPosition); } Maybe nsTreeBodyFrame::GetSelectionRegion() { if (!mView) { return Nothing(); } AutoWeakFrame wf(this); nsCOMPtr selection = GetSelection(); if (!selection || !wf.IsAlive()) { return Nothing(); } nsIntRect rect = mRect.ToOutsidePixels(AppUnitsPerCSSPixel()); nsIFrame* rootFrame = PresShell()->GetRootFrame(); nsPoint origin = GetOffsetTo(rootFrame); CSSIntRegion region; // iterate through the visible rows and add the selected ones to the // drag region int32_t x = nsPresContext::AppUnitsToIntCSSPixels(origin.x); int32_t y = nsPresContext::AppUnitsToIntCSSPixels(origin.y); int32_t top = y; int32_t end = LastVisibleRow(); int32_t rowHeight = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); for (int32_t i = mTopRowIndex; i <= end; i++) { bool isSelected; selection->IsSelected(i, &isSelected); if (isSelected) { region.OrWith(CSSIntRect(x, y, rect.width, rowHeight)); } y += rowHeight; } // clip to the tree boundary in case one row extends past it region.AndWith(CSSIntRect(x, top, rect.width, rect.height)); return Some(region); } nsresult nsTreeBodyFrame::Invalidate() { if (mUpdateBatchNest) return NS_OK; InvalidateFrame(); return NS_OK; } nsresult nsTreeBodyFrame::InvalidateColumn(nsTreeColumn* aCol) { if (mUpdateBatchNest) return NS_OK; if (!aCol) return NS_ERROR_INVALID_ARG; #ifdef ACCESSIBILITY if (GetAccService()) { FireInvalidateEvent(-1, -1, aCol, aCol); } #endif // #ifdef ACCESSIBILITY nsRect columnRect; nsresult rv = aCol->GetRect(this, mInnerBox.y, mInnerBox.height, &columnRect); NS_ENSURE_SUCCESS(rv, rv); // When false then column is out of view if (OffsetForHorzScroll(columnRect, true)) InvalidateFrameWithRect(columnRect); return NS_OK; } nsresult nsTreeBodyFrame::InvalidateRow(int32_t aIndex) { if (mUpdateBatchNest) return NS_OK; #ifdef ACCESSIBILITY if (GetAccService()) { FireInvalidateEvent(aIndex, aIndex, nullptr, nullptr); } #endif // #ifdef ACCESSIBILITY aIndex -= mTopRowIndex; if (aIndex < 0 || aIndex > mPageLength) return NS_OK; nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * aIndex, mInnerBox.width, mRowHeight); InvalidateFrameWithRect(rowRect); return NS_OK; } nsresult nsTreeBodyFrame::InvalidateCell(int32_t aIndex, nsTreeColumn* aCol) { if (mUpdateBatchNest) return NS_OK; #ifdef ACCESSIBILITY if (GetAccService()) { FireInvalidateEvent(aIndex, aIndex, aCol, aCol); } #endif // #ifdef ACCESSIBILITY aIndex -= mTopRowIndex; if (aIndex < 0 || aIndex > mPageLength) return NS_OK; if (!aCol) return NS_ERROR_INVALID_ARG; nsRect cellRect; nsresult rv = aCol->GetRect(this, mInnerBox.y + mRowHeight * aIndex, mRowHeight, &cellRect); NS_ENSURE_SUCCESS(rv, rv); if (OffsetForHorzScroll(cellRect, true)) InvalidateFrameWithRect(cellRect); return NS_OK; } nsresult nsTreeBodyFrame::InvalidateRange(int32_t aStart, int32_t aEnd) { if (mUpdateBatchNest) return NS_OK; if (aStart == aEnd) return InvalidateRow(aStart); int32_t last = LastVisibleRow(); if (aStart > aEnd || aEnd < mTopRowIndex || aStart > last) return NS_OK; if (aStart < mTopRowIndex) aStart = mTopRowIndex; if (aEnd > last) aEnd = last; #ifdef ACCESSIBILITY if (GetAccService()) { int32_t end = mRowCount > 0 ? ((mRowCount <= aEnd) ? mRowCount - 1 : aEnd) : 0; FireInvalidateEvent(aStart, end, nullptr, nullptr); } #endif // #ifdef ACCESSIBILITY nsRect rangeRect(mInnerBox.x, mInnerBox.y + mRowHeight * (aStart - mTopRowIndex), mInnerBox.width, mRowHeight * (aEnd - aStart + 1)); InvalidateFrameWithRect(rangeRect); return NS_OK; } static void FindScrollParts(nsIFrame* aCurrFrame, nsTreeBodyFrame::ScrollParts* aResult) { if (!aResult->mColumnsScrollFrame) { nsIScrollableFrame* f = do_QueryFrame(aCurrFrame); if (f) { aResult->mColumnsFrame = aCurrFrame; aResult->mColumnsScrollFrame = f; } } if (nsScrollbarFrame* sf = do_QueryFrame(aCurrFrame)) { if (!sf->IsHorizontal()) { if (!aResult->mVScrollbar) { aResult->mVScrollbar = sf; } } else { if (!aResult->mHScrollbar) { aResult->mHScrollbar = sf; } } // don't bother searching inside a scrollbar return; } nsIFrame* child = aCurrFrame->PrincipalChildList().FirstChild(); while (child && !child->GetContent()->IsRootOfNativeAnonymousSubtree() && (!aResult->mVScrollbar || !aResult->mHScrollbar || !aResult->mColumnsScrollFrame)) { FindScrollParts(child, aResult); child = child->GetNextSibling(); } } nsTreeBodyFrame::ScrollParts nsTreeBodyFrame::GetScrollParts() { ScrollParts result = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; XULTreeElement* tree = GetBaseElement(); if (nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr) { // The way we do this, searching through the entire frame subtree, is pretty // dumb! We should know where these frames are. FindScrollParts(treeFrame, &result); if (result.mHScrollbar) { result.mHScrollbar->SetScrollbarMediatorContent(GetContent()); result.mHScrollbarContent = result.mHScrollbar->GetContent()->AsElement(); } if (result.mVScrollbar) { result.mVScrollbar->SetScrollbarMediatorContent(GetContent()); result.mVScrollbarContent = result.mVScrollbar->GetContent()->AsElement(); } } return result; } void nsTreeBodyFrame::UpdateScrollbars(const ScrollParts& aParts) { nscoord rowHeightAsPixels = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); AutoWeakFrame weakFrame(this); if (aParts.mVScrollbar) { nsAutoString curPos; curPos.AppendInt(mTopRowIndex * rowHeightAsPixels); aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, curPos, true); // 'this' might be deleted here } if (weakFrame.IsAlive() && aParts.mHScrollbar) { nsAutoString curPos; curPos.AppendInt(mHorzPosition); aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::curpos, curPos, true); // 'this' might be deleted here } if (weakFrame.IsAlive() && mScrollbarActivity) { mScrollbarActivity->ActivityOccurred(); } } void nsTreeBodyFrame::CheckOverflow(const ScrollParts& aParts) { bool verticalOverflowChanged = false; bool horizontalOverflowChanged = false; if (!mVerticalOverflow && mRowCount > mPageLength) { mVerticalOverflow = true; verticalOverflowChanged = true; } else if (mVerticalOverflow && mRowCount <= mPageLength) { mVerticalOverflow = false; verticalOverflowChanged = true; } if (aParts.mColumnsFrame) { nsRect bounds = aParts.mColumnsFrame->GetRect(); if (bounds.width != 0) { /* Ignore overflows that are less than half a pixel. Yes these happen all over the place when flex boxes are compressed real small. Probably a result of a rounding errors somewhere in the layout code. */ bounds.width += nsPresContext::CSSPixelsToAppUnits(0.5f); if (!mHorizontalOverflow && bounds.width < mHorzWidth) { mHorizontalOverflow = true; horizontalOverflowChanged = true; } else if (mHorizontalOverflow && bounds.width >= mHorzWidth) { mHorizontalOverflow = false; horizontalOverflowChanged = true; } } } if (!horizontalOverflowChanged && !verticalOverflowChanged) { return; } AutoWeakFrame weakFrame(this); RefPtr presContext = PresContext(); RefPtr presShell = presContext->GetPresShell(); nsCOMPtr content = mContent; if (verticalOverflowChanged) { InternalScrollPortEvent event( true, mVerticalOverflow ? eScrollPortOverflow : eScrollPortUnderflow, nullptr); event.mOrient = InternalScrollPortEvent::eVertical; EventDispatcher::Dispatch(content, presContext, &event); } if (horizontalOverflowChanged) { InternalScrollPortEvent event( true, mHorizontalOverflow ? eScrollPortOverflow : eScrollPortUnderflow, nullptr); event.mOrient = InternalScrollPortEvent::eHorizontal; EventDispatcher::Dispatch(content, presContext, &event); } // The synchronous event dispatch above can trigger reflow notifications. // Flush those explicitly now, so that we can guard against potential infinite // recursion. See bug 905909. if (!weakFrame.IsAlive()) { return; } NS_ASSERTION(!mCheckingOverflow, "mCheckingOverflow should not already be set"); // Don't use AutoRestore since we want to not touch mCheckingOverflow if we // fail the weakFrame.IsAlive() check below mCheckingOverflow = true; presShell->FlushPendingNotifications(FlushType::Layout); if (!weakFrame.IsAlive()) { return; } mCheckingOverflow = false; } void nsTreeBodyFrame::InvalidateScrollbars(const ScrollParts& aParts, AutoWeakFrame& aWeakColumnsFrame) { if (mUpdateBatchNest || !mView) return; AutoWeakFrame weakFrame(this); if (aParts.mVScrollbar) { // Do Vertical Scrollbar nsAutoString maxposStr; nscoord rowHeightAsPixels = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); int32_t size = rowHeightAsPixels * (mRowCount > mPageLength ? mRowCount - mPageLength : 0); maxposStr.AppendInt(size); aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos, maxposStr, true); NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); // Also set our page increment and decrement. nscoord pageincrement = mPageLength * rowHeightAsPixels; nsAutoString pageStr; pageStr.AppendInt(pageincrement); aParts.mVScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::pageincrement, pageStr, true); NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); } if (aParts.mHScrollbar && aParts.mColumnsFrame && aWeakColumnsFrame.IsAlive()) { // And now Horizontal scrollbar nsRect bounds = aParts.mColumnsFrame->GetRect(); nsAutoString maxposStr; maxposStr.AppendInt(mHorzWidth > bounds.width ? mHorzWidth - bounds.width : 0); aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::maxpos, maxposStr, true); NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); nsAutoString pageStr; pageStr.AppendInt(bounds.width); aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::pageincrement, pageStr, true); NS_ENSURE_TRUE_VOID(weakFrame.IsAlive()); pageStr.Truncate(); pageStr.AppendInt(nsPresContext::CSSPixelsToAppUnits(16)); aParts.mHScrollbarContent->SetAttr(kNameSpaceID_None, nsGkAtoms::increment, pageStr, true); } if (weakFrame.IsAlive() && mScrollbarActivity) { mScrollbarActivity->ActivityOccurred(); } } // Takes client x/y in pixels, converts them to appunits, and converts into // values relative to this nsTreeBodyFrame frame. nsPoint nsTreeBodyFrame::AdjustClientCoordsToBoxCoordSpace(int32_t aX, int32_t aY) { nsPoint point(nsPresContext::CSSPixelsToAppUnits(aX), nsPresContext::CSSPixelsToAppUnits(aY)); nsPresContext* presContext = PresContext(); point -= GetOffsetTo(presContext->GetPresShell()->GetRootFrame()); // Adjust by the inner box coords, so that we're in the inner box's // coordinate space. point -= mInnerBox.TopLeft(); return point; } // AdjustClientCoordsToBoxCoordSpace int32_t nsTreeBodyFrame::GetRowAt(int32_t aX, int32_t aY) { if (!mView) { return 0; } nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY); // Check if the coordinates are above our visible space. if (point.y < 0) { return -1; } return GetRowAtInternal(point.x, point.y); } nsresult nsTreeBodyFrame::GetCellAt(int32_t aX, int32_t aY, int32_t* aRow, nsTreeColumn** aCol, nsACString& aChildElt) { if (!mView) return NS_OK; nsPoint point = AdjustClientCoordsToBoxCoordSpace(aX, aY); // Check if the coordinates are above our visible space. if (point.y < 0) { *aRow = -1; return NS_OK; } nsTreeColumn* col; nsCSSAnonBoxPseudoStaticAtom* child; GetCellAt(point.x, point.y, aRow, &col, &child); if (col) { NS_ADDREF(*aCol = col); if (child == nsCSSAnonBoxes::mozTreeCell()) aChildElt.AssignLiteral("cell"); else if (child == nsCSSAnonBoxes::mozTreeTwisty()) aChildElt.AssignLiteral("twisty"); else if (child == nsCSSAnonBoxes::mozTreeImage()) aChildElt.AssignLiteral("image"); else if (child == nsCSSAnonBoxes::mozTreeCellText()) aChildElt.AssignLiteral("text"); } return NS_OK; } // // GetCoordsForCellItem // // Find the x/y location and width/height (all in PIXELS) of the given object // in the given column. // // XXX IMPORTANT XXX: // Hyatt says in the bug for this, that the following needs to be done: // (1) You need to deal with overflow when computing cell rects. See other // column iteration examples... if you don't deal with this, you'll mistakenly // extend the cell into the scrollbar's rect. // // (2) You are adjusting the cell rect by the *row" border padding. That's // wrong. You need to first adjust a row rect by its border/padding, and then // the cell rect fits inside the adjusted row rect. It also can have // border/padding as well as margins. The vertical direction isn't that // important, but you need to get the horizontal direction right. // // (3) GetImageSize() does not include margins (but it does include // border/padding). You need to make sure to add in the image's margins as well. // nsresult nsTreeBodyFrame::GetCoordsForCellItem(int32_t aRow, nsTreeColumn* aCol, const nsACString& aElement, int32_t* aX, int32_t* aY, int32_t* aWidth, int32_t* aHeight) { *aX = 0; *aY = 0; *aWidth = 0; *aHeight = 0; bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; nscoord currX = mInnerBox.x - mHorzPosition; // The Rect for the requested item. nsRect theRect; nsPresContext* presContext = PresContext(); nsCOMPtr view = GetExistingView(); for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; currCol = currCol->GetNext()) { // The Rect for the current cell. nscoord colWidth; #ifdef DEBUG nsresult rv = #endif currCol->GetWidthInTwips(this, &colWidth); NS_ASSERTION(NS_SUCCEEDED(rv), "invalid column"); nsRect cellRect(currX, mInnerBox.y + mRowHeight * (aRow - mTopRowIndex), colWidth, mRowHeight); // Check the ID of the current column to see if it matches. If it doesn't // increment the current X value and continue to the next column. if (currCol != aCol) { currX += cellRect.width; continue; } // Now obtain the properties for our cell. PrefillPropertyArray(aRow, currCol); nsAutoString properties; view->GetCellProperties(aRow, currCol, properties); nsTreeUtils::TokenizeProperties(properties, mScratchArray); ComputedStyle* rowContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); // We don't want to consider any of the decorations that may be present // on the current row, so we have to deflate the rect by the border and // padding and offset its left and top coordinates appropriately. AdjustForBorderPadding(rowContext, cellRect); ComputedStyle* cellContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); constexpr auto cell = "cell"_ns; if (currCol->IsCycler() || cell.Equals(aElement)) { // If the current Column is a Cycler, then the Rect is just the cell - the // margins. Similarly, if we're just being asked for the cell rect, // provide it. theRect = cellRect; nsMargin cellMargin; cellContext->StyleMargin()->GetMargin(cellMargin); theRect.Deflate(cellMargin); break; } // Since we're not looking for the cell, and since the cell isn't a cycler, // we're looking for some subcomponent, and now we need to subtract the // borders and padding of the cell from cellRect so this does not // interfere with our computations. AdjustForBorderPadding(cellContext, cellRect); UniquePtr rc = presContext->PresShell()->CreateReferenceRenderingContext(); // Now we'll start making our way across the cell, starting at the edge of // the cell and proceeding until we hit the right edge. |cellX| is the // working X value that we will increment as we crawl from left to right. nscoord cellX = cellRect.x; nscoord remainWidth = cellRect.width; if (currCol->IsPrimary()) { // If the current Column is a Primary, then we need to take into account // the indentation and possibly a twisty. // The amount of indentation is the indentation width (|mIndentation|) by // the level. int32_t level; view->GetLevel(aRow, &level); if (!isRTL) cellX += mIndentation * level; remainWidth -= mIndentation * level; // Find the twisty rect by computing its size. nsRect imageRect; nsRect twistyRect(cellRect); ComputedStyle* twistyContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); GetTwistyRect(aRow, currCol, imageRect, twistyRect, presContext, twistyContext); if ("twisty"_ns.Equals(aElement)) { // If we're looking for the twisty Rect, just return the size theRect = twistyRect; break; } // Now we need to add in the margins of the twisty element, so that we // can find the offset of the next element in the cell. nsMargin twistyMargin; twistyContext->StyleMargin()->GetMargin(twistyMargin); twistyRect.Inflate(twistyMargin); // Adjust our working X value with the twisty width (image size, margins, // borders, padding. if (!isRTL) cellX += twistyRect.width; } // Cell Image ComputedStyle* imageContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); nsRect imageSize = GetImageSize(aRow, currCol, false, imageContext); if ("image"_ns.Equals(aElement)) { theRect = imageSize; theRect.x = cellX; theRect.y = cellRect.y; break; } // Add in the margins of the cell image. nsMargin imageMargin; imageContext->StyleMargin()->GetMargin(imageMargin); imageSize.Inflate(imageMargin); // Increment cellX by the image width if (!isRTL) cellX += imageSize.width; // Cell Text nsAutoString cellText; view->GetCellText(aRow, currCol, cellText); // We're going to measure this text so we need to ensure bidi is enabled if // necessary CheckTextForBidi(cellText); // Create a scratch rect to represent the text rectangle, with the current // X and Y coords, and a guess at the width and height. The width is the // remaining width we have left to traverse in the cell, which will be the // widest possible value for the text rect, and the row height. nsRect textRect(cellX, cellRect.y, remainWidth, cellRect.height); // Measure the width of the text. If the width of the text is greater than // the remaining width available, then we just assume that the text has // been cropped and use the remaining rect as the text Rect. Otherwise, // we add in borders and padding to the text dimension and give that back. ComputedStyle* textContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); RefPtr fm = nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext); nscoord height = fm->MaxHeight(); nsMargin textMargin; textContext->StyleMargin()->GetMargin(textMargin); textRect.Deflate(textMargin); // Center the text. XXX Obey vertical-align style prop? if (height < textRect.height) { textRect.y += (textRect.height - height) / 2; textRect.height = height; } nsMargin bp(0, 0, 0, 0); GetBorderPadding(textContext, bp); textRect.height += bp.top + bp.bottom; AdjustForCellText(cellText, aRow, currCol, *rc, *fm, textRect); theRect = textRect; } if (isRTL) theRect.x = mInnerBox.width - theRect.x - theRect.width; *aX = nsPresContext::AppUnitsToIntCSSPixels(theRect.x); *aY = nsPresContext::AppUnitsToIntCSSPixels(theRect.y); *aWidth = nsPresContext::AppUnitsToIntCSSPixels(theRect.width); *aHeight = nsPresContext::AppUnitsToIntCSSPixels(theRect.height); return NS_OK; } int32_t nsTreeBodyFrame::GetRowAtInternal(nscoord aX, nscoord aY) { if (mRowHeight <= 0) return -1; // Now just mod by our total inner box height and add to our top row index. int32_t row = (aY / mRowHeight) + mTopRowIndex; // Check if the coordinates are below our visible space (or within our visible // space but below any row). if (row > mTopRowIndex + mPageLength || row >= mRowCount) return -1; return row; } void nsTreeBodyFrame::CheckTextForBidi(nsAutoString& aText) { // We could check to see whether the prescontext already has bidi enabled, // but usually it won't, so it's probably faster to avoid the call to // GetPresContext() when it's not needed. if (HasRTLChars(aText)) { PresContext()->SetBidiEnabled(); } } void nsTreeBodyFrame::AdjustForCellText(nsAutoString& aText, int32_t aRowIndex, nsTreeColumn* aColumn, gfxContext& aRenderingContext, nsFontMetrics& aFontMetrics, nsRect& aTextRect) { MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); nscoord maxWidth = aTextRect.width; bool widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan( aText, aFontMetrics, drawTarget, maxWidth); nsCOMPtr view = GetExistingView(); if (aColumn->Overflow()) { DebugOnly rv; nsTreeColumn* nextColumn = aColumn->GetNext(); while (nextColumn && widthIsGreater) { while (nextColumn) { nscoord width; rv = nextColumn->GetWidthInTwips(this, &width); NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid"); if (width != 0) { break; } nextColumn = nextColumn->GetNext(); } if (nextColumn) { nsAutoString nextText; view->GetCellText(aRowIndex, nextColumn, nextText); // We don't measure or draw this text so no need to check it for // bidi-ness if (nextText.Length() == 0) { nscoord width; rv = nextColumn->GetWidthInTwips(this, &width); NS_ASSERTION(NS_SUCCEEDED(rv), "nextColumn is invalid"); maxWidth += width; widthIsGreater = nsLayoutUtils::StringWidthIsGreaterThan( aText, aFontMetrics, drawTarget, maxWidth); nextColumn = nextColumn->GetNext(); } else { nextColumn = nullptr; } } } } CroppingStyle cropType = CroppingStyle::CropRight; if (aColumn->GetCropStyle() == 1) { cropType = CroppingStyle::CropCenter; } else if (aColumn->GetCropStyle() == 2) { cropType = CroppingStyle::CropLeft; } CropStringForWidth(aText, aRenderingContext, aFontMetrics, maxWidth, cropType); nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi( aText, this, aFontMetrics, aRenderingContext); switch (aColumn->GetTextAlignment()) { case mozilla::StyleTextAlign::Right: aTextRect.x += aTextRect.width - width; break; case mozilla::StyleTextAlign::Center: aTextRect.x += (aTextRect.width - width) / 2; break; default: break; } aTextRect.width = width; } nsCSSAnonBoxPseudoStaticAtom* nsTreeBodyFrame::GetItemWithinCellAt( nscoord aX, const nsRect& aCellRect, int32_t aRowIndex, nsTreeColumn* aColumn) { MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); // Obtain the properties for our cell. PrefillPropertyArray(aRowIndex, aColumn); nsAutoString properties; nsCOMPtr view = GetExistingView(); view->GetCellProperties(aRowIndex, aColumn, properties); nsTreeUtils::TokenizeProperties(properties, mScratchArray); // Resolve style for the cell. ComputedStyle* cellContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); // Obtain the margins for the cell and then deflate our rect by that // amount. The cell is assumed to be contained within the deflated rect. nsRect cellRect(aCellRect); nsMargin cellMargin; cellContext->StyleMargin()->GetMargin(cellMargin); cellRect.Deflate(cellMargin); // Adjust the rect for its border and padding. AdjustForBorderPadding(cellContext, cellRect); if (aX < cellRect.x || aX >= cellRect.x + cellRect.width) { // The user clicked within the cell's margins/borders/padding. This // constitutes a click on the cell. return nsCSSAnonBoxes::mozTreeCell(); } nscoord currX = cellRect.x; nscoord remainingWidth = cellRect.width; // Handle right alignment hit testing. bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; nsPresContext* presContext = PresContext(); UniquePtr rc = presContext->PresShell()->CreateReferenceRenderingContext(); if (aColumn->IsPrimary()) { // If we're the primary column, we have indentation and a twisty. int32_t level; view->GetLevel(aRowIndex, &level); if (!isRTL) currX += mIndentation * level; remainingWidth -= mIndentation * level; if ((isRTL && aX > currX + remainingWidth) || (!isRTL && aX < currX)) { // The user clicked within the indentation. return nsCSSAnonBoxes::mozTreeCell(); } // Always leave space for the twisty. nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height); bool hasTwisty = false; bool isContainer = false; view->IsContainer(aRowIndex, &isContainer); if (isContainer) { bool isContainerEmpty = false; view->IsContainerEmpty(aRowIndex, &isContainerEmpty); if (!isContainerEmpty) hasTwisty = true; } // Resolve style for the twisty. ComputedStyle* twistyContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); nsRect imageSize; GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, presContext, twistyContext); // We will treat a click as hitting the twisty if it happens on the margins, // borders, padding, or content of the twisty object. By allowing a "slop" // into the margin, we make it a little bit easier for a user to hit the // twisty. (We don't want to be too picky here.) nsMargin twistyMargin; twistyContext->StyleMargin()->GetMargin(twistyMargin); twistyRect.Inflate(twistyMargin); if (isRTL) twistyRect.x = currX + remainingWidth - twistyRect.width; // Now we test to see if aX is actually within the twistyRect. If it is, // and if the item should have a twisty, then we return "twisty". If it is // within the rect but we shouldn't have a twisty, then we return "cell". if (aX >= twistyRect.x && aX < twistyRect.x + twistyRect.width) { if (hasTwisty) return nsCSSAnonBoxes::mozTreeTwisty(); else return nsCSSAnonBoxes::mozTreeCell(); } if (!isRTL) currX += twistyRect.width; remainingWidth -= twistyRect.width; } // Now test to see if the user hit the icon for the cell. nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height); // Resolve style for the image. ComputedStyle* imageContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); nsRect iconSize = GetImageSize(aRowIndex, aColumn, false, imageContext); nsMargin imageMargin; imageContext->StyleMargin()->GetMargin(imageMargin); iconSize.Inflate(imageMargin); iconRect.width = iconSize.width; if (isRTL) iconRect.x = currX + remainingWidth - iconRect.width; if (aX >= iconRect.x && aX < iconRect.x + iconRect.width) { // The user clicked on the image. return nsCSSAnonBoxes::mozTreeImage(); } if (!isRTL) currX += iconRect.width; remainingWidth -= iconRect.width; nsAutoString cellText; view->GetCellText(aRowIndex, aColumn, cellText); // We're going to measure this text so we need to ensure bidi is enabled if // necessary CheckTextForBidi(cellText); nsRect textRect(currX, cellRect.y, remainingWidth, cellRect.height); ComputedStyle* textContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); nsMargin textMargin; textContext->StyleMargin()->GetMargin(textMargin); textRect.Deflate(textMargin); AdjustForBorderPadding(textContext, textRect); RefPtr fm = nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, presContext); AdjustForCellText(cellText, aRowIndex, aColumn, *rc, *fm, textRect); if (aX >= textRect.x && aX < textRect.x + textRect.width) return nsCSSAnonBoxes::mozTreeCellText(); else return nsCSSAnonBoxes::mozTreeCell(); } void nsTreeBodyFrame::GetCellAt(nscoord aX, nscoord aY, int32_t* aRow, nsTreeColumn** aCol, nsCSSAnonBoxPseudoStaticAtom** aChildElt) { *aCol = nullptr; *aChildElt = nullptr; *aRow = GetRowAtInternal(aX, aY); if (*aRow < 0) return; // Determine the column hit. for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; currCol = currCol->GetNext()) { nsRect cellRect; nsresult rv = currCol->GetRect( this, mInnerBox.y + mRowHeight * (*aRow - mTopRowIndex), mRowHeight, &cellRect); if (NS_FAILED(rv)) { MOZ_ASSERT_UNREACHABLE("column has no frame"); continue; } if (!OffsetForHorzScroll(cellRect, false)) continue; if (aX >= cellRect.x && aX < cellRect.x + cellRect.width) { // We know the column hit now. *aCol = currCol; if (currCol->IsCycler()) // Cyclers contain only images. Fill this in immediately and return. *aChildElt = nsCSSAnonBoxes::mozTreeImage(); else *aChildElt = GetItemWithinCellAt(aX, cellRect, *aRow, currCol); break; } } } nsresult nsTreeBodyFrame::GetCellWidth(int32_t aRow, nsTreeColumn* aCol, gfxContext* aRenderingContext, nscoord& aDesiredSize, nscoord& aCurrentSize) { MOZ_ASSERT(aCol, "aCol must not be null"); MOZ_ASSERT(aRenderingContext, "aRenderingContext must not be null"); // The rect for the current cell. nscoord colWidth; nsresult rv = aCol->GetWidthInTwips(this, &colWidth); NS_ENSURE_SUCCESS(rv, rv); nsRect cellRect(0, 0, colWidth, mRowHeight); int32_t overflow = cellRect.x + cellRect.width - (mInnerBox.x + mInnerBox.width); if (overflow > 0) cellRect.width -= overflow; // Adjust borders and padding for the cell. ComputedStyle* cellContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); nsMargin bp(0, 0, 0, 0); GetBorderPadding(cellContext, bp); aCurrentSize = cellRect.width; aDesiredSize = bp.left + bp.right; nsCOMPtr view = GetExistingView(); if (aCol->IsPrimary()) { // If the current Column is a Primary, then we need to take into account // the indentation and possibly a twisty. // The amount of indentation is the indentation width (|mIndentation|) by // the level. int32_t level; view->GetLevel(aRow, &level); aDesiredSize += mIndentation * level; // Find the twisty rect by computing its size. ComputedStyle* twistyContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); nsRect imageSize; nsRect twistyRect(cellRect); GetTwistyRect(aRow, aCol, imageSize, twistyRect, PresContext(), twistyContext); // Add in the margins of the twisty element. nsMargin twistyMargin; twistyContext->StyleMargin()->GetMargin(twistyMargin); twistyRect.Inflate(twistyMargin); aDesiredSize += twistyRect.width; } ComputedStyle* imageContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); // Account for the width of the cell image. nsRect imageSize = GetImageSize(aRow, aCol, false, imageContext); // Add in the margins of the cell image. nsMargin imageMargin; imageContext->StyleMargin()->GetMargin(imageMargin); imageSize.Inflate(imageMargin); aDesiredSize += imageSize.width; // Get the cell text. nsAutoString cellText; view->GetCellText(aRow, aCol, cellText); // We're going to measure this text so we need to ensure bidi is enabled if // necessary CheckTextForBidi(cellText); ComputedStyle* textContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); // Get the borders and padding for the text. GetBorderPadding(textContext, bp); RefPtr fm = nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext()); // Get the width of the text itself nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(cellText, this, *fm, *aRenderingContext); nscoord totalTextWidth = width + bp.left + bp.right; aDesiredSize += totalTextWidth; return NS_OK; } nsresult nsTreeBodyFrame::IsCellCropped(int32_t aRow, nsTreeColumn* aCol, bool* _retval) { nscoord currentSize, desiredSize; nsresult rv; if (!aCol) return NS_ERROR_INVALID_ARG; UniquePtr rc = PresShell()->CreateReferenceRenderingContext(); rv = GetCellWidth(aRow, aCol, rc.get(), desiredSize, currentSize); NS_ENSURE_SUCCESS(rv, rv); *_retval = desiredSize > currentSize; return NS_OK; } nsresult nsTreeBodyFrame::CreateTimer(const LookAndFeel::IntID aID, nsTimerCallbackFunc aFunc, int32_t aType, nsITimer** aTimer, const char* aName) { // Get the delay from the look and feel service. int32_t delay = LookAndFeel::GetInt(aID, 0); nsCOMPtr timer; // Create a new timer only if the delay is greater than zero. // Zero value means that this feature is completely disabled. if (delay > 0) { MOZ_TRY_VAR(timer, NS_NewTimerWithFuncCallback(aFunc, this, delay, aType, aName, GetMainThreadSerialEventTarget())); } timer.forget(aTimer); return NS_OK; } nsresult nsTreeBodyFrame::RowCountChanged(int32_t aIndex, int32_t aCount) { if (aCount == 0 || !mView) { return NS_OK; // Nothing to do. } #ifdef ACCESSIBILITY if (GetAccService()) { FireRowCountChangedEvent(aIndex, aCount); } #endif // #ifdef ACCESSIBILITY AutoWeakFrame weakFrame(this); // Adjust our selection. if (nsCOMPtr sel = GetSelection()) { sel->AdjustSelection(aIndex, aCount); } NS_ENSURE_STATE(weakFrame.IsAlive()); if (mUpdateBatchNest) return NS_OK; mRowCount += aCount; #ifdef DEBUG int32_t rowCount = mRowCount; mView->GetRowCount(&rowCount); NS_ASSERTION( rowCount == mRowCount, "row count did not change by the amount suggested, check caller"); #endif int32_t count = Abs(aCount); int32_t last = LastVisibleRow(); if (aIndex >= mTopRowIndex && aIndex <= last) InvalidateRange(aIndex, last); ScrollParts parts = GetScrollParts(); if (mTopRowIndex == 0) { // Just update the scrollbar and return. FullScrollbarsUpdate(false); return NS_OK; } bool needsInvalidation = false; // Adjust our top row index. if (aCount > 0) { if (mTopRowIndex > aIndex) { // Rows came in above us. Augment the top row index. mTopRowIndex += aCount; } } else if (aCount < 0) { if (mTopRowIndex > aIndex + count - 1) { // No need to invalidate. The remove happened // completely above us (offscreen). mTopRowIndex -= count; } else if (mTopRowIndex >= aIndex) { // This is a full-blown invalidate. if (mTopRowIndex + mPageLength > mRowCount - 1) { mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength); } needsInvalidation = true; } } FullScrollbarsUpdate(needsInvalidation); return NS_OK; } nsresult nsTreeBodyFrame::BeginUpdateBatch() { ++mUpdateBatchNest; return NS_OK; } nsresult nsTreeBodyFrame::EndUpdateBatch() { NS_ASSERTION(mUpdateBatchNest > 0, "badly nested update batch"); if (--mUpdateBatchNest != 0) { return NS_OK; } nsCOMPtr view = GetExistingView(); if (!view) { return NS_OK; } Invalidate(); int32_t countBeforeUpdate = mRowCount; view->GetRowCount(&mRowCount); if (countBeforeUpdate != mRowCount) { if (mTopRowIndex + mPageLength > mRowCount - 1) { mTopRowIndex = std::max(0, mRowCount - 1 - mPageLength); } FullScrollbarsUpdate(false); } return NS_OK; } void nsTreeBodyFrame::PrefillPropertyArray(int32_t aRowIndex, nsTreeColumn* aCol) { MOZ_ASSERT(!aCol || aCol->GetFrame(), "invalid column passed"); mScratchArray.Clear(); // focus if (mFocused) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::focus); else mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::blur); // sort bool sorted = false; mView->IsSorted(&sorted); if (sorted) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::sorted); // drag session if (mSlots && mSlots->mIsDragging) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dragSession); if (aRowIndex != -1) { if (aRowIndex == mMouseOverRow) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::hover); nsCOMPtr selection = GetSelection(); if (selection) { // selected bool isSelected; selection->IsSelected(aRowIndex, &isSelected); if (isSelected) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::selected); // current int32_t currentIndex; selection->GetCurrentIndex(¤tIndex); if (aRowIndex == currentIndex) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::current); } // container or leaf bool isContainer = false; mView->IsContainer(aRowIndex, &isContainer); if (isContainer) { mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::container); // open or closed bool isOpen = false; mView->IsContainerOpen(aRowIndex, &isOpen); if (isOpen) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::open); else mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::closed); } else { mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::leaf); } // drop orientation if (mSlots && mSlots->mDropAllowed && mSlots->mDropRow == aRowIndex) { if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropBefore); else if (mSlots->mDropOrient == nsITreeView::DROP_ON) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropOn); else if (mSlots->mDropOrient == nsITreeView::DROP_AFTER) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::dropAfter); } // odd or even if (aRowIndex % 2) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::odd); else mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::even); XULTreeElement* tree = GetBaseElement(); if (tree && tree->HasAttr(nsGkAtoms::editing)) { mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::editing); } // multiple columns if (mColumns->GetColumnAt(1)) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::multicol); } if (aCol) { mScratchArray.AppendElement(aCol->GetAtom()); if (aCol->IsPrimary()) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::primary); if (aCol->GetType() == TreeColumn_Binding::TYPE_CHECKBOX) { mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checkbox); if (aRowIndex != -1) { nsAutoString value; mView->GetCellValue(aRowIndex, aCol, value); if (value.EqualsLiteral("true")) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::checked); } } // Read special properties from attributes on the column content node if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertbefore, nsGkAtoms::_true, eCaseMatters)) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertbefore); if (aCol->mContent->AttrValueIs(kNameSpaceID_None, nsGkAtoms::insertafter, nsGkAtoms::_true, eCaseMatters)) mScratchArray.AppendElement((nsStaticAtom*)nsGkAtoms::insertafter); } } nsITheme* nsTreeBodyFrame::GetTwistyRect(int32_t aRowIndex, nsTreeColumn* aColumn, nsRect& aImageRect, nsRect& aTwistyRect, nsPresContext* aPresContext, ComputedStyle* aTwistyContext) { // The twisty rect extends all the way to the end of the cell. This is // incorrect. We need to determine the twisty rect's true width. This is // done by examining the ComputedStyle for a width first. If it has one, we // use that. If it doesn't, we use the image's natural width. If the image // hasn't loaded and if no width is specified, then we just bail. If there is // a -moz-appearance involved, adjust the rect by the minimum widget size // provided by the theme implementation. aImageRect = GetImageSize(aRowIndex, aColumn, true, aTwistyContext); if (aImageRect.height > aTwistyRect.height) aImageRect.height = aTwistyRect.height; if (aImageRect.width > aTwistyRect.width) aImageRect.width = aTwistyRect.width; else aTwistyRect.width = aImageRect.width; bool useTheme = false; nsITheme* theme = nullptr; StyleAppearance appearance = aTwistyContext->StyleDisplay()->EffectiveAppearance(); if (appearance != StyleAppearance::None) { theme = aPresContext->Theme(); if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) useTheme = true; } if (useTheme) { LayoutDeviceIntSize minTwistySizePx = theme->GetMinimumWidgetSize(aPresContext, this, appearance); // GMWS() returns size in pixels, we need to convert it back to app units nsSize minTwistySize; minTwistySize.width = aPresContext->DevPixelsToAppUnits(minTwistySizePx.width); minTwistySize.height = aPresContext->DevPixelsToAppUnits(minTwistySizePx.height); if (aTwistyRect.width < minTwistySize.width) { aTwistyRect.width = minTwistySize.width; } } return useTheme ? theme : nullptr; } nsresult nsTreeBodyFrame::GetImage(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext, ComputedStyle* aComputedStyle, imgIContainer** aResult) { *aResult = nullptr; nsAutoString imageSrc; mView->GetImageSrc(aRowIndex, aCol, imageSrc); RefPtr styleRequest; if (aUseContext || imageSrc.IsEmpty()) { // Obtain the URL from the ComputedStyle. styleRequest = aComputedStyle->StyleList()->mListStyleImage.GetImageRequest(); if (!styleRequest) return NS_OK; nsCOMPtr uri; styleRequest->GetURI(getter_AddRefs(uri)); nsAutoCString spec; nsresult rv = uri->GetSpec(spec); NS_ENSURE_SUCCESS(rv, rv); CopyUTF8toUTF16(spec, imageSrc); } // Look the image up in our cache. nsTreeImageCacheEntry entry; if (mImageCache.Get(imageSrc, &entry)) { // Find out if the image has loaded. uint32_t status; imgIRequest* imgReq = entry.request; imgReq->GetImageStatus(&status); imgReq->GetImage(aResult); // We hand back the image here. The GetImage // call addrefs *aResult. bool animated = true; // Assuming animated is the safe option // We can only call GetAnimated if we're decoded if (*aResult && (status & imgIRequest::STATUS_DECODE_COMPLETE)) (*aResult)->GetAnimated(&animated); if ((!(status & imgIRequest::STATUS_LOAD_COMPLETE)) || animated) { // We either aren't done loading, or we're animating. Add our row as a // listener for invalidations. nsCOMPtr obs; imgReq->GetNotificationObserver(getter_AddRefs(obs)); if (obs) { static_cast(obs.get())->AddCell(aRowIndex, aCol); } return NS_OK; } } if (!*aResult) { // Create a new nsTreeImageListener object and pass it our row and column // information. nsTreeImageListener* listener = new nsTreeImageListener(this); if (!listener) return NS_ERROR_OUT_OF_MEMORY; mCreatedListeners.Insert(listener); listener->AddCell(aRowIndex, aCol); nsCOMPtr imgNotificationObserver = listener; Document* doc = mContent->GetComposedDoc(); if (!doc) // The page is currently being torn down. Why bother. return NS_ERROR_FAILURE; RefPtr imageRequest; if (styleRequest) { styleRequest->SyncClone(imgNotificationObserver, doc, getter_AddRefs(imageRequest)); } else { nsCOMPtr srcURI; nsContentUtils::NewURIWithDocumentCharset( getter_AddRefs(srcURI), imageSrc, doc, mContent->GetBaseURI()); if (!srcURI) return NS_ERROR_FAILURE; auto referrerInfo = MakeRefPtr(*doc); // XXXbz what's the origin principal for this stuff that comes from our // view? I guess we should assume that it's the node's principal... nsresult rv = nsContentUtils::LoadImage( srcURI, mContent, doc, mContent->NodePrincipal(), 0, referrerInfo, imgNotificationObserver, nsIRequest::LOAD_NORMAL, u""_ns, getter_AddRefs(imageRequest)); NS_ENSURE_SUCCESS(rv, rv); // NOTE(heycam): If it's an SVG image, and we need to want the image to // able to respond to media query changes, it needs to be added to the // document's ImageTracker. For now, assume we don't need this. } listener->UnsuppressInvalidation(); if (!imageRequest) return NS_ERROR_FAILURE; // We don't want discarding/decode-on-draw for xul images imageRequest->StartDecoding(imgIContainer::FLAG_ASYNC_NOTIFY); imageRequest->LockImage(); // In a case it was already cached. imageRequest->GetImage(aResult); nsTreeImageCacheEntry cacheEntry(imageRequest, imgNotificationObserver); mImageCache.InsertOrUpdate(imageSrc, cacheEntry); } return NS_OK; } nsRect nsTreeBodyFrame::GetImageSize(int32_t aRowIndex, nsTreeColumn* aCol, bool aUseContext, ComputedStyle* aComputedStyle) { // XXX We should respond to visibility rules for collapsed vs. hidden. // This method returns the width of the twisty INCLUDING borders and padding. // It first checks the ComputedStyle for a width. If none is found, it tries // to use the default image width for the twisty. If no image is found, it // defaults to border+padding. nsRect r(0, 0, 0, 0); nsMargin bp(0, 0, 0, 0); GetBorderPadding(aComputedStyle, bp); r.Inflate(bp); // Now r contains our border+padding info. We now need to get our width and // height. bool needWidth = false; bool needHeight = false; // We have to load image even though we already have a size. // Don't change this, otherwise things start to go awry. nsCOMPtr image; GetImage(aRowIndex, aCol, aUseContext, aComputedStyle, getter_AddRefs(image)); const nsStylePosition* myPosition = aComputedStyle->StylePosition(); if (myPosition->mWidth.ConvertsToLength()) { int32_t val = myPosition->mWidth.ToLength(); r.width += val; } else { needWidth = true; } if (myPosition->mHeight.ConvertsToLength()) { int32_t val = myPosition->mHeight.ToLength(); r.height += val; } else { needHeight = true; } if (image) { if (needWidth || needHeight) { // Get the natural image size. if (needWidth) { // Get the size from the image. nscoord width; image->GetWidth(&width); r.width += nsPresContext::CSSPixelsToAppUnits(width); } if (needHeight) { nscoord height; image->GetHeight(&height); r.height += nsPresContext::CSSPixelsToAppUnits(height); } } } return r; } // GetImageDestSize returns the destination size of the image. // The width and height do not include borders and padding. // The width and height have not been adjusted to fit in the row height // or cell width. // The width and height reflect the destination size specified in CSS, // or the image region specified in CSS, or the natural size of the // image. // If only the destination width has been specified in CSS, the height is // calculated to maintain the aspect ratio of the image. // If only the destination height has been specified in CSS, the width is // calculated to maintain the aspect ratio of the image. nsSize nsTreeBodyFrame::GetImageDestSize(ComputedStyle* aComputedStyle, imgIContainer* image) { nsSize size(0, 0); // We need to get the width and height. bool needWidth = false; bool needHeight = false; // Get the style position to see if the CSS has specified the // destination width/height. const nsStylePosition* myPosition = aComputedStyle->StylePosition(); if (myPosition->mWidth.ConvertsToLength()) { // CSS has specified the destination width. size.width = myPosition->mWidth.ToLength(); } else { // We'll need to get the width of the image/region. needWidth = true; } if (myPosition->mHeight.ConvertsToLength()) { // CSS has specified the destination height. size.height = myPosition->mHeight.ToLength(); } else { // We'll need to get the height of the image/region. needHeight = true; } if (needWidth || needHeight) { // We need to get the size of the image/region. nsSize imageSize(0, 0); if (image) { nscoord width; image->GetWidth(&width); imageSize.width = nsPresContext::CSSPixelsToAppUnits(width); nscoord height; image->GetHeight(&height); imageSize.height = nsPresContext::CSSPixelsToAppUnits(height); } if (needWidth) { if (!needHeight && imageSize.height != 0) { // The CSS specified the destination height, but not the destination // width. We need to calculate the width so that we maintain the // image's aspect ratio. size.width = imageSize.width * size.height / imageSize.height; } else { size.width = imageSize.width; } } if (needHeight) { if (!needWidth && imageSize.width != 0) { // The CSS specified the destination width, but not the destination // height. We need to calculate the height so that we maintain the // image's aspect ratio. size.height = imageSize.height * size.width / imageSize.width; } else { size.height = imageSize.height; } } } return size; } // GetImageSourceRect returns the source rectangle of the image to be // displayed. // The width and height reflect the image region specified in CSS, or // the natural size of the image. // The width and height do not include borders and padding. // The width and height do not reflect the destination size specified // in CSS. nsRect nsTreeBodyFrame::GetImageSourceRect(ComputedStyle* aComputedStyle, imgIContainer* image) { if (!image) { return nsRect(); } nsRect r; // Use the actual image size. nscoord coord; if (NS_SUCCEEDED(image->GetWidth(&coord))) { r.width = nsPresContext::CSSPixelsToAppUnits(coord); } if (NS_SUCCEEDED(image->GetHeight(&coord))) { r.height = nsPresContext::CSSPixelsToAppUnits(coord); } return r; } int32_t nsTreeBodyFrame::GetRowHeight() { // Look up the correct height. It is equal to the specified height // + the specified margins. mScratchArray.Clear(); ComputedStyle* rowContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); if (rowContext) { const nsStylePosition* myPosition = rowContext->StylePosition(); nscoord minHeight = 0; if (myPosition->mMinHeight.ConvertsToLength()) { minHeight = myPosition->mMinHeight.ToLength(); } nscoord height = 0; if (myPosition->mHeight.ConvertsToLength()) { height = myPosition->mHeight.ToLength(); } if (height < minHeight) height = minHeight; if (height > 0) { height = nsPresContext::AppUnitsToIntCSSPixels(height); height += height % 2; height = nsPresContext::CSSPixelsToAppUnits(height); // XXX Check box-sizing to determine if border/padding should augment the // height Inflate the height by our margins. nsRect rowRect(0, 0, 0, height); nsMargin rowMargin; rowContext->StyleMargin()->GetMargin(rowMargin); rowRect.Inflate(rowMargin); height = rowRect.height; return height; } } return nsPresContext::CSSPixelsToAppUnits(18); // As good a default as any. } int32_t nsTreeBodyFrame::GetIndentation() { // Look up the correct indentation. It is equal to the specified indentation // width. mScratchArray.Clear(); ComputedStyle* indentContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeIndentation()); if (indentContext) { const nsStylePosition* myPosition = indentContext->StylePosition(); if (myPosition->mWidth.ConvertsToLength()) { return myPosition->mWidth.ToLength(); } } return nsPresContext::CSSPixelsToAppUnits(16); // As good a default as any. } void nsTreeBodyFrame::CalcInnerBox() { mInnerBox.SetRect(0, 0, mRect.width, mRect.height); AdjustForBorderPadding(mComputedStyle, mInnerBox); } nscoord nsTreeBodyFrame::CalcHorzWidth(const ScrollParts& aParts) { // Compute the adjustment to the last column. This varies depending on the // visibility of the columnpicker and the scrollbar. if (aParts.mColumnsFrame) mAdjustWidth = mRect.width - aParts.mColumnsFrame->GetRect().width; else mAdjustWidth = 0; nscoord width = 0; // We calculate this from the scrollable frame, so that it // properly covers all contingencies of what could be // scrollable (columns, body, etc...) if (aParts.mColumnsScrollFrame) { width = aParts.mColumnsScrollFrame->GetScrollRange().width + aParts.mColumnsScrollFrame->GetScrollPortRect().width; } // If no horz scrolling periphery is present, then just return our width if (width == 0) width = mRect.width; return width; } nsIFrame::Cursor nsTreeBodyFrame::GetCursor(const nsPoint& aPoint) { // Check the GetScriptHandlingObject so we don't end up running code when // the document is a zombie. bool dummy; if (mView && GetContent()->GetComposedDoc()->GetScriptHandlingObject(dummy)) { int32_t row; nsTreeColumn* col; nsCSSAnonBoxPseudoStaticAtom* child; GetCellAt(aPoint.x, aPoint.y, &row, &col, &child); if (child) { // Our scratch array is already prefilled. RefPtr childContext = GetPseudoComputedStyle(child); StyleCursorKind kind = childContext->StyleUI()->Cursor().keyword; if (kind == StyleCursorKind::Auto) { kind = StyleCursorKind::Default; } return Cursor{kind, AllowCustomCursorImage::Yes, std::move(childContext)}; } } return SimpleXULLeafFrame::GetCursor(aPoint); } static uint32_t GetDropEffect(WidgetGUIEvent* aEvent) { NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type"); WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); nsContentUtils::SetDataTransferInEvent(dragEvent); uint32_t action = 0; if (dragEvent->mDataTransfer) { action = dragEvent->mDataTransfer->DropEffectInt(); } return action; } nsresult nsTreeBodyFrame::HandleEvent(nsPresContext* aPresContext, WidgetGUIEvent* aEvent, nsEventStatus* aEventStatus) { if (aEvent->mMessage == eMouseOver || aEvent->mMessage == eMouseMove) { nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this}); int32_t xTwips = pt.x - mInnerBox.x; int32_t yTwips = pt.y - mInnerBox.y; int32_t newrow = GetRowAtInternal(xTwips, yTwips); if (mMouseOverRow != newrow) { // redraw the old and the new row if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow); mMouseOverRow = newrow; if (mMouseOverRow != -1) InvalidateRow(mMouseOverRow); } } else if (aEvent->mMessage == eMouseOut) { if (mMouseOverRow != -1) { InvalidateRow(mMouseOverRow); mMouseOverRow = -1; } } else if (aEvent->mMessage == eDragEnter) { if (!mSlots) { mSlots = MakeUnique(); } // Cache several things we'll need throughout the course of our work. These // will all get released on a drag exit. if (mSlots->mTimer) { mSlots->mTimer->Cancel(); mSlots->mTimer = nullptr; } // Cache the drag session. mSlots->mIsDragging = true; mSlots->mDropRow = -1; mSlots->mDropOrient = -1; mSlots->mDragAction = GetDropEffect(aEvent); } else if (aEvent->mMessage == eDragOver) { // The mouse is hovering over this tree. If we determine things are // different from the last time, invalidate the drop feedback at the old // position, query the view to see if the current location is droppable, // and then invalidate the drop feedback at the new location if it is. // The mouse may or may not have changed position from the last time // we were called, so optimize out a lot of the extra notifications by // checking if anything changed first. For drop feedback we use drop, // dropBefore and dropAfter property. if (!mView || !mSlots) { return NS_OK; } // Save last values, we will need them. int32_t lastDropRow = mSlots->mDropRow; int16_t lastDropOrient = mSlots->mDropOrient; #ifndef XP_MACOSX int16_t lastScrollLines = mSlots->mScrollLines; #endif // Find out the current drag action uint32_t lastDragAction = mSlots->mDragAction; mSlots->mDragAction = GetDropEffect(aEvent); // Compute the row mouse is over and the above/below/on state. // Below we'll use this to see if anything changed. // Also check if we want to auto-scroll. ComputeDropPosition(aEvent, &mSlots->mDropRow, &mSlots->mDropOrient, &mSlots->mScrollLines); // While we're here, handle tracking of scrolling during a drag. if (mSlots->mScrollLines) { if (mSlots->mDropAllowed) { // Invalidate primary cell at old location. mSlots->mDropAllowed = false; InvalidateDropFeedback(lastDropRow, lastDropOrient); } #ifdef XP_MACOSX ScrollByLines(mSlots->mScrollLines); #else if (!lastScrollLines) { // Cancel any previously initialized timer. if (mSlots->mTimer) { mSlots->mTimer->Cancel(); mSlots->mTimer = nullptr; } // Set a timer to trigger the tree scrolling. CreateTimer(LookAndFeel::IntID::TreeLazyScrollDelay, LazyScrollCallback, nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer), "nsTreeBodyFrame::LazyScrollCallback"); } #endif // Bail out to prevent spring loaded timer and feedback line settings. return NS_OK; } // If changed from last time, invalidate primary cell at the old location // and if allowed, invalidate primary cell at the new location. If nothing // changed, just bail. if (mSlots->mDropRow != lastDropRow || mSlots->mDropOrient != lastDropOrient || mSlots->mDragAction != lastDragAction) { // Invalidate row at the old location. if (mSlots->mDropAllowed) { mSlots->mDropAllowed = false; InvalidateDropFeedback(lastDropRow, lastDropOrient); } if (mSlots->mTimer) { // Timer is active but for a different row than the current one, kill // it. mSlots->mTimer->Cancel(); mSlots->mTimer = nullptr; } if (mSlots->mDropRow >= 0) { if (!mSlots->mTimer && mSlots->mDropOrient == nsITreeView::DROP_ON) { // Either there wasn't a timer running or it was just killed above. // If over a folder, start up a timer to open the folder. bool isContainer = false; mView->IsContainer(mSlots->mDropRow, &isContainer); if (isContainer) { bool isOpen = false; mView->IsContainerOpen(mSlots->mDropRow, &isOpen); if (!isOpen) { // This node isn't expanded, set a timer to expand it. CreateTimer(LookAndFeel::IntID::TreeOpenDelay, OpenCallback, nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer), "nsTreeBodyFrame::OpenCallback"); } } } // The dataTransfer was initialized by the call to GetDropEffect above. bool canDropAtNewLocation = false; mView->CanDrop(mSlots->mDropRow, mSlots->mDropOrient, aEvent->AsDragEvent()->mDataTransfer, &canDropAtNewLocation); if (canDropAtNewLocation) { // Invalidate row at the new location. mSlots->mDropAllowed = canDropAtNewLocation; InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient); } } } // Indicate that the drop is allowed by preventing the default behaviour. if (mSlots->mDropAllowed) *aEventStatus = nsEventStatus_eConsumeNoDefault; } else if (aEvent->mMessage == eDrop) { // this event was meant for another frame, so ignore it if (!mSlots) return NS_OK; // Tell the view where the drop happened. // Remove the drop folder and all its parents from the array. int32_t parentIndex; nsresult rv = mView->GetParentIndex(mSlots->mDropRow, &parentIndex); while (NS_SUCCEEDED(rv) && parentIndex >= 0) { mSlots->mArray.RemoveElement(parentIndex); rv = mView->GetParentIndex(parentIndex, &parentIndex); } NS_ASSERTION(aEvent->mClass == eDragEventClass, "wrong event type"); WidgetDragEvent* dragEvent = aEvent->AsDragEvent(); nsContentUtils::SetDataTransferInEvent(dragEvent); mView->Drop(mSlots->mDropRow, mSlots->mDropOrient, dragEvent->mDataTransfer); mSlots->mDropRow = -1; mSlots->mDropOrient = -1; mSlots->mIsDragging = false; *aEventStatus = nsEventStatus_eConsumeNoDefault; // already handled the drop } else if (aEvent->mMessage == eDragExit) { // this event was meant for another frame, so ignore it if (!mSlots) return NS_OK; // Clear out all our tracking vars. if (mSlots->mDropAllowed) { mSlots->mDropAllowed = false; InvalidateDropFeedback(mSlots->mDropRow, mSlots->mDropOrient); } else mSlots->mDropAllowed = false; mSlots->mIsDragging = false; mSlots->mScrollLines = 0; // If a drop is occuring, the exit event will fire just before the drop // event, so don't reset mDropRow or mDropOrient as these fields are used // by the drop event. if (mSlots->mTimer) { mSlots->mTimer->Cancel(); mSlots->mTimer = nullptr; } if (!mSlots->mArray.IsEmpty()) { // Close all spring loaded folders except the drop folder. CreateTimer(LookAndFeel::IntID::TreeCloseDelay, CloseCallback, nsITimer::TYPE_ONE_SHOT, getter_AddRefs(mSlots->mTimer), "nsTreeBodyFrame::CloseCallback"); } } return NS_OK; } namespace mozilla { class nsDisplayTreeBody final : public nsPaintedDisplayItem { public: nsDisplayTreeBody(nsDisplayListBuilder* aBuilder, nsIFrame* aFrame) : nsPaintedDisplayItem(aBuilder, aFrame) { MOZ_COUNT_CTOR(nsDisplayTreeBody); } MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayTreeBody) nsDisplayItemGeometry* AllocateGeometry( nsDisplayListBuilder* aBuilder) override { return new nsDisplayTreeBodyGeometry(this, aBuilder, IsWindowActive()); } void Destroy(nsDisplayListBuilder* aBuilder) override { aBuilder->UnregisterThemeGeometry(this); nsPaintedDisplayItem::Destroy(aBuilder); } bool IsWindowActive() const { return !mFrame->PresContext()->Document()->IsTopLevelWindowInactive(); } void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder, const nsDisplayItemGeometry* aGeometry, nsRegion* aInvalidRegion) const override { auto geometry = static_cast(aGeometry); if (IsWindowActive() != geometry->mWindowIsActive) { bool snap; aInvalidRegion->Or(*aInvalidRegion, GetBounds(aBuilder, &snap)); } nsPaintedDisplayItem::ComputeInvalidationRegion(aBuilder, aGeometry, aInvalidRegion); } void Paint(nsDisplayListBuilder* aBuilder, gfxContext* aCtx) override { MOZ_ASSERT(aBuilder); Unused << static_cast(mFrame)->PaintTreeBody( *aCtx, GetPaintRect(aBuilder, aCtx), ToReferenceFrame(), aBuilder); } NS_DISPLAY_DECL_NAME("XULTreeBody", TYPE_XUL_TREE_BODY) nsRect GetComponentAlphaBounds( nsDisplayListBuilder* aBuilder) const override { bool snap; return GetBounds(aBuilder, &snap); } }; } // namespace mozilla // Painting routines void nsTreeBodyFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, const nsDisplayListSet& aLists) { // REVIEW: why did we paint if we were collapsed? that makes no sense! if (!IsVisibleForPainting()) return; // We're invisible. Don't paint. // Handles painting our background, border, and outline. SimpleXULLeafFrame::BuildDisplayList(aBuilder, aLists); // Bail out now if there's no view or we can't run script because the // document is a zombie if (!mView || !GetContent()->GetComposedDoc()->GetWindow()) return; nsDisplayItem* item = MakeDisplayItem(aBuilder, this); aLists.Content()->AppendToTop(item); } ImgDrawResult nsTreeBodyFrame::PaintTreeBody(gfxContext& aRenderingContext, const nsRect& aDirtyRect, nsPoint aPt, nsDisplayListBuilder* aBuilder) { // Update our available height and our page count. CalcInnerBox(); DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); aRenderingContext.Save(); aRenderingContext.Clip(NSRectToSnappedRect( mInnerBox + aPt, PresContext()->AppUnitsPerDevPixel(), *drawTarget)); int32_t oldPageCount = mPageLength; if (!mHasFixedRowCount) { mPageLength = (mRowHeight > 0) ? (mInnerBox.height / mRowHeight) : mRowCount; } if (oldPageCount != mPageLength || mHorzWidth != CalcHorzWidth(GetScrollParts())) { // Schedule a ResizeReflow that will update our info properly. PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, NS_FRAME_IS_DIRTY); } #ifdef DEBUG int32_t rowCount = mRowCount; mView->GetRowCount(&rowCount); NS_WARNING_ASSERTION(mRowCount == rowCount, "row count changed unexpectedly"); #endif ImgDrawResult result = ImgDrawResult::SUCCESS; // Loop through our columns and paint them (e.g., for sorting). This is only // relevant when painting backgrounds, since columns contain no content. // Content is contained in the rows. for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; currCol = currCol->GetNext()) { nsRect colRect; nsresult rv = currCol->GetRect(this, mInnerBox.y, mInnerBox.height, &colRect); // Don't paint hidden columns. if (NS_FAILED(rv) || colRect.width == 0) continue; if (OffsetForHorzScroll(colRect, false)) { nsRect dirtyRect; colRect += aPt; if (dirtyRect.IntersectRect(aDirtyRect, colRect)) { result &= PaintColumn(currCol, colRect, PresContext(), aRenderingContext, aDirtyRect); } } } // Loop through our on-screen rows. for (int32_t i = mTopRowIndex; i < mRowCount && i <= mTopRowIndex + mPageLength; i++) { nsRect rowRect(mInnerBox.x, mInnerBox.y + mRowHeight * (i - mTopRowIndex), mInnerBox.width, mRowHeight); nsRect dirtyRect; if (dirtyRect.IntersectRect(aDirtyRect, rowRect + aPt) && rowRect.y < (mInnerBox.y + mInnerBox.height)) { result &= PaintRow(i, rowRect + aPt, PresContext(), aRenderingContext, aDirtyRect, aPt, aBuilder); } } if (mSlots && mSlots->mDropAllowed && (mSlots->mDropOrient == nsITreeView::DROP_BEFORE || mSlots->mDropOrient == nsITreeView::DROP_AFTER)) { nscoord yPos = mInnerBox.y + mRowHeight * (mSlots->mDropRow - mTopRowIndex) - mRowHeight / 2; nsRect feedbackRect(mInnerBox.x, yPos, mInnerBox.width, mRowHeight); if (mSlots->mDropOrient == nsITreeView::DROP_AFTER) feedbackRect.y += mRowHeight; nsRect dirtyRect; feedbackRect += aPt; if (dirtyRect.IntersectRect(aDirtyRect, feedbackRect)) { result &= PaintDropFeedback(feedbackRect, PresContext(), aRenderingContext, aDirtyRect, aPt); } } aRenderingContext.Restore(); return result; } ImgDrawResult nsTreeBodyFrame::PaintColumn(nsTreeColumn* aColumn, const nsRect& aColumnRect, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aDirtyRect) { MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); // Now obtain the properties for our cell. PrefillPropertyArray(-1, aColumn); nsAutoString properties; nsCOMPtr view = GetExistingView(); view->GetColumnProperties(aColumn, properties); nsTreeUtils::TokenizeProperties(properties, mScratchArray); // Resolve style for the column. It contains all the info we need to lay // ourselves out and to paint. ComputedStyle* colContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeColumn()); // Obtain the margins for the cell and then deflate our rect by that // amount. The cell is assumed to be contained within the deflated rect. nsRect colRect(aColumnRect); nsMargin colMargin; colContext->StyleMargin()->GetMargin(colMargin); colRect.Deflate(colMargin); return PaintBackgroundLayer(colContext, aPresContext, aRenderingContext, colRect, aDirtyRect); } ImgDrawResult nsTreeBodyFrame::PaintRow(int32_t aRowIndex, const nsRect& aRowRect, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aDirtyRect, nsPoint aPt, nsDisplayListBuilder* aBuilder) { // We have been given a rect for our row. We treat this row like a full-blown // frame, meaning that it can have borders, margins, padding, and a // background. // Without a view, we have no data. Check for this up front. nsCOMPtr view = GetExistingView(); if (!view) { return ImgDrawResult::SUCCESS; } nsresult rv; // Now obtain the properties for our row. // XXX Automatically fill in the following props: open, closed, container, // leaf, selected, focused PrefillPropertyArray(aRowIndex, nullptr); nsAutoString properties; view->GetRowProperties(aRowIndex, properties); nsTreeUtils::TokenizeProperties(properties, mScratchArray); // Resolve style for the row. It contains all the info we need to lay // ourselves out and to paint. ComputedStyle* rowContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); // Obtain the margins for the row and then deflate our rect by that // amount. The row is assumed to be contained within the deflated rect. nsRect rowRect(aRowRect); nsMargin rowMargin; rowContext->StyleMargin()->GetMargin(rowMargin); rowRect.Deflate(rowMargin); ImgDrawResult result = ImgDrawResult::SUCCESS; // Paint our borders and background for our row rect. nsITheme* theme = nullptr; auto appearance = rowContext->StyleDisplay()->EffectiveAppearance(); if (appearance != StyleAppearance::None) { theme = aPresContext->Theme(); } if (theme && theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) { nsRect dirty; dirty.IntersectRect(rowRect, aDirtyRect); theme->DrawWidgetBackground(&aRenderingContext, this, appearance, rowRect, dirty); } else { result &= PaintBackgroundLayer(rowContext, aPresContext, aRenderingContext, rowRect, aDirtyRect); } // Adjust the rect for its border and padding. nsRect originalRowRect = rowRect; AdjustForBorderPadding(rowContext, rowRect); bool isSeparator = false; view->IsSeparator(aRowIndex, &isSeparator); if (isSeparator) { // The row is a separator. nscoord primaryX = rowRect.x; nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn(); if (primaryCol) { // Paint the primary cell. nsRect cellRect; rv = primaryCol->GetRect(this, rowRect.y, rowRect.height, &cellRect); if (NS_FAILED(rv)) { MOZ_ASSERT_UNREACHABLE("primary column is invalid"); return result; } if (OffsetForHorzScroll(cellRect, false)) { cellRect.x += aPt.x; nsRect dirtyRect; nsRect checkRect(cellRect.x, originalRowRect.y, cellRect.width, originalRowRect.height); if (dirtyRect.IntersectRect(aDirtyRect, checkRect)) { result &= PaintCell(aRowIndex, primaryCol, cellRect, aPresContext, aRenderingContext, aDirtyRect, primaryX, aPt, aBuilder); } } // Paint the left side of the separator. nscoord currX; nsTreeColumn* previousCol = primaryCol->GetPrevious(); if (previousCol) { nsRect prevColRect; rv = previousCol->GetRect(this, 0, 0, &prevColRect); if (NS_SUCCEEDED(rv)) { currX = (prevColRect.x - mHorzPosition) + prevColRect.width + aPt.x; } else { MOZ_ASSERT_UNREACHABLE( "The column before the primary column is " "invalid"); currX = rowRect.x; } } else { currX = rowRect.x; } int32_t level; view->GetLevel(aRowIndex, &level); if (level == 0) currX += mIndentation; if (currX > rowRect.x) { nsRect separatorRect(rowRect); separatorRect.width -= rowRect.x + rowRect.width - currX; result &= PaintSeparator(aRowIndex, separatorRect, aPresContext, aRenderingContext, aDirtyRect); } } // Paint the right side (whole) separator. nsRect separatorRect(rowRect); if (primaryX > rowRect.x) { separatorRect.width -= primaryX - rowRect.x; separatorRect.x += primaryX - rowRect.x; } result &= PaintSeparator(aRowIndex, separatorRect, aPresContext, aRenderingContext, aDirtyRect); } else { // Now loop over our cells. Only paint a cell if it intersects with our // dirty rect. for (nsTreeColumn* currCol = mColumns->GetFirstColumn(); currCol; currCol = currCol->GetNext()) { nsRect cellRect; rv = currCol->GetRect(this, rowRect.y, rowRect.height, &cellRect); // Don't paint cells in hidden columns. if (NS_FAILED(rv) || cellRect.width == 0) continue; if (OffsetForHorzScroll(cellRect, false)) { cellRect.x += aPt.x; // for primary columns, use the row's vertical size so that the // lines get drawn properly nsRect checkRect = cellRect; if (currCol->IsPrimary()) checkRect = nsRect(cellRect.x, originalRowRect.y, cellRect.width, originalRowRect.height); nsRect dirtyRect; nscoord dummy; if (dirtyRect.IntersectRect(aDirtyRect, checkRect)) result &= PaintCell(aRowIndex, currCol, cellRect, aPresContext, aRenderingContext, aDirtyRect, dummy, aPt, aBuilder); } } } return result; } ImgDrawResult nsTreeBodyFrame::PaintSeparator(int32_t aRowIndex, const nsRect& aSeparatorRect, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aDirtyRect) { // Resolve style for the separator. ComputedStyle* separatorContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeSeparator()); bool useTheme = false; nsITheme* theme = nullptr; StyleAppearance appearance = separatorContext->StyleDisplay()->EffectiveAppearance(); if (appearance != StyleAppearance::None) { theme = aPresContext->Theme(); if (theme->ThemeSupportsWidget(aPresContext, nullptr, appearance)) useTheme = true; } ImgDrawResult result = ImgDrawResult::SUCCESS; // use -moz-appearance if provided. if (useTheme) { nsRect dirty; dirty.IntersectRect(aSeparatorRect, aDirtyRect); theme->DrawWidgetBackground(&aRenderingContext, this, appearance, aSeparatorRect, dirty); } else { const nsStylePosition* stylePosition = separatorContext->StylePosition(); // Obtain the height for the separator or use the default value. nscoord height; if (stylePosition->mHeight.ConvertsToLength()) { height = stylePosition->mHeight.ToLength(); } else { // Use default height 2px. height = nsPresContext::CSSPixelsToAppUnits(2); } // Obtain the margins for the separator and then deflate our rect by that // amount. The separator is assumed to be contained within the deflated // rect. nsRect separatorRect(aSeparatorRect.x, aSeparatorRect.y, aSeparatorRect.width, height); nsMargin separatorMargin; separatorContext->StyleMargin()->GetMargin(separatorMargin); separatorRect.Deflate(separatorMargin); // Center the separator. separatorRect.y += (aSeparatorRect.height - height) / 2; result &= PaintBackgroundLayer(separatorContext, aPresContext, aRenderingContext, separatorRect, aDirtyRect); } return result; } ImgDrawResult nsTreeBodyFrame::PaintCell( int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aCellRect, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aDirtyRect, nscoord& aCurrX, nsPoint aPt, nsDisplayListBuilder* aBuilder) { MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); // Now obtain the properties for our cell. // XXX Automatically fill in the following props: open, closed, container, // leaf, selected, focused, and the col ID. PrefillPropertyArray(aRowIndex, aColumn); nsAutoString properties; nsCOMPtr view = GetExistingView(); view->GetCellProperties(aRowIndex, aColumn, properties); nsTreeUtils::TokenizeProperties(properties, mScratchArray); // Resolve style for the cell. It contains all the info we need to lay // ourselves out and to paint. ComputedStyle* cellContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; // Obtain the margins for the cell and then deflate our rect by that // amount. The cell is assumed to be contained within the deflated rect. nsRect cellRect(aCellRect); nsMargin cellMargin; cellContext->StyleMargin()->GetMargin(cellMargin); cellRect.Deflate(cellMargin); // Paint our borders and background for our row rect. ImgDrawResult result = PaintBackgroundLayer( cellContext, aPresContext, aRenderingContext, cellRect, aDirtyRect); // Adjust the rect for its border and padding. AdjustForBorderPadding(cellContext, cellRect); nscoord currX = cellRect.x; nscoord remainingWidth = cellRect.width; // Now we paint the contents of the cells. // Directionality of the tree determines the order in which we paint. // StyleDirection::Ltr means paint from left to right. // StyleDirection::Rtl means paint from right to left. if (aColumn->IsPrimary()) { // If we're the primary column, we need to indent and paint the twisty and // any connecting lines between siblings. int32_t level; view->GetLevel(aRowIndex, &level); if (!isRTL) currX += mIndentation * level; remainingWidth -= mIndentation * level; // Resolve the style to use for the connecting lines. ComputedStyle* lineContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeLine()); if (mIndentation && level && lineContext->StyleVisibility()->IsVisibleOrCollapsed()) { // Paint the thread lines. // Get the size of the twisty. We don't want to paint the twisty // before painting of connecting lines since it would paint lines over // the twisty. But we need to leave a place for it. ComputedStyle* twistyContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); nsRect imageSize; nsRect twistyRect(aCellRect); GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, aPresContext, twistyContext); nsMargin twistyMargin; twistyContext->StyleMargin()->GetMargin(twistyMargin); twistyRect.Inflate(twistyMargin); const nsStyleBorder* borderStyle = lineContext->StyleBorder(); // Resolve currentcolor values against the treeline context nscolor color = borderStyle->mBorderLeftColor.CalcColor(*lineContext); ColorPattern colorPatt(ToDeviceColor(color)); StyleBorderStyle style = borderStyle->GetBorderStyle(eSideLeft); StrokeOptions strokeOptions; nsLayoutUtils::InitDashPattern(strokeOptions, style); nscoord srcX = currX + twistyRect.width - mIndentation / 2; nscoord lineY = (aRowIndex - mTopRowIndex) * mRowHeight + aPt.y; DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); nsPresContext* pc = PresContext(); // Don't paint off our cell. if (srcX <= cellRect.x + cellRect.width) { nscoord destX = currX + twistyRect.width; if (destX > cellRect.x + cellRect.width) destX = cellRect.x + cellRect.width; if (isRTL) { srcX = currX + remainingWidth - (srcX - cellRect.x); destX = currX + remainingWidth - (destX - cellRect.x); } Point p1(pc->AppUnitsToGfxUnits(srcX), pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2)); Point p2(pc->AppUnitsToGfxUnits(destX), pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2)); SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget, strokeOptions.mLineWidth); drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions); } int32_t currentParent = aRowIndex; for (int32_t i = level; i > 0; i--) { if (srcX <= cellRect.x + cellRect.width) { // Paint full vertical line only if we have next sibling. bool hasNextSibling; view->HasNextSibling(currentParent, aRowIndex, &hasNextSibling); if (hasNextSibling || i == level) { Point p1(pc->AppUnitsToGfxUnits(srcX), pc->AppUnitsToGfxUnits(lineY)); Point p2; p2.x = pc->AppUnitsToGfxUnits(srcX); if (hasNextSibling) p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight); else if (i == level) p2.y = pc->AppUnitsToGfxUnits(lineY + mRowHeight / 2); SnapLineToDevicePixelsForStroking(p1, p2, *drawTarget, strokeOptions.mLineWidth); drawTarget->StrokeLine(p1, p2, colorPatt, strokeOptions); } } int32_t parent; if (NS_FAILED(view->GetParentIndex(currentParent, &parent)) || parent < 0) break; currentParent = parent; srcX -= mIndentation; } } // Always leave space for the twisty. nsRect twistyRect(currX, cellRect.y, remainingWidth, cellRect.height); result &= PaintTwisty(aRowIndex, aColumn, twistyRect, aPresContext, aRenderingContext, aDirtyRect, remainingWidth, currX); } // Now paint the icon for our cell. nsRect iconRect(currX, cellRect.y, remainingWidth, cellRect.height); nsRect dirtyRect; if (dirtyRect.IntersectRect(aDirtyRect, iconRect)) { result &= PaintImage(aRowIndex, aColumn, iconRect, aPresContext, aRenderingContext, aDirtyRect, remainingWidth, currX, aBuilder); } // Now paint our element, but only if we aren't a cycler column. // XXX until we have the ability to load images, allow the view to // insert text into cycler columns... if (!aColumn->IsCycler()) { nsRect elementRect(currX, cellRect.y, remainingWidth, cellRect.height); nsRect dirtyRect; if (dirtyRect.IntersectRect(aDirtyRect, elementRect)) { switch (aColumn->GetType()) { case TreeColumn_Binding::TYPE_TEXT: result &= PaintText(aRowIndex, aColumn, elementRect, aPresContext, aRenderingContext, aDirtyRect, currX); break; case TreeColumn_Binding::TYPE_CHECKBOX: result &= PaintCheckbox(aRowIndex, aColumn, elementRect, aPresContext, aRenderingContext, aDirtyRect); break; } } } aCurrX = currX; return result; } ImgDrawResult nsTreeBodyFrame::PaintTwisty( int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTwistyRect, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX) { MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; nscoord rightEdge = aCurrX + aRemainingWidth; // Paint the twisty, but only if we are a non-empty container. bool shouldPaint = false; bool isContainer = false; nsCOMPtr view = GetExistingView(); view->IsContainer(aRowIndex, &isContainer); if (isContainer) { bool isContainerEmpty = false; view->IsContainerEmpty(aRowIndex, &isContainerEmpty); if (!isContainerEmpty) shouldPaint = true; } // Resolve style for the twisty. ComputedStyle* twistyContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); // Obtain the margins for the twisty and then deflate our rect by that // amount. The twisty is assumed to be contained within the deflated rect. nsRect twistyRect(aTwistyRect); nsMargin twistyMargin; twistyContext->StyleMargin()->GetMargin(twistyMargin); twistyRect.Deflate(twistyMargin); nsRect imageSize; nsITheme* theme = GetTwistyRect(aRowIndex, aColumn, imageSize, twistyRect, aPresContext, twistyContext); // Subtract out the remaining width. This is done even when we don't actually // paint a twisty in this cell, so that cells in different rows still line up. nsRect copyRect(twistyRect); copyRect.Inflate(twistyMargin); aRemainingWidth -= copyRect.width; if (!isRTL) aCurrX += copyRect.width; ImgDrawResult result = ImgDrawResult::SUCCESS; if (shouldPaint) { // Paint our borders and background for our image rect. result &= PaintBackgroundLayer(twistyContext, aPresContext, aRenderingContext, twistyRect, aDirtyRect); if (theme) { if (isRTL) twistyRect.x = rightEdge - twistyRect.width; // yeah, I know it says we're drawing a background, but a twisty is really // a fg object since it doesn't have anything that gecko would want to // draw over it. Besides, we have to prevent imagelib from drawing it. nsRect dirty; dirty.IntersectRect(twistyRect, aDirtyRect); theme->DrawWidgetBackground( &aRenderingContext, this, twistyContext->StyleDisplay()->EffectiveAppearance(), twistyRect, dirty); } else { // Time to paint the twisty. // Adjust the rect for its border and padding. nsMargin bp(0, 0, 0, 0); GetBorderPadding(twistyContext, bp); twistyRect.Deflate(bp); if (isRTL) twistyRect.x = rightEdge - twistyRect.width; imageSize.Deflate(bp); // Get the image for drawing. nsCOMPtr image; GetImage(aRowIndex, aColumn, true, twistyContext, getter_AddRefs(image)); if (image) { nsPoint anchorPoint = twistyRect.TopLeft(); // Center the image. XXX Obey vertical-align style prop? if (imageSize.height < twistyRect.height) { anchorPoint.y += (twistyRect.height - imageSize.height) / 2; } // Apply context paint if applicable SVGImageContext svgContext; SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext, *twistyContext, image); // Paint the image. result &= nsLayoutUtils::DrawSingleUnscaledImage( aRenderingContext, aPresContext, image, SamplingFilter::POINT, anchorPoint, &aDirtyRect, svgContext, imgIContainer::FLAG_NONE, &imageSize); } } } return result; } ImgDrawResult nsTreeBodyFrame::PaintImage( int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aImageRect, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aDirtyRect, nscoord& aRemainingWidth, nscoord& aCurrX, nsDisplayListBuilder* aBuilder) { MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; nscoord rightEdge = aCurrX + aRemainingWidth; // Resolve style for the image. ComputedStyle* imageContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeImage()); // Obtain the margins for the image and then deflate our rect by that // amount. The image is assumed to be contained within the deflated rect. nsRect imageRect(aImageRect); nsMargin imageMargin; imageContext->StyleMargin()->GetMargin(imageMargin); imageRect.Deflate(imageMargin); // Get the image. nsCOMPtr image; GetImage(aRowIndex, aColumn, false, imageContext, getter_AddRefs(image)); // Get the image destination size. nsSize imageDestSize = GetImageDestSize(imageContext, image); if (!imageDestSize.width || !imageDestSize.height) { return ImgDrawResult::SUCCESS; } // Get the borders and padding. nsMargin bp(0, 0, 0, 0); GetBorderPadding(imageContext, bp); // destRect will be passed as the aDestRect argument in the DrawImage method. // Start with the imageDestSize width and height. nsRect destRect(0, 0, imageDestSize.width, imageDestSize.height); // Inflate destRect for borders and padding so that we can compare/adjust // with respect to imageRect. destRect.Inflate(bp); // The destRect width and height have not been adjusted to fit within the // cell width and height. // We must adjust the width even if image is null, because the width is used // to update the aRemainingWidth and aCurrX values. // Since the height isn't used unless the image is not null, we will adjust // the height inside the if (image) block below. if (destRect.width > imageRect.width) { // The destRect is too wide to fit within the cell width. // Adjust destRect width to fit within the cell width. destRect.width = imageRect.width; } else { // The cell is wider than the destRect. // In a cycler column, the image is centered horizontally. if (!aColumn->IsCycler()) { // If this column is not a cycler, we won't center the image horizontally. // We adjust the imageRect width so that the image is placed at the start // of the cell. imageRect.width = destRect.width; } } ImgDrawResult result = ImgDrawResult::SUCCESS; if (image) { if (isRTL) imageRect.x = rightEdge - imageRect.width; // Paint our borders and background for our image rect result &= PaintBackgroundLayer(imageContext, aPresContext, aRenderingContext, imageRect, aDirtyRect); // The destRect x and y have not been set yet. Let's do that now. // Initially, we use the imageRect x and y. destRect.x = imageRect.x; destRect.y = imageRect.y; if (destRect.width < imageRect.width) { // The destRect width is smaller than the cell width. // Center the image horizontally in the cell. // Adjust the destRect x accordingly. destRect.x += (imageRect.width - destRect.width) / 2; } // Now it's time to adjust the destRect height to fit within the cell // height. if (destRect.height > imageRect.height) { // The destRect height is larger than the cell height. // Adjust destRect height to fit within the cell height. destRect.height = imageRect.height; } else if (destRect.height < imageRect.height) { // The destRect height is smaller than the cell height. // Center the image vertically in the cell. // Adjust the destRect y accordingly. destRect.y += (imageRect.height - destRect.height) / 2; } // It's almost time to paint the image. // Deflate destRect for the border and padding. destRect.Deflate(bp); // Compute the area where our whole image would be mapped, to get the // desired subregion onto our actual destRect: nsRect wholeImageDest; CSSIntSize rawImageCSSIntSize; if (NS_SUCCEEDED(image->GetWidth(&rawImageCSSIntSize.width)) && NS_SUCCEEDED(image->GetHeight(&rawImageCSSIntSize.height))) { // Get the image source rectangle - the rectangle containing the part of // the image that we are going to display. sourceRect will be passed as // the aSrcRect argument in the DrawImage method. nsRect sourceRect = GetImageSourceRect(imageContext, image); // Let's say that the image is 100 pixels tall and that the CSS has // specified that the destination height should be 50 pixels tall. Let's // say that the cell height is only 20 pixels. So, in those 20 visible // pixels, we want to see the top 20/50ths of the image. So, the // sourceRect.height should be 100 * 20 / 50, which is 40 pixels. // Essentially, we are scaling the image as dictated by the CSS // destination height and width, and we are then clipping the scaled // image by the cell width and height. nsSize rawImageSize(CSSPixel::ToAppUnits(rawImageCSSIntSize)); wholeImageDest = nsLayoutUtils::GetWholeImageDestination( rawImageSize, sourceRect, nsRect(destRect.TopLeft(), imageDestSize)); } else { // GetWidth/GetHeight failed, so we can't easily map a subregion of the // source image onto the destination area. // * If this happens with a RasterImage, it probably means the image is // in an error state, and we shouldn't draw anything. Hence, we leave // wholeImageDest as an empty rect (its initial state). // * If this happens with a VectorImage, it probably means the image has // no explicit width or height attribute -- but we can still proceed and // just treat the destination area as our whole SVG image area. Hence, we // set wholeImageDest to the full destRect. if (image->GetType() == imgIContainer::TYPE_VECTOR) { wholeImageDest = destRect; } } const auto* styleEffects = imageContext->StyleEffects(); gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aRenderingContext); if (!styleEffects->IsOpaque()) { autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, styleEffects->mOpacity); } uint32_t drawFlags = aBuilder && aBuilder->UseHighQualityScaling() ? imgIContainer::FLAG_HIGH_QUALITY_SCALING : imgIContainer::FLAG_NONE; result &= nsLayoutUtils::DrawImage( aRenderingContext, imageContext, aPresContext, image, nsLayoutUtils::GetSamplingFilterForFrame(this), wholeImageDest, destRect, destRect.TopLeft(), aDirtyRect, drawFlags); } // Update the aRemainingWidth and aCurrX values. imageRect.Inflate(imageMargin); aRemainingWidth -= imageRect.width; if (!isRTL) { aCurrX += imageRect.width; } return result; } ImgDrawResult nsTreeBodyFrame::PaintText( int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aTextRect, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aDirtyRect, nscoord& aCurrX) { MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); bool isRTL = StyleVisibility()->mDirection == StyleDirection::Rtl; // Now obtain the text for our cell. nsAutoString text; nsCOMPtr view = GetExistingView(); view->GetCellText(aRowIndex, aColumn, text); // We're going to paint this text so we need to ensure bidi is enabled if // necessary CheckTextForBidi(text); ImgDrawResult result = ImgDrawResult::SUCCESS; if (text.Length() == 0) { // Don't paint an empty string. XXX What about background/borders? Still // paint? return result; } int32_t appUnitsPerDevPixel = PresContext()->AppUnitsPerDevPixel(); DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); // Resolve style for the text. It contains all the info we need to lay // ourselves out and to paint. ComputedStyle* textContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCellText()); // Obtain the margins for the text and then deflate our rect by that // amount. The text is assumed to be contained within the deflated rect. nsRect textRect(aTextRect); nsMargin textMargin; textContext->StyleMargin()->GetMargin(textMargin); textRect.Deflate(textMargin); // Adjust the rect for its border and padding. nsMargin bp(0, 0, 0, 0); GetBorderPadding(textContext, bp); textRect.Deflate(bp); // Compute our text size. RefPtr fontMet = nsLayoutUtils::GetFontMetricsForComputedStyle(textContext, PresContext()); nscoord height = fontMet->MaxHeight(); nscoord baseline = fontMet->MaxAscent(); // Center the text. XXX Obey vertical-align style prop? if (height < textRect.height) { textRect.y += (textRect.height - height) / 2; textRect.height = height; } // Set our font. AdjustForCellText(text, aRowIndex, aColumn, aRenderingContext, *fontMet, textRect); textRect.Inflate(bp); // Subtract out the remaining width. if (!isRTL) aCurrX += textRect.width + textMargin.LeftRight(); result &= PaintBackgroundLayer(textContext, aPresContext, aRenderingContext, textRect, aDirtyRect); // Time to paint our text. textRect.Deflate(bp); // Set our color. ColorPattern color(ToDeviceColor(textContext->StyleText()->mColor)); // Draw decorations. StyleTextDecorationLine decorations = textContext->StyleTextReset()->mTextDecorationLine; nscoord offset; nscoord size; if (decorations & (StyleTextDecorationLine::OVERLINE | StyleTextDecorationLine::UNDERLINE)) { fontMet->GetUnderline(offset, size); if (decorations & StyleTextDecorationLine::OVERLINE) { nsRect r(textRect.x, textRect.y, textRect.width, size); Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); drawTarget->FillRect(devPxRect, color); } if (decorations & StyleTextDecorationLine::UNDERLINE) { nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width, size); Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); drawTarget->FillRect(devPxRect, color); } } if (decorations & StyleTextDecorationLine::LINE_THROUGH) { fontMet->GetStrikeout(offset, size); nsRect r(textRect.x, textRect.y + baseline - offset, textRect.width, size); Rect devPxRect = NSRectToSnappedRect(r, appUnitsPerDevPixel, *drawTarget); drawTarget->FillRect(devPxRect, color); } ComputedStyle* cellContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCell()); const auto* styleEffects = textContext->StyleEffects(); gfxGroupForBlendAutoSaveRestore autoGroupForBlend(&aRenderingContext); if (!styleEffects->IsOpaque()) { autoGroupForBlend.PushGroupForBlendBack(gfxContentType::COLOR_ALPHA, styleEffects->mOpacity); } aRenderingContext.SetColor( sRGBColor::FromABGR(textContext->StyleText()->mColor.ToColor())); nsLayoutUtils::DrawString( this, *fontMet, &aRenderingContext, text.get(), text.Length(), textRect.TopLeft() + nsPoint(0, baseline), cellContext); return result; } ImgDrawResult nsTreeBodyFrame::PaintCheckbox(int32_t aRowIndex, nsTreeColumn* aColumn, const nsRect& aCheckboxRect, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aDirtyRect) { MOZ_ASSERT(aColumn && aColumn->GetFrame(), "invalid column passed"); // Resolve style for the checkbox. ComputedStyle* checkboxContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeCheckbox()); nscoord rightEdge = aCheckboxRect.XMost(); // Obtain the margins for the checkbox and then deflate our rect by that // amount. The checkbox is assumed to be contained within the deflated rect. nsRect checkboxRect(aCheckboxRect); nsMargin checkboxMargin; checkboxContext->StyleMargin()->GetMargin(checkboxMargin); checkboxRect.Deflate(checkboxMargin); nsRect imageSize = GetImageSize(aRowIndex, aColumn, true, checkboxContext); if (imageSize.height > checkboxRect.height) { imageSize.height = checkboxRect.height; } if (imageSize.width > checkboxRect.width) { imageSize.width = checkboxRect.width; } if (StyleVisibility()->mDirection == StyleDirection::Rtl) { checkboxRect.x = rightEdge - checkboxRect.width; } // Paint our borders and background for our image rect. ImgDrawResult result = PaintBackgroundLayer(checkboxContext, aPresContext, aRenderingContext, checkboxRect, aDirtyRect); // Time to paint the checkbox. // Adjust the rect for its border and padding. nsMargin bp(0, 0, 0, 0); GetBorderPadding(checkboxContext, bp); checkboxRect.Deflate(bp); // Get the image for drawing. nsCOMPtr image; GetImage(aRowIndex, aColumn, true, checkboxContext, getter_AddRefs(image)); if (image) { nsPoint pt = checkboxRect.TopLeft(); if (imageSize.height < checkboxRect.height) { pt.y += (checkboxRect.height - imageSize.height) / 2; } if (imageSize.width < checkboxRect.width) { pt.x += (checkboxRect.width - imageSize.width) / 2; } // Apply context paint if applicable SVGImageContext svgContext; SVGImageContext::MaybeStoreContextPaint(svgContext, *aPresContext, *checkboxContext, image); // Paint the image. result &= nsLayoutUtils::DrawSingleUnscaledImage( aRenderingContext, aPresContext, image, SamplingFilter::POINT, pt, &aDirtyRect, svgContext, imgIContainer::FLAG_NONE, &imageSize); } return result; } ImgDrawResult nsTreeBodyFrame::PaintDropFeedback( const nsRect& aDropFeedbackRect, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aDirtyRect, nsPoint aPt) { // Paint the drop feedback in between rows. nscoord currX; // Adjust for the primary cell. nsTreeColumn* primaryCol = mColumns->GetPrimaryColumn(); if (primaryCol) { #ifdef DEBUG nsresult rv = #endif primaryCol->GetXInTwips(this, &currX); NS_ASSERTION(NS_SUCCEEDED(rv), "primary column is invalid?"); currX += aPt.x - mHorzPosition; } else { currX = aDropFeedbackRect.x; } PrefillPropertyArray(mSlots->mDropRow, primaryCol); // Resolve the style to use for the drop feedback. ComputedStyle* feedbackContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeDropFeedback()); ImgDrawResult result = ImgDrawResult::SUCCESS; // Paint only if it is visible. nsCOMPtr view = GetExistingView(); if (feedbackContext->StyleVisibility()->IsVisibleOrCollapsed()) { int32_t level; view->GetLevel(mSlots->mDropRow, &level); // If our previous or next row has greater level use that for // correct visual indentation. if (mSlots->mDropOrient == nsITreeView::DROP_BEFORE) { if (mSlots->mDropRow > 0) { int32_t previousLevel; view->GetLevel(mSlots->mDropRow - 1, &previousLevel); if (previousLevel > level) level = previousLevel; } } else { if (mSlots->mDropRow < mRowCount - 1) { int32_t nextLevel; view->GetLevel(mSlots->mDropRow + 1, &nextLevel); if (nextLevel > level) level = nextLevel; } } currX += mIndentation * level; if (primaryCol) { ComputedStyle* twistyContext = GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeTwisty()); nsRect imageSize; nsRect twistyRect; GetTwistyRect(mSlots->mDropRow, primaryCol, imageSize, twistyRect, aPresContext, twistyContext); nsMargin twistyMargin; twistyContext->StyleMargin()->GetMargin(twistyMargin); twistyRect.Inflate(twistyMargin); currX += twistyRect.width; } const nsStylePosition* stylePosition = feedbackContext->StylePosition(); // Obtain the width for the drop feedback or use default value. nscoord width; if (stylePosition->mWidth.ConvertsToLength()) { width = stylePosition->mWidth.ToLength(); } else { // Use default width 50px. width = nsPresContext::CSSPixelsToAppUnits(50); } // Obtain the height for the drop feedback or use default value. nscoord height; if (stylePosition->mHeight.ConvertsToLength()) { height = stylePosition->mHeight.ToLength(); } else { // Use default height 2px. height = nsPresContext::CSSPixelsToAppUnits(2); } // Obtain the margins for the drop feedback and then deflate our rect // by that amount. nsRect feedbackRect(currX, aDropFeedbackRect.y, width, height); nsMargin margin; feedbackContext->StyleMargin()->GetMargin(margin); feedbackRect.Deflate(margin); feedbackRect.y += (aDropFeedbackRect.height - height) / 2; // Finally paint the drop feedback. result &= PaintBackgroundLayer(feedbackContext, aPresContext, aRenderingContext, feedbackRect, aDirtyRect); } return result; } ImgDrawResult nsTreeBodyFrame::PaintBackgroundLayer( ComputedStyle* aComputedStyle, nsPresContext* aPresContext, gfxContext& aRenderingContext, const nsRect& aRect, const nsRect& aDirtyRect) { const nsStyleBorder* myBorder = aComputedStyle->StyleBorder(); nsCSSRendering::PaintBGParams params = nsCSSRendering::PaintBGParams::ForAllLayers( *aPresContext, aDirtyRect, aRect, this, nsCSSRendering::PAINTBG_SYNC_DECODE_IMAGES); ImgDrawResult result = nsCSSRendering::PaintStyleImageLayerWithSC( params, aRenderingContext, aComputedStyle, *myBorder); result &= nsCSSRendering::PaintBorderWithStyleBorder( aPresContext, aRenderingContext, this, aDirtyRect, aRect, *myBorder, mComputedStyle, PaintBorderFlags::SyncDecodeImages); nsCSSRendering::PaintNonThemedOutline(aPresContext, aRenderingContext, this, aDirtyRect, aRect, aComputedStyle); return result; } // Scrolling nsresult nsTreeBodyFrame::EnsureRowIsVisible(int32_t aRow) { ScrollParts parts = GetScrollParts(); nsresult rv = EnsureRowIsVisibleInternal(parts, aRow); NS_ENSURE_SUCCESS(rv, rv); UpdateScrollbars(parts); return rv; } nsresult nsTreeBodyFrame::EnsureRowIsVisibleInternal(const ScrollParts& aParts, int32_t aRow) { if (!mView || !mPageLength) { return NS_OK; } if (mTopRowIndex <= aRow && mTopRowIndex + mPageLength > aRow) return NS_OK; if (aRow < mTopRowIndex) ScrollToRowInternal(aParts, aRow); else { // Bring it just on-screen. int32_t distance = aRow - (mTopRowIndex + mPageLength) + 1; ScrollToRowInternal(aParts, mTopRowIndex + distance); } return NS_OK; } nsresult nsTreeBodyFrame::EnsureCellIsVisible(int32_t aRow, nsTreeColumn* aCol) { if (!aCol) return NS_ERROR_INVALID_ARG; ScrollParts parts = GetScrollParts(); nscoord result = -1; nsresult rv; nscoord columnPos; rv = aCol->GetXInTwips(this, &columnPos); if (NS_FAILED(rv)) return rv; nscoord columnWidth; rv = aCol->GetWidthInTwips(this, &columnWidth); if (NS_FAILED(rv)) return rv; // If the start of the column is before the // start of the horizontal view, then scroll if (columnPos < mHorzPosition) result = columnPos; // If the end of the column is past the end of // the horizontal view, then scroll else if ((columnPos + columnWidth) > (mHorzPosition + mInnerBox.width)) result = ((columnPos + columnWidth) - (mHorzPosition + mInnerBox.width)) + mHorzPosition; if (result != -1) { rv = ScrollHorzInternal(parts, result); if (NS_FAILED(rv)) return rv; } rv = EnsureRowIsVisibleInternal(parts, aRow); NS_ENSURE_SUCCESS(rv, rv); UpdateScrollbars(parts); return rv; } void nsTreeBodyFrame::ScrollToRow(int32_t aRow) { ScrollParts parts = GetScrollParts(); ScrollToRowInternal(parts, aRow); UpdateScrollbars(parts); } nsresult nsTreeBodyFrame::ScrollToRowInternal(const ScrollParts& aParts, int32_t aRow) { ScrollInternal(aParts, aRow); return NS_OK; } void nsTreeBodyFrame::ScrollByLines(int32_t aNumLines) { if (!mView) { return; } int32_t newIndex = mTopRowIndex + aNumLines; ScrollToRow(newIndex); } void nsTreeBodyFrame::ScrollByPages(int32_t aNumPages) { if (!mView) { return; } int32_t newIndex = mTopRowIndex + aNumPages * mPageLength; ScrollToRow(newIndex); } nsresult nsTreeBodyFrame::ScrollInternal(const ScrollParts& aParts, int32_t aRow) { if (!mView) { return NS_OK; } // Note that we may be "over scrolled" at this point; that is the // current mTopRowIndex may be larger than mRowCount - mPageLength. // This can happen when items are removed for example. (bug 1085050) int32_t maxTopRowIndex = std::max(0, mRowCount - mPageLength); aRow = mozilla::clamped(aRow, 0, maxTopRowIndex); if (aRow == mTopRowIndex) { return NS_OK; } mTopRowIndex = aRow; Invalidate(); PostScrollEvent(); return NS_OK; } nsresult nsTreeBodyFrame::ScrollHorzInternal(const ScrollParts& aParts, int32_t aPosition) { if (!mView || !aParts.mColumnsScrollFrame || !aParts.mHScrollbar) return NS_OK; if (aPosition == mHorzPosition) return NS_OK; if (aPosition < 0 || aPosition > mHorzWidth) return NS_OK; nsRect bounds = aParts.mColumnsFrame->GetRect(); if (aPosition > (mHorzWidth - bounds.width)) aPosition = mHorzWidth - bounds.width; mHorzPosition = aPosition; Invalidate(); // Update the column scroll view AutoWeakFrame weakFrame(this); aParts.mColumnsScrollFrame->ScrollTo(nsPoint(mHorzPosition, 0), ScrollMode::Instant); if (!weakFrame.IsAlive()) { return NS_ERROR_FAILURE; } // And fire off an event about it all PostScrollEvent(); return NS_OK; } void nsTreeBodyFrame::ScrollByPage(nsScrollbarFrame* aScrollbar, int32_t aDirection, ScrollSnapFlags aSnapFlags) { // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored MOZ_ASSERT(aScrollbar != nullptr); ScrollByPages(aDirection); } void nsTreeBodyFrame::ScrollByWhole(nsScrollbarFrame* aScrollbar, int32_t aDirection, ScrollSnapFlags aSnapFlags) { // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored MOZ_ASSERT(aScrollbar != nullptr); int32_t newIndex = aDirection < 0 ? 0 : mTopRowIndex; ScrollToRow(newIndex); } void nsTreeBodyFrame::ScrollByLine(nsScrollbarFrame* aScrollbar, int32_t aDirection, ScrollSnapFlags aSnapFlags) { // CSS Scroll Snapping is not enabled for XUL, aSnap is ignored MOZ_ASSERT(aScrollbar != nullptr); ScrollByLines(aDirection); } void nsTreeBodyFrame::ScrollByUnit( nsScrollbarFrame* aScrollbar, ScrollMode aMode, int32_t aDirection, ScrollUnit aUnit, ScrollSnapFlags aSnapFlags /* = Disabled */) { MOZ_ASSERT_UNREACHABLE("Can't get here, we pass false to MoveToNewPosition"); } void nsTreeBodyFrame::RepeatButtonScroll(nsScrollbarFrame* aScrollbar) { ScrollParts parts = GetScrollParts(); int32_t increment = aScrollbar->GetIncrement(); int32_t direction = 0; if (increment < 0) { direction = -1; } else if (increment > 0) { direction = 1; } bool isHorizontal = aScrollbar->IsHorizontal(); AutoWeakFrame weakFrame(this); if (isHorizontal) { int32_t curpos = aScrollbar->MoveToNewPosition( nsScrollbarFrame::ImplementsScrollByUnit::No); if (weakFrame.IsAlive()) { ScrollHorzInternal(parts, curpos); } } else { ScrollToRowInternal(parts, mTopRowIndex + direction); } if (weakFrame.IsAlive() && mScrollbarActivity) { mScrollbarActivity->ActivityOccurred(); } if (weakFrame.IsAlive()) { UpdateScrollbars(parts); } } void nsTreeBodyFrame::ThumbMoved(nsScrollbarFrame* aScrollbar, nscoord aOldPos, nscoord aNewPos) { ScrollParts parts = GetScrollParts(); if (aOldPos == aNewPos) return; AutoWeakFrame weakFrame(this); // Vertical Scrollbar if (parts.mVScrollbar == aScrollbar) { nscoord rh = nsPresContext::AppUnitsToIntCSSPixels(mRowHeight); nscoord newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos); nscoord newrow = (rh > 0) ? (newIndex / rh) : 0; ScrollInternal(parts, newrow); // Horizontal Scrollbar } else if (parts.mHScrollbar == aScrollbar) { int32_t newIndex = nsPresContext::AppUnitsToIntCSSPixels(aNewPos); ScrollHorzInternal(parts, newIndex); } if (weakFrame.IsAlive()) { UpdateScrollbars(parts); } } // The style cache. ComputedStyle* nsTreeBodyFrame::GetPseudoComputedStyle( nsCSSAnonBoxPseudoStaticAtom* aPseudoElement) { return mStyleCache.GetComputedStyle(PresContext(), mContent, mComputedStyle, aPseudoElement, mScratchArray); } XULTreeElement* nsTreeBodyFrame::GetBaseElement() { if (!mTree) { nsIFrame* parent = GetParent(); while (parent) { nsIContent* content = parent->GetContent(); if (content && content->IsXULElement(nsGkAtoms::tree)) { mTree = XULTreeElement::FromNodeOrNull(content->AsElement()); break; } parent = parent->GetInFlowParent(); } } return mTree; } nsresult nsTreeBodyFrame::ClearStyleAndImageCaches() { mStyleCache.Clear(); CancelImageRequests(); mImageCache.Clear(); return NS_OK; } void nsTreeBodyFrame::RemoveImageCacheEntry(int32_t aRowIndex, nsTreeColumn* aCol) { nsAutoString imageSrc; nsCOMPtr view = GetExistingView(); if (!view || NS_FAILED(view->GetImageSrc(aRowIndex, aCol, imageSrc))) { return; } nsTreeImageCacheEntry entry; if (!mImageCache.Get(imageSrc, &entry)) { return; } nsLayoutUtils::DeregisterImageRequest(PresContext(), entry.request, nullptr); entry.request->UnlockImage(); entry.request->CancelAndForgetObserver(NS_BINDING_ABORTED); mImageCache.Remove(imageSrc); } /* virtual */ void nsTreeBodyFrame::DidSetComputedStyle(ComputedStyle* aOldComputedStyle) { SimpleXULLeafFrame::DidSetComputedStyle(aOldComputedStyle); // Clear the style cache; the pointers are no longer even valid mStyleCache.Clear(); // XXX The following is hacky, but it's not incorrect, // and appears to fix a few bugs with style changes, like text zoom and // dpi changes mIndentation = GetIndentation(); mRowHeight = GetRowHeight(); } bool nsTreeBodyFrame::OffsetForHorzScroll(nsRect& rect, bool clip) { rect.x -= mHorzPosition; // Scrolled out before if (rect.XMost() <= mInnerBox.x) return false; // Scrolled out after if (rect.x > mInnerBox.XMost()) return false; if (clip) { nscoord leftEdge = std::max(rect.x, mInnerBox.x); nscoord rightEdge = std::min(rect.XMost(), mInnerBox.XMost()); rect.x = leftEdge; rect.width = rightEdge - leftEdge; // Should have returned false above NS_ASSERTION(rect.width >= 0, "horz scroll code out of sync"); } return true; } bool nsTreeBodyFrame::CanAutoScroll(int32_t aRowIndex) { // Check first for partially visible last row. if (aRowIndex == mRowCount - 1) { nscoord y = mInnerBox.y + (aRowIndex - mTopRowIndex) * mRowHeight; if (y < mInnerBox.height && y + mRowHeight > mInnerBox.height) return true; } if (aRowIndex > 0 && aRowIndex < mRowCount - 1) return true; return false; } // Given a dom event, figure out which row in the tree the mouse is over, // if we should drop before/after/on that row or we should auto-scroll. // Doesn't query the content about if the drag is allowable, that's done // elsewhere. // // For containers, we break up the vertical space of the row as follows: if in // the topmost 25%, the drop is _before_ the row the mouse is over; if in the // last 25%, _after_; in the middle 50%, we consider it a drop _on_ the // container. // // For non-containers, if the mouse is in the top 50% of the row, the drop is // _before_ and the bottom 50% _after_ void nsTreeBodyFrame::ComputeDropPosition(WidgetGUIEvent* aEvent, int32_t* aRow, int16_t* aOrient, int16_t* aScrollLines) { *aOrient = -1; *aScrollLines = 0; // Convert the event's point to our coordinates. We want it in // the coordinates of our inner box's coordinates. nsPoint pt = nsLayoutUtils::GetEventCoordinatesRelativeTo(aEvent, RelativeTo{this}); int32_t xTwips = pt.x - mInnerBox.x; int32_t yTwips = pt.y - mInnerBox.y; nsCOMPtr view = GetExistingView(); *aRow = GetRowAtInternal(xTwips, yTwips); if (*aRow >= 0) { // Compute the top/bottom of the row in question. int32_t yOffset = yTwips - mRowHeight * (*aRow - mTopRowIndex); bool isContainer = false; view->IsContainer(*aRow, &isContainer); if (isContainer) { // for a container, use a 25%/50%/25% breakdown if (yOffset < mRowHeight / 4) *aOrient = nsITreeView::DROP_BEFORE; else if (yOffset > mRowHeight - (mRowHeight / 4)) *aOrient = nsITreeView::DROP_AFTER; else *aOrient = nsITreeView::DROP_ON; } else { // for a non-container use a 50%/50% breakdown if (yOffset < mRowHeight / 2) *aOrient = nsITreeView::DROP_BEFORE; else *aOrient = nsITreeView::DROP_AFTER; } } if (CanAutoScroll(*aRow)) { // Get the max value from the look and feel service. int32_t scrollLinesMax = LookAndFeel::GetInt(LookAndFeel::IntID::TreeScrollLinesMax, 0); scrollLinesMax--; if (scrollLinesMax < 0) scrollLinesMax = 0; // Determine if we're w/in a margin of the top/bottom of the tree during a // drag. This will ultimately cause us to scroll, but that's done elsewhere. nscoord height = (3 * mRowHeight) / 4; if (yTwips < height) { // scroll up *aScrollLines = NSToIntRound(-scrollLinesMax * (1 - (float)yTwips / height) - 1); } else if (yTwips > mRect.height - height) { // scroll down *aScrollLines = NSToIntRound( scrollLinesMax * (1 - (float)(mRect.height - yTwips) / height) + 1); } } } // ComputeDropPosition void nsTreeBodyFrame::OpenCallback(nsITimer* aTimer, void* aClosure) { auto* self = static_cast(aClosure); if (!self) { return; } aTimer->Cancel(); self->mSlots->mTimer = nullptr; nsCOMPtr view = self->GetExistingView(); if (self->mSlots->mDropRow >= 0) { self->mSlots->mArray.AppendElement(self->mSlots->mDropRow); view->ToggleOpenState(self->mSlots->mDropRow); } } void nsTreeBodyFrame::CloseCallback(nsITimer* aTimer, void* aClosure) { auto* self = static_cast(aClosure); if (!self) { return; } aTimer->Cancel(); self->mSlots->mTimer = nullptr; nsCOMPtr view = self->GetExistingView(); auto array = std::move(self->mSlots->mArray); if (!view) { return; } for (auto elem : Reversed(array)) { view->ToggleOpenState(elem); } } void nsTreeBodyFrame::LazyScrollCallback(nsITimer* aTimer, void* aClosure) { nsTreeBodyFrame* self = static_cast(aClosure); if (self) { aTimer->Cancel(); self->mSlots->mTimer = nullptr; if (self->mView) { // Set a new timer to scroll the tree repeatedly. self->CreateTimer(LookAndFeel::IntID::TreeScrollDelay, ScrollCallback, nsITimer::TYPE_REPEATING_SLACK, getter_AddRefs(self->mSlots->mTimer), "nsTreeBodyFrame::ScrollCallback"); self->ScrollByLines(self->mSlots->mScrollLines); // ScrollByLines may have deleted |self|. } } } void nsTreeBodyFrame::ScrollCallback(nsITimer* aTimer, void* aClosure) { nsTreeBodyFrame* self = static_cast(aClosure); if (self) { // Don't scroll if we are already at the top or bottom of the view. if (self->mView && self->CanAutoScroll(self->mSlots->mDropRow)) { self->ScrollByLines(self->mSlots->mScrollLines); } else { aTimer->Cancel(); self->mSlots->mTimer = nullptr; } } } // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsTreeBodyFrame::ScrollEvent::Run() { if (mInner) { mInner->FireScrollEvent(); } return NS_OK; } void nsTreeBodyFrame::FireScrollEvent() { mScrollEvent.Forget(); WidgetGUIEvent event(true, eScroll, nullptr); // scroll events fired at elements don't bubble event.mFlags.mBubbles = false; RefPtr content = GetContent(); RefPtr presContext = PresContext(); EventDispatcher::Dispatch(content, presContext, &event); } void nsTreeBodyFrame::PostScrollEvent() { if (mScrollEvent.IsPending()) return; RefPtr event = new ScrollEvent(this); nsresult rv = mContent->OwnerDoc()->Dispatch(do_AddRef(event)); if (NS_FAILED(rv)) { NS_WARNING("failed to dispatch ScrollEvent"); } else { mScrollEvent = std::move(event); } } void nsTreeBodyFrame::ScrollbarActivityStarted() const { if (mScrollbarActivity) { mScrollbarActivity->ActivityStarted(); } } void nsTreeBodyFrame::ScrollbarActivityStopped() const { if (mScrollbarActivity) { mScrollbarActivity->ActivityStopped(); } } void nsTreeBodyFrame::DetachImageListeners() { mCreatedListeners.Clear(); } void nsTreeBodyFrame::RemoveTreeImageListener(nsTreeImageListener* aListener) { if (aListener) { mCreatedListeners.Remove(aListener); } } #ifdef ACCESSIBILITY static void InitCustomEvent(CustomEvent* aEvent, const nsAString& aType, nsIWritablePropertyBag2* aDetail) { AutoJSAPI jsapi; if (!jsapi.Init(aEvent->GetParentObject())) { return; } JSContext* cx = jsapi.cx(); JS::Rooted detail(cx); if (!ToJSValue(cx, aDetail, &detail)) { jsapi.ClearException(); return; } aEvent->InitCustomEvent(cx, aType, /* aCanBubble = */ true, /* aCancelable = */ false, detail); } void nsTreeBodyFrame::FireRowCountChangedEvent(int32_t aIndex, int32_t aCount) { RefPtr tree(GetBaseElement()); if (!tree) return; RefPtr doc = tree->OwnerDoc(); MOZ_ASSERT(doc); RefPtr event = doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors()); CustomEvent* treeEvent = event->AsCustomEvent(); if (!treeEvent) { return; } nsCOMPtr propBag( do_CreateInstance("@mozilla.org/hash-property-bag;1")); if (!propBag) { return; } // Set 'index' data - the row index rows are changed from. propBag->SetPropertyAsInt32(u"index"_ns, aIndex); // Set 'count' data - the number of changed rows. propBag->SetPropertyAsInt32(u"count"_ns, aCount); InitCustomEvent(treeEvent, u"TreeRowCountChanged"_ns, propBag); event->SetTrusted(true); RefPtr asyncDispatcher = new AsyncEventDispatcher(tree, event.forget()); asyncDispatcher->PostDOMEvent(); } void nsTreeBodyFrame::FireInvalidateEvent(int32_t aStartRowIdx, int32_t aEndRowIdx, nsTreeColumn* aStartCol, nsTreeColumn* aEndCol) { RefPtr tree(GetBaseElement()); if (!tree) return; RefPtr doc = tree->OwnerDoc(); RefPtr event = doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors()); CustomEvent* treeEvent = event->AsCustomEvent(); if (!treeEvent) { return; } nsCOMPtr propBag( do_CreateInstance("@mozilla.org/hash-property-bag;1")); if (!propBag) { return; } if (aStartRowIdx != -1 && aEndRowIdx != -1) { // Set 'startrow' data - the start index of invalidated rows. propBag->SetPropertyAsInt32(u"startrow"_ns, aStartRowIdx); // Set 'endrow' data - the end index of invalidated rows. propBag->SetPropertyAsInt32(u"endrow"_ns, aEndRowIdx); } if (aStartCol && aEndCol) { // Set 'startcolumn' data - the start index of invalidated rows. int32_t startColIdx = aStartCol->GetIndex(); propBag->SetPropertyAsInt32(u"startcolumn"_ns, startColIdx); // Set 'endcolumn' data - the start index of invalidated rows. int32_t endColIdx = aEndCol->GetIndex(); propBag->SetPropertyAsInt32(u"endcolumn"_ns, endColIdx); } InitCustomEvent(treeEvent, u"TreeInvalidated"_ns, propBag); event->SetTrusted(true); RefPtr asyncDispatcher = new AsyncEventDispatcher(tree, event.forget()); asyncDispatcher->PostDOMEvent(); } #endif class nsOverflowChecker : public Runnable { public: explicit nsOverflowChecker(nsTreeBodyFrame* aFrame) : mozilla::Runnable("nsOverflowChecker"), mFrame(aFrame) {} NS_IMETHOD Run() override { if (mFrame.IsAlive()) { nsTreeBodyFrame* tree = static_cast(mFrame.GetFrame()); nsTreeBodyFrame::ScrollParts parts = tree->GetScrollParts(); tree->CheckOverflow(parts); } return NS_OK; } private: WeakFrame mFrame; }; bool nsTreeBodyFrame::FullScrollbarsUpdate(bool aNeedsFullInvalidation) { ScrollParts parts = GetScrollParts(); AutoWeakFrame weakFrame(this); AutoWeakFrame weakColumnsFrame(parts.mColumnsFrame); UpdateScrollbars(parts); NS_ENSURE_TRUE(weakFrame.IsAlive(), false); if (aNeedsFullInvalidation) { Invalidate(); } InvalidateScrollbars(parts, weakColumnsFrame); NS_ENSURE_TRUE(weakFrame.IsAlive(), false); // Overflow checking dispatches synchronous events, which can cause infinite // recursion during reflow. Do the first overflow check synchronously, but // force any nested checks to round-trip through the event loop. See bug // 905909. RefPtr checker = new nsOverflowChecker(this); if (!mCheckingOverflow) { nsContentUtils::AddScriptRunner(checker); } else { mContent->OwnerDoc()->Dispatch(checker.forget()); } return weakFrame.IsAlive(); } void nsTreeBodyFrame::OnImageIsAnimated(imgIRequest* aRequest) { nsLayoutUtils::RegisterImageRequest(PresContext(), aRequest, nullptr); }