diff options
Diffstat (limited to '')
-rw-r--r-- | layout/xul/tree/nsTreeBodyFrame.cpp | 4363 |
1 files changed, 4363 insertions, 0 deletions
diff --git a/layout/xul/tree/nsTreeBodyFrame.cpp b/layout/xul/tree/nsTreeBodyFrame.cpp new file mode 100644 index 0000000000..c7e8272aa0 --- /dev/null +++ b/layout/xul/tree/nsTreeBodyFrame.cpp @@ -0,0 +1,4363 @@ +/* -*- 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/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 <algorithm> +#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<uint32_t> 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<uint32_t> 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<uint32_t> 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), + mImageCache(), + 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<XULTreeElement> 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<nsIScrollbarMediator*>(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<nsITreeView> view = std::move(mView)) { + nsCOMPtr<nsITreeSelection> 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<XULTreeElement> tree = GetBaseElement(); + if (!tree) { + return; + } + nsCOMPtr<nsITreeView> 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<nsITreeSelection> 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<nsITreeView> oldView = std::move(mView); + if (oldView) { + AutoWeakFrame weakFrame(this); + + nsCOMPtr<nsITreeSelection> 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<XULTreeElement> 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<nsITreeSelection> 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<nsITreeSelection> nsTreeBodyFrame::GetSelection() const { + nsCOMPtr<nsITreeSelection> sel; + if (nsCOMPtr<nsITreeView> view = GetExistingView()) { + view->GetSelection(getter_AddRefs(sel)); + } + return sel.forget(); +} + +nsresult nsTreeBodyFrame::SetFocused(bool aFocused) { + if (mFocused != aFocused) { + mFocused = aFocused; + if (nsCOMPtr<nsITreeSelection> 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> 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<CSSIntRegion> nsTreeBodyFrame::GetSelectionRegion() { + if (!mView) { + return Nothing(); + } + + AutoWeakFrame wf(this); + nsCOMPtr<nsITreeSelection> selection = GetSelection(); + if (!selection || !wf.IsAlive()) { + return Nothing(); + } + + RefPtr<nsPresContext> presContext = PresContext(); + nsIntRect rect = mRect.ToOutsidePixels(AppUnitsPerCSSPixel()); + + nsIFrame* rootFrame = presContext->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<nsPresContext> presContext = PresContext(); + RefPtr<mozilla::PresShell> presShell = presContext->GetPresShell(); + nsCOMPtr<nsIContent> 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<nsITreeView> 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<gfxContext> 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<nsFontMetrics> 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<nsITreeView> view = GetExistingView(); + if (aColumn->Overflow()) { + DebugOnly<nsresult> 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<nsITreeView> 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<gfxContext> 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<nsFontMetrics> 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<nsITreeView> 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<nsFontMetrics> 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<gfxContext> 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<nsITimer> 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, + mContent->OwnerDoc()->EventTargetFor(TaskCategory::Other))); + } + + 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<nsITreeSelection> 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<nsITreeView> 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<nsITreeSelection> 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(kNameSpaceID_None, 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<imgRequestProxy> styleRequest; + if (aUseContext || imageSrc.IsEmpty()) { + // Obtain the URL from the ComputedStyle. + styleRequest = + aComputedStyle->StyleList()->mListStyleImage.GetImageRequest(); + if (!styleRequest) return NS_OK; + nsCOMPtr<nsIURI> 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<imgINotificationObserver> obs; + imgReq->GetNotificationObserver(getter_AddRefs(obs)); + + if (obs) { + static_cast<nsTreeImageListener*>(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<imgINotificationObserver> imgNotificationObserver = listener; + + Document* doc = mContent->GetComposedDoc(); + if (!doc) + // The page is currently being torn down. Why bother. + return NS_ERROR_FAILURE; + + RefPtr<imgRequestProxy> imageRequest; + if (styleRequest) { + styleRequest->SyncClone(imgNotificationObserver, doc, + getter_AddRefs(imageRequest)); + } else { + nsCOMPtr<nsIURI> srcURI; + nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(srcURI), imageSrc, doc, mContent->GetBaseURI()); + if (!srcURI) return NS_ERROR_FAILURE; + + auto referrerInfo = MakeRefPtr<mozilla::dom::ReferrerInfo>(*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<imgIContainer> 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; +} + +Maybe<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<ComputedStyle> childContext = GetPseudoComputedStyle(child); + StyleCursorKind kind = childContext->StyleUI()->Cursor().keyword; + if (kind == StyleCursorKind::Auto) { + kind = StyleCursorKind::Default; + } + return Some( + 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<Slots>(); + } + + // 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 { + DocumentState docState = + mFrame->PresContext()->Document()->GetDocumentState(); + return !docState.HasState(DocumentState::WINDOW_INACTIVE); + } + + void ComputeInvalidationRegion(nsDisplayListBuilder* aBuilder, + const nsDisplayItemGeometry* aGeometry, + nsRegion* aInvalidRegion) const override { + auto geometry = static_cast<const nsDisplayTreeBodyGeometry*>(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<nsTreeBodyFrame*>(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 + +#ifdef XP_MACOSX +static bool IsInSourceList(nsIFrame* aFrame) { + for (nsIFrame* frame = aFrame; frame; + frame = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame)) { + if (frame->StyleDisplay()->EffectiveAppearance() == + StyleAppearance::MozMacSourceList) { + return true; + } + } + return false; +} +#endif + +// 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<nsDisplayTreeBody>(aBuilder, this); + aLists.Content()->AppendToTop(item); + +#ifdef XP_MACOSX + XULTreeElement* tree = GetBaseElement(); + nsIFrame* treeFrame = tree ? tree->GetPrimaryFrame() : nullptr; + nsCOMPtr<nsITreeView> view = GetExistingView(); + nsCOMPtr<nsITreeSelection> selection = GetSelection(); + nsITheme* theme = PresContext()->Theme(); + // On Mac, we support native theming of selected rows. On 10.10 and higher, + // this means applying vibrancy which require us to register the theme + // geometrics for the row. In order to make the vibrancy effect to work + // properly, we also need an ancestor frame to be themed as a source list. + if (selection && theme && IsInSourceList(treeFrame)) { + // Loop through our onscreen rows. If the row is selected and a + // -moz-appearance is provided, RegisterThemeGeometry might be necessary. + const auto end = std::min(mRowCount, LastVisibleRow() + 1); + for (auto i = FirstVisibleRow(); i < end; i++) { + bool isSelected; + selection->IsSelected(i, &isSelected); + if (isSelected) { + PrefillPropertyArray(i, nullptr); + nsAutoString properties; + view->GetRowProperties(i, properties); + nsTreeUtils::TokenizeProperties(properties, mScratchArray); + ComputedStyle* rowContext = + GetPseudoComputedStyle(nsCSSAnonBoxes::mozTreeRow()); + auto appearance = rowContext->StyleDisplay()->EffectiveAppearance(); + if (appearance != StyleAppearance::None) { + if (theme->ThemeSupportsWidget(PresContext(), this, appearance)) { + nsITheme::ThemeGeometryType type = + theme->ThemeGeometryTypeForWidget(this, appearance); + if (type != nsITheme::eThemeGeometryTypeUnknown) { + nsRect rowRect(mInnerBox.x, + mInnerBox.y + mRowHeight * (i - FirstVisibleRow()), + mInnerBox.width, mRowHeight); + aBuilder->RegisterThemeGeometry( + type, item, + LayoutDeviceIntRect::FromUnknownRect( + (rowRect + aBuilder->ToReferenceFrame(this)) + .ToNearestPixels( + PresContext()->AppUnitsPerDevPixel()))); + } + } + } + } + } + } +#endif +} + +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<nsITreeView> 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<nsITreeView> 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<nsITreeView> 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<nsITreeView> 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<imgIContainer> 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<imgIContainer> 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<nsITreeView> 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<nsFontMetrics> 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<imgIContainer> 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<nsITreeView> 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<nsITreeView> view = GetExistingView(); + if (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<nsITreeView> 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<nsTreeBodyFrame*>(aClosure); + if (!self) { + return; + } + + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + + nsCOMPtr<nsITreeView> 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<nsTreeBodyFrame*>(aClosure); + if (!self) { + return; + } + + aTimer->Cancel(); + self->mSlots->mTimer = nullptr; + + nsCOMPtr<nsITreeView> 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<nsTreeBodyFrame*>(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<nsTreeBodyFrame*>(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<nsIContent> content = GetContent(); + RefPtr<nsPresContext> presContext = PresContext(); + EventDispatcher::Dispatch(content, presContext, &event); +} + +void nsTreeBodyFrame::PostScrollEvent() { + if (mScrollEvent.IsPending()) return; + + RefPtr<ScrollEvent> event = new ScrollEvent(this); + nsresult rv = + mContent->OwnerDoc()->Dispatch(TaskCategory::Other, 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<JS::Value> 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<XULTreeElement> tree(GetBaseElement()); + if (!tree) return; + + RefPtr<Document> doc = tree->OwnerDoc(); + MOZ_ASSERT(doc); + + RefPtr<Event> event = + doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors()); + + CustomEvent* treeEvent = event->AsCustomEvent(); + if (!treeEvent) { + return; + } + + nsCOMPtr<nsIWritablePropertyBag2> 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<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(tree, event); + asyncDispatcher->PostDOMEvent(); +} + +void nsTreeBodyFrame::FireInvalidateEvent(int32_t aStartRowIdx, + int32_t aEndRowIdx, + nsTreeColumn* aStartCol, + nsTreeColumn* aEndCol) { + RefPtr<XULTreeElement> tree(GetBaseElement()); + if (!tree) return; + + RefPtr<Document> doc = tree->OwnerDoc(); + + RefPtr<Event> event = + doc->CreateEvent(u"customevent"_ns, CallerType::System, IgnoreErrors()); + + CustomEvent* treeEvent = event->AsCustomEvent(); + if (!treeEvent) { + return; + } + + nsCOMPtr<nsIWritablePropertyBag2> 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<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(tree, event); + 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<nsTreeBodyFrame*>(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<nsOverflowChecker> checker = new nsOverflowChecker(this); + if (!mCheckingOverflow) { + nsContentUtils::AddScriptRunner(checker); + } else { + mContent->OwnerDoc()->Dispatch(TaskCategory::Other, checker.forget()); + } + return weakFrame.IsAlive(); +} + +void nsTreeBodyFrame::OnImageIsAnimated(imgIRequest* aRequest) { + nsLayoutUtils::RegisterImageRequest(PresContext(), aRequest, nullptr); +} |