/* -*- 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 "mozilla/AsyncEventDispatcher.h" #include "mozilla/dom/Element.h" #include "nsCOMPtr.h" #include "nsTreeSelection.h" #include "XULTreeElement.h" #include "nsITreeView.h" #include "nsString.h" #include "nsIContent.h" #include "nsNameSpaceManager.h" #include "nsGkAtoms.h" #include "nsComponentManagerUtils.h" #include "nsTreeColumns.h" using namespace mozilla; using dom::XULTreeElement; // A helper class for managing our ranges of selection. struct nsTreeRange { nsTreeSelection* mSelection; nsTreeRange* mPrev; nsTreeRange* mNext; int32_t mMin; int32_t mMax; nsTreeRange(nsTreeSelection* aSel, int32_t aSingleVal) : mSelection(aSel), mPrev(nullptr), mNext(nullptr), mMin(aSingleVal), mMax(aSingleVal) {} nsTreeRange(nsTreeSelection* aSel, int32_t aMin, int32_t aMax) : mSelection(aSel), mPrev(nullptr), mNext(nullptr), mMin(aMin), mMax(aMax) {} ~nsTreeRange() { delete mNext; } void Connect(nsTreeRange* aPrev = nullptr, nsTreeRange* aNext = nullptr) { if (aPrev) aPrev->mNext = this; else mSelection->mFirstRange = this; if (aNext) aNext->mPrev = this; mPrev = aPrev; mNext = aNext; } nsresult RemoveRange(int32_t aStart, int32_t aEnd) { // This should so be a loop... sigh... // We start past the range to remove, so no more to remove if (aEnd < mMin) return NS_OK; // We are the last range to be affected if (aEnd < mMax) { if (aStart <= mMin) { // Just chop the start of the range off mMin = aEnd + 1; } else { // We need to split the range nsTreeRange* range = new nsTreeRange(mSelection, aEnd + 1, mMax); if (!range) return NS_ERROR_OUT_OF_MEMORY; mMax = aStart - 1; range->Connect(this, mNext); } return NS_OK; } nsTreeRange* next = mNext; if (aStart <= mMin) { // The remove includes us, remove ourselves from the list if (mPrev) mPrev->mNext = next; else mSelection->mFirstRange = next; if (next) next->mPrev = mPrev; mPrev = mNext = nullptr; delete this; } else if (aStart <= mMax) { // Just chop the end of the range off mMax = aStart - 1; } return next ? next->RemoveRange(aStart, aEnd) : NS_OK; } nsresult Remove(int32_t aIndex) { if (aIndex >= mMin && aIndex <= mMax) { // We have found the range that contains us. if (mMin == mMax) { // Delete the whole range. if (mPrev) mPrev->mNext = mNext; if (mNext) mNext->mPrev = mPrev; nsTreeRange* first = mSelection->mFirstRange; if (first == this) mSelection->mFirstRange = mNext; mNext = mPrev = nullptr; delete this; } else if (aIndex == mMin) mMin++; else if (aIndex == mMax) mMax--; else { // We have to break this range. nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex + 1, mMax); if (!newRange) return NS_ERROR_OUT_OF_MEMORY; newRange->Connect(this, mNext); mMax = aIndex - 1; } } else if (mNext) return mNext->Remove(aIndex); return NS_OK; } nsresult Add(int32_t aIndex) { if (aIndex < mMin) { // We have found a spot to insert. if (aIndex + 1 == mMin) mMin = aIndex; else if (mPrev && mPrev->mMax + 1 == aIndex) mPrev->mMax = aIndex; else { // We have to create a new range. nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex); if (!newRange) return NS_ERROR_OUT_OF_MEMORY; newRange->Connect(mPrev, this); } } else if (mNext) mNext->Add(aIndex); else { // Insert on to the end. if (mMax + 1 == aIndex) mMax = aIndex; else { // We have to create a new range. nsTreeRange* newRange = new nsTreeRange(mSelection, aIndex); if (!newRange) return NS_ERROR_OUT_OF_MEMORY; newRange->Connect(this, nullptr); } } return NS_OK; } bool Contains(int32_t aIndex) { if (aIndex >= mMin && aIndex <= mMax) return true; if (mNext) return mNext->Contains(aIndex); return false; } int32_t Count() { int32_t total = mMax - mMin + 1; if (mNext) total += mNext->Count(); return total; } static void CollectRanges(nsTreeRange* aRange, nsTArray& aRanges) { nsTreeRange* cur = aRange; while (cur) { aRanges.AppendElement(cur->mMin); aRanges.AppendElement(cur->mMax); cur = cur->mNext; } } static void InvalidateRanges(XULTreeElement* aTree, nsTArray& aRanges) { if (aTree) { RefPtr tree = aTree; for (uint32_t i = 0; i < aRanges.Length(); i += 2) { aTree->InvalidateRange(aRanges[i], aRanges[i + 1]); } } } void Invalidate() { nsTArray ranges; CollectRanges(this, ranges); InvalidateRanges(mSelection->mTree, ranges); } void RemoveAllBut(int32_t aIndex) { if (aIndex >= mMin && aIndex <= mMax) { // Invalidate everything in this list. nsTArray ranges; CollectRanges(mSelection->mFirstRange, ranges); mMin = aIndex; mMax = aIndex; nsTreeRange* first = mSelection->mFirstRange; if (mPrev) mPrev->mNext = mNext; if (mNext) mNext->mPrev = mPrev; mNext = mPrev = nullptr; if (first != this) { delete mSelection->mFirstRange; mSelection->mFirstRange = this; } InvalidateRanges(mSelection->mTree, ranges); } else if (mNext) mNext->RemoveAllBut(aIndex); } void Insert(nsTreeRange* aRange) { if (mMin >= aRange->mMax) aRange->Connect(mPrev, this); else if (mNext) mNext->Insert(aRange); else aRange->Connect(this, nullptr); } }; nsTreeSelection::nsTreeSelection(XULTreeElement* aTree) : mTree(aTree), mSuppressed(false), mCurrentIndex(-1), mShiftSelectPivot(-1), mFirstRange(nullptr) {} nsTreeSelection::~nsTreeSelection() { delete mFirstRange; if (mSelectTimer) mSelectTimer->Cancel(); } NS_IMPL_CYCLE_COLLECTION(nsTreeSelection, mTree) NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTreeSelection) NS_IMPL_CYCLE_COLLECTING_RELEASE(nsTreeSelection) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsTreeSelection) NS_INTERFACE_MAP_ENTRY(nsITreeSelection) NS_INTERFACE_MAP_ENTRY(nsINativeTreeSelection) NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMETHODIMP nsTreeSelection::GetTree(XULTreeElement** aTree) { NS_IF_ADDREF(*aTree = mTree); return NS_OK; } NS_IMETHODIMP nsTreeSelection::SetTree(XULTreeElement* aTree) { if (mSelectTimer) { mSelectTimer->Cancel(); mSelectTimer = nullptr; } mTree = aTree; return NS_OK; } NS_IMETHODIMP nsTreeSelection::GetSingle(bool* aSingle) { if (!mTree) { return NS_ERROR_NULL_POINTER; } *aSingle = mTree->AttrValueIs(kNameSpaceID_None, nsGkAtoms::seltype, u"single"_ns, eCaseMatters); return NS_OK; } NS_IMETHODIMP nsTreeSelection::IsSelected(int32_t aIndex, bool* aResult) { if (mFirstRange) *aResult = mFirstRange->Contains(aIndex); else *aResult = false; return NS_OK; } NS_IMETHODIMP nsTreeSelection::TimedSelect(int32_t aIndex, int32_t aMsec) { bool suppressSelect = mSuppressed; if (aMsec != -1) mSuppressed = true; nsresult rv = Select(aIndex); if (NS_FAILED(rv)) return rv; if (aMsec != -1) { mSuppressed = suppressSelect; if (!mSuppressed) { if (mSelectTimer) mSelectTimer->Cancel(); if (!mTree) { return NS_ERROR_UNEXPECTED; } nsIEventTarget* target = mTree->OwnerDoc()->EventTargetFor(TaskCategory::Other); NS_NewTimerWithFuncCallback(getter_AddRefs(mSelectTimer), SelectCallback, this, aMsec, nsITimer::TYPE_ONE_SHOT, "nsTreeSelection::SelectCallback", target); } } return NS_OK; } NS_IMETHODIMP nsTreeSelection::Select(int32_t aIndex) { mShiftSelectPivot = -1; nsresult rv = SetCurrentIndex(aIndex); if (NS_FAILED(rv)) return rv; if (mFirstRange) { bool alreadySelected = mFirstRange->Contains(aIndex); if (alreadySelected) { int32_t count = mFirstRange->Count(); if (count > 1) { // We need to deselect everything but our item. mFirstRange->RemoveAllBut(aIndex); FireOnSelectHandler(); } return NS_OK; } else { // Clear out our selection. mFirstRange->Invalidate(); delete mFirstRange; } } // Create our new selection. mFirstRange = new nsTreeRange(this, aIndex); if (!mFirstRange) return NS_ERROR_OUT_OF_MEMORY; mFirstRange->Invalidate(); // Fire the select event FireOnSelectHandler(); return NS_OK; } NS_IMETHODIMP nsTreeSelection::ToggleSelect(int32_t aIndex) { // There are six cases that can occur on a ToggleSelect with our // range code. // (1) A new range should be made for a selection. // (2) A single range is removed from the selection. // (3) The item is added to an existing range. // (4) The item is removed from an existing range. // (5) The addition of the item causes two ranges to be merged. // (6) The removal of the item causes two ranges to be split. mShiftSelectPivot = -1; nsresult rv = SetCurrentIndex(aIndex); if (NS_FAILED(rv)) return rv; if (!mFirstRange) Select(aIndex); else { if (!mFirstRange->Contains(aIndex)) { bool single; rv = GetSingle(&single); if (NS_SUCCEEDED(rv) && !single) rv = mFirstRange->Add(aIndex); } else rv = mFirstRange->Remove(aIndex); if (NS_SUCCEEDED(rv)) { if (mTree) mTree->InvalidateRow(aIndex); FireOnSelectHandler(); } } return rv; } NS_IMETHODIMP nsTreeSelection::RangedSelect(int32_t aStartIndex, int32_t aEndIndex, bool aAugment) { bool single; nsresult rv = GetSingle(&single); if (NS_FAILED(rv)) return rv; if ((mFirstRange || (aStartIndex != aEndIndex)) && single) return NS_OK; if (!aAugment) { // Clear our selection. if (mFirstRange) { mFirstRange->Invalidate(); delete mFirstRange; mFirstRange = nullptr; } } if (aStartIndex == -1) { if (mShiftSelectPivot != -1) aStartIndex = mShiftSelectPivot; else if (mCurrentIndex != -1) aStartIndex = mCurrentIndex; else aStartIndex = aEndIndex; } mShiftSelectPivot = aStartIndex; rv = SetCurrentIndex(aEndIndex); if (NS_FAILED(rv)) return rv; int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex; int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex; if (aAugment && mFirstRange) { // We need to remove all the items within our selected range from the // selection, and then we insert our new range into the list. nsresult rv = mFirstRange->RemoveRange(start, end); if (NS_FAILED(rv)) return rv; } nsTreeRange* range = new nsTreeRange(this, start, end); if (!range) return NS_ERROR_OUT_OF_MEMORY; range->Invalidate(); if (aAugment && mFirstRange) mFirstRange->Insert(range); else mFirstRange = range; FireOnSelectHandler(); return NS_OK; } NS_IMETHODIMP nsTreeSelection::ClearRange(int32_t aStartIndex, int32_t aEndIndex) { nsresult rv = SetCurrentIndex(aEndIndex); if (NS_FAILED(rv)) return rv; if (mFirstRange) { int32_t start = aStartIndex < aEndIndex ? aStartIndex : aEndIndex; int32_t end = aStartIndex < aEndIndex ? aEndIndex : aStartIndex; mFirstRange->RemoveRange(start, end); if (mTree) mTree->InvalidateRange(start, end); } return NS_OK; } NS_IMETHODIMP nsTreeSelection::ClearSelection() { if (mFirstRange) { mFirstRange->Invalidate(); delete mFirstRange; mFirstRange = nullptr; } mShiftSelectPivot = -1; FireOnSelectHandler(); return NS_OK; } NS_IMETHODIMP nsTreeSelection::SelectAll() { if (!mTree) return NS_OK; nsCOMPtr view = mTree->GetView(); if (!view) return NS_OK; int32_t rowCount; view->GetRowCount(&rowCount); bool single; nsresult rv = GetSingle(&single); if (NS_FAILED(rv)) return rv; if (rowCount == 0 || (rowCount > 1 && single)) return NS_OK; mShiftSelectPivot = -1; // Invalidate not necessary when clearing selection, since // we're going to invalidate the world on the SelectAll. delete mFirstRange; mFirstRange = new nsTreeRange(this, 0, rowCount - 1); mFirstRange->Invalidate(); FireOnSelectHandler(); return NS_OK; } NS_IMETHODIMP nsTreeSelection::GetRangeCount(int32_t* aResult) { int32_t count = 0; nsTreeRange* curr = mFirstRange; while (curr) { count++; curr = curr->mNext; } *aResult = count; return NS_OK; } NS_IMETHODIMP nsTreeSelection::GetRangeAt(int32_t aIndex, int32_t* aMin, int32_t* aMax) { *aMin = *aMax = -1; int32_t i = -1; nsTreeRange* curr = mFirstRange; while (curr) { i++; if (i == aIndex) { *aMin = curr->mMin; *aMax = curr->mMax; break; } curr = curr->mNext; } return NS_OK; } NS_IMETHODIMP nsTreeSelection::GetCount(int32_t* count) { if (mFirstRange) *count = mFirstRange->Count(); else // No range available, so there's no selected row. *count = 0; return NS_OK; } NS_IMETHODIMP nsTreeSelection::GetSelectEventsSuppressed( bool* aSelectEventsSuppressed) { *aSelectEventsSuppressed = mSuppressed; return NS_OK; } NS_IMETHODIMP nsTreeSelection::SetSelectEventsSuppressed( bool aSelectEventsSuppressed) { mSuppressed = aSelectEventsSuppressed; if (!mSuppressed) FireOnSelectHandler(); return NS_OK; } NS_IMETHODIMP nsTreeSelection::GetCurrentIndex(int32_t* aCurrentIndex) { *aCurrentIndex = mCurrentIndex; return NS_OK; } NS_IMETHODIMP nsTreeSelection::SetCurrentIndex(int32_t aIndex) { if (!mTree) { return NS_ERROR_UNEXPECTED; } if (mCurrentIndex == aIndex) { return NS_OK; } if (mCurrentIndex != -1 && mTree) mTree->InvalidateRow(mCurrentIndex); mCurrentIndex = aIndex; if (!mTree) return NS_OK; if (aIndex != -1) mTree->InvalidateRow(aIndex); // Fire DOMMenuItemActive or DOMMenuItemInactive event for tree. NS_ENSURE_STATE(mTree); constexpr auto DOMMenuItemActive = u"DOMMenuItemActive"_ns; constexpr auto DOMMenuItemInactive = u"DOMMenuItemInactive"_ns; RefPtr asyncDispatcher = new AsyncEventDispatcher( mTree, (aIndex != -1 ? DOMMenuItemActive : DOMMenuItemInactive), CanBubble::eYes, ChromeOnlyDispatch::eNo); return asyncDispatcher->PostDOMEvent(); } #define ADD_NEW_RANGE(macro_range, macro_selection, macro_start, macro_end) \ { \ int32_t start = macro_start; \ int32_t end = macro_end; \ if (start > end) { \ end = start; \ } \ nsTreeRange* macro_new_range = \ new nsTreeRange(macro_selection, start, end); \ if (macro_range) \ macro_range->Insert(macro_new_range); \ else \ macro_range = macro_new_range; \ } NS_IMETHODIMP nsTreeSelection::AdjustSelection(int32_t aIndex, int32_t aCount) { NS_ASSERTION(aCount != 0, "adjusting by zero"); if (!aCount) return NS_OK; // adjust mShiftSelectPivot, if necessary if ((mShiftSelectPivot != 1) && (aIndex <= mShiftSelectPivot)) { // if we are deleting and the delete includes the shift select pivot, reset // it if (aCount < 0 && (mShiftSelectPivot <= (aIndex - aCount - 1))) { mShiftSelectPivot = -1; } else { mShiftSelectPivot += aCount; } } // adjust mCurrentIndex, if necessary if ((mCurrentIndex != -1) && (aIndex <= mCurrentIndex)) { // if we are deleting and the delete includes the current index, reset it if (aCount < 0 && (mCurrentIndex <= (aIndex - aCount - 1))) { mCurrentIndex = -1; } else { mCurrentIndex += aCount; } } // no selection, so nothing to do. if (!mFirstRange) return NS_OK; bool selChanged = false; nsTreeRange* oldFirstRange = mFirstRange; nsTreeRange* curr = mFirstRange; mFirstRange = nullptr; while (curr) { if (aCount > 0) { // inserting if (aIndex > curr->mMax) { // adjustment happens after the range, so no change ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax); } else if (aIndex <= curr->mMin) { // adjustment happens before the start of the range, so shift down ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount, curr->mMax + aCount); selChanged = true; } else { // adjustment happen inside the range. // break apart the range and create two ranges ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1); ADD_NEW_RANGE(mFirstRange, this, aIndex + aCount, curr->mMax + aCount); selChanged = true; } } else { // deleting if (aIndex > curr->mMax) { // adjustment happens after the range, so no change ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax); } else { // remember, aCount is negative selChanged = true; int32_t lastIndexOfAdjustment = aIndex - aCount - 1; if (aIndex <= curr->mMin) { if (lastIndexOfAdjustment < curr->mMin) { // adjustment happens before the start of the range, so shift up ADD_NEW_RANGE(mFirstRange, this, curr->mMin + aCount, curr->mMax + aCount); } else if (lastIndexOfAdjustment >= curr->mMax) { // adjustment contains the range. remove the range by not adding it // to the newRange } else { // adjustment starts before the range, and ends in the middle of it, // so trim the range ADD_NEW_RANGE(mFirstRange, this, aIndex, curr->mMax + aCount) } } else if (lastIndexOfAdjustment >= curr->mMax) { // adjustment starts in the middle of the current range, and contains // the end of the range, so trim the range ADD_NEW_RANGE(mFirstRange, this, curr->mMin, aIndex - 1) } else { // range contains the adjustment, so shorten the range ADD_NEW_RANGE(mFirstRange, this, curr->mMin, curr->mMax + aCount) } } } curr = curr->mNext; } delete oldFirstRange; // Fire the select event if (selChanged) FireOnSelectHandler(); return NS_OK; } NS_IMETHODIMP nsTreeSelection::InvalidateSelection() { if (mFirstRange) mFirstRange->Invalidate(); return NS_OK; } NS_IMETHODIMP nsTreeSelection::GetShiftSelectPivot(int32_t* aIndex) { *aIndex = mShiftSelectPivot; return NS_OK; } nsresult nsTreeSelection::FireOnSelectHandler() { if (mSuppressed || !mTree) { return NS_OK; } AsyncEventDispatcher::RunDOMEventWhenSafe( *mTree, u"select"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo); return NS_OK; } void nsTreeSelection::SelectCallback(nsITimer* aTimer, void* aClosure) { RefPtr self = static_cast(aClosure); if (self) { self->FireOnSelectHandler(); aTimer->Cancel(); self->mSelectTimer = nullptr; } } /////////////////////////////////////////////////////////////////////////////////// nsresult NS_NewTreeSelection(XULTreeElement* aTree, nsITreeSelection** aResult) { *aResult = new nsTreeSelection(aTree); if (!*aResult) return NS_ERROR_OUT_OF_MEMORY; NS_ADDREF(*aResult); return NS_OK; }