diff options
Diffstat (limited to 'layout/xul/tree/nsTreeSelection.cpp')
-rw-r--r-- | layout/xul/tree/nsTreeSelection.cpp | 723 |
1 files changed, 723 insertions, 0 deletions
diff --git a/layout/xul/tree/nsTreeSelection.cpp b/layout/xul/tree/nsTreeSelection.cpp new file mode 100644 index 0000000000..7915b3feb2 --- /dev/null +++ b/layout/xul/tree/nsTreeSelection.cpp @@ -0,0 +1,723 @@ +/* -*- 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<int32_t>& aRanges) { + nsTreeRange* cur = aRange; + while (cur) { + aRanges.AppendElement(cur->mMin); + aRanges.AppendElement(cur->mMax); + cur = cur->mNext; + } + } + + static void InvalidateRanges(XULTreeElement* aTree, + nsTArray<int32_t>& aRanges) { + if (aTree) { + RefPtr<nsXULElement> tree = aTree; + for (uint32_t i = 0; i < aRanges.Length(); i += 2) { + aTree->InvalidateRange(aRanges[i], aRanges[i + 1]); + } + } + } + + void Invalidate() { + nsTArray<int32_t> ranges; + CollectRanges(this, ranges); + InvalidateRanges(mSelection->mTree, ranges); + } + + void RemoveAllBut(int32_t aIndex) { + if (aIndex >= mMin && aIndex <= mMax) { + // Invalidate everything in this list. + nsTArray<int32_t> 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<nsITreeView> 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<AsyncEventDispatcher> 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; + + RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher( + mTree, u"select"_ns, CanBubble::eYes, ChromeOnlyDispatch::eNo); + asyncDispatcher->RunDOMEventWhenSafe(); + return NS_OK; +} + +void nsTreeSelection::SelectCallback(nsITimer* aTimer, void* aClosure) { + RefPtr<nsTreeSelection> self = static_cast<nsTreeSelection*>(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; +} |