3267 lines
116 KiB
C++
3267 lines
116 KiB
C++
/* -*- 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/. */
|
|
|
|
/*
|
|
* Implementation of nsFrameSelection
|
|
*/
|
|
|
|
#include "nsFrameSelection.h"
|
|
|
|
#include "ErrorList.h"
|
|
#include "mozilla/intl/BidiEmbeddingLevel.h"
|
|
#include "mozilla/Attributes.h"
|
|
#include "mozilla/AutoRestore.h"
|
|
#include "mozilla/BasePrincipal.h"
|
|
#include "mozilla/HTMLEditor.h"
|
|
#include "mozilla/IntegerRange.h"
|
|
#include "mozilla/Logging.h"
|
|
#include "mozilla/PresShell.h"
|
|
#include "mozilla/PseudoStyleType.h"
|
|
#include "mozilla/ScrollContainerFrame.h"
|
|
#include "mozilla/ScrollTypes.h"
|
|
#include "mozilla/StaticAnalysisFunctions.h"
|
|
#include "mozilla/StaticPrefs_bidi.h"
|
|
#include "mozilla/StaticPrefs_dom.h"
|
|
#include "mozilla/StaticPrefs_layout.h"
|
|
#include "mozilla/Unused.h"
|
|
|
|
#include "nsCOMPtr.h"
|
|
#include "nsDebug.h"
|
|
#include "nsFrameTraversal.h"
|
|
#include "nsString.h"
|
|
#include "nsISelectionListener.h"
|
|
#include "nsDeviceContext.h"
|
|
#include "nsIContent.h"
|
|
#include "nsRange.h"
|
|
#include "nsITableCellLayout.h"
|
|
#include "nsTArray.h"
|
|
#include "nsTableWrapperFrame.h"
|
|
#include "nsTableCellFrame.h"
|
|
#include "nsCCUncollectableMarker.h"
|
|
#include "nsTextFragment.h"
|
|
#include <algorithm>
|
|
#include "nsContentUtils.h"
|
|
#include "nsCSSFrameConstructor.h"
|
|
|
|
#include "nsGkAtoms.h"
|
|
#include "nsLayoutUtils.h"
|
|
#include "nsBidiPresUtils.h"
|
|
#include "nsTextFrame.h"
|
|
|
|
#include "nsThreadUtils.h"
|
|
#include "mozilla/Preferences.h"
|
|
|
|
#include "mozilla/PresShell.h"
|
|
#include "nsPresContext.h"
|
|
#include "nsCaret.h"
|
|
|
|
#include "mozilla/MouseEvents.h"
|
|
#include "mozilla/TextEvents.h"
|
|
|
|
// notifications
|
|
#include "mozilla/dom/Document.h"
|
|
|
|
#include "nsISelectionController.h" //for the enums
|
|
#include "nsCopySupport.h"
|
|
#include "nsIClipboard.h"
|
|
#include "nsIFrameInlines.h"
|
|
|
|
#include "nsError.h"
|
|
#include "mozilla/AutoCopyListener.h"
|
|
#include "mozilla/dom/AncestorIterator.h"
|
|
#include "mozilla/dom/Element.h"
|
|
#include "mozilla/dom/Highlight.h"
|
|
#include "mozilla/dom/Selection.h"
|
|
#include "mozilla/dom/ShadowRoot.h"
|
|
#include "mozilla/dom/StaticRange.h"
|
|
#include "mozilla/dom/Text.h"
|
|
#include "mozilla/ErrorResult.h"
|
|
#include "mozilla/dom/SelectionBinding.h"
|
|
#include "mozilla/AsyncEventDispatcher.h"
|
|
|
|
#include "nsFocusManager.h"
|
|
#include "nsPIDOMWindow.h"
|
|
|
|
#include "SelectionMovementUtils.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::dom;
|
|
|
|
static LazyLogModule sFrameSelectionLog("FrameSelection");
|
|
|
|
std::ostream& operator<<(std::ostream& aStream,
|
|
const nsFrameSelection& aFrameSelection) {
|
|
return aStream << "{ mPresShell=" << aFrameSelection.mPresShell
|
|
<< ", mLimiters={ mIndependentSelectionRootElement="
|
|
<< aFrameSelection.mLimiters.mIndependentSelectionRootElement
|
|
<< ", mAncestorLimiter="
|
|
<< aFrameSelection.mLimiters.mAncestorLimiter
|
|
<< "}, IsBatching()=" << std::boolalpha
|
|
<< aFrameSelection.IsBatching()
|
|
<< ", IsInTableSelectionMode()=" << std::boolalpha
|
|
<< aFrameSelection.IsInTableSelectionMode()
|
|
<< ", GetDragState()=" << std::boolalpha
|
|
<< aFrameSelection.GetDragState()
|
|
<< ", HighlightSelectionCount()="
|
|
<< aFrameSelection.HighlightSelectionCount() << " }";
|
|
}
|
|
|
|
namespace mozilla {
|
|
extern LazyLogModule sSelectionAPILog;
|
|
extern void LogStackForSelectionAPI();
|
|
|
|
static void LogSelectionAPI(const dom::Selection* aSelection,
|
|
const char* aFuncName, const char* aArgName,
|
|
const nsIContent* aContent) {
|
|
MOZ_LOG(sSelectionAPILog, LogLevel::Info,
|
|
("%p nsFrameSelection::%s(%s=%s)", aSelection, aFuncName, aArgName,
|
|
aContent ? ToString(*aContent).c_str() : "<nullptr>"));
|
|
}
|
|
} // namespace mozilla
|
|
|
|
// #define DEBUG_TABLE 1
|
|
|
|
/**
|
|
* Add cells to the selection inside of the given cells range.
|
|
*
|
|
* @param aTable [in] HTML table element
|
|
* @param aStartRowIndex [in] row index where the cells range starts
|
|
* @param aStartColumnIndex [in] column index where the cells range starts
|
|
* @param aEndRowIndex [in] row index where the cells range ends
|
|
* @param aEndColumnIndex [in] column index where the cells range ends
|
|
*/
|
|
static nsresult AddCellsToSelection(const nsIContent* aTableContent,
|
|
int32_t aStartRowIndex,
|
|
int32_t aStartColumnIndex,
|
|
int32_t aEndRowIndex,
|
|
int32_t aEndColumnIndex,
|
|
Selection& aNormalSelection);
|
|
|
|
static nsAtom* GetTag(nsINode* aNode);
|
|
|
|
static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode);
|
|
MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult CreateAndAddRange(
|
|
nsINode* aContainer, int32_t aOffset, Selection& aNormalSelection);
|
|
static nsresult SelectCellElement(nsIContent* aCellElement,
|
|
Selection& aNormalSelection);
|
|
|
|
#ifdef XP_MACOSX
|
|
static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel);
|
|
#endif // XP_MACOSX
|
|
|
|
#ifdef PRINT_RANGE
|
|
static void printRange(nsRange* aDomRange);
|
|
# define DEBUG_OUT_RANGE(x) printRange(x)
|
|
#else
|
|
# define DEBUG_OUT_RANGE(x)
|
|
#endif // PRINT_RANGE
|
|
|
|
/******************************************************************************
|
|
* mozilla::PeekOffsetStruct
|
|
******************************************************************************/
|
|
|
|
// #define DEBUG_SELECTION // uncomment for printf describing every collapse and
|
|
// extend. #define DEBUG_NAVIGATION
|
|
|
|
// #define DEBUG_TABLE_SELECTION 1
|
|
|
|
namespace mozilla {
|
|
|
|
PeekOffsetStruct::PeekOffsetStruct(
|
|
nsSelectionAmount aAmount, nsDirection aDirection, int32_t aStartOffset,
|
|
nsPoint aDesiredCaretPos, const PeekOffsetOptions aOptions,
|
|
EWordMovementType aWordMovementType /* = eDefaultBehavior */,
|
|
const Element* aAncestorLimiter /* = nullptr */)
|
|
: mAmount(aAmount),
|
|
mDirection(aDirection),
|
|
mStartOffset(aStartOffset),
|
|
mDesiredCaretPos(aDesiredCaretPos),
|
|
mWordMovementType(aWordMovementType),
|
|
mOptions(aOptions),
|
|
mAncestorLimiter(aAncestorLimiter),
|
|
mResultFrame(nullptr),
|
|
mContentOffset(0),
|
|
mAttach(CaretAssociationHint::Before) {}
|
|
|
|
} // namespace mozilla
|
|
|
|
// Array which contains index of each SelectionType in
|
|
// Selection::mDOMSelections. For avoiding using if nor switch to retrieve the
|
|
// index, this needs to have -1 for SelectionTypes which won't be created its
|
|
// Selection instance.
|
|
static const int8_t kIndexOfSelections[] = {
|
|
-1, // SelectionType::eInvalid
|
|
-1, // SelectionType::eNone
|
|
0, // SelectionType::eNormal
|
|
1, // SelectionType::eSpellCheck
|
|
2, // SelectionType::eIMERawClause
|
|
3, // SelectionType::eIMESelectedRawClause
|
|
4, // SelectionType::eIMEConvertedClause
|
|
5, // SelectionType::eIMESelectedClause
|
|
6, // SelectionType::eAccessibility
|
|
7, // SelectionType::eFind
|
|
8, // SelectionType::eURLSecondary
|
|
9, // SelectionType::eURLStrikeout
|
|
10, // SelectionType::eTargetText
|
|
-1, // SelectionType::eHighlight
|
|
};
|
|
|
|
inline int8_t GetIndexFromSelectionType(SelectionType aSelectionType) {
|
|
// The enum value of eInvalid is -1 and the others are sequential value
|
|
// starting from 0. Therefore, |SelectionType + 1| is the index of
|
|
// kIndexOfSelections.
|
|
return kIndexOfSelections[static_cast<int8_t>(aSelectionType) + 1];
|
|
}
|
|
|
|
/*
|
|
The limiter is used specifically for the text areas and textfields
|
|
In that case it is the DIV tag that is anonymously created for the text
|
|
areas/fields. Text nodes and BR nodes fall beneath it. In the case of a
|
|
BR node the limiter will be the parent and the offset will point before or
|
|
after the BR node. In the case of the text node the parent content is
|
|
the text node itself and the offset will be the exact character position.
|
|
The offset is not important to check for validity. Simply look at the
|
|
passed in content. If it equals the limiter then the selection point is valid.
|
|
If its parent it the limiter then the point is also valid. In the case of
|
|
NO limiter all points are valid since you are in a topmost iframe. (browser
|
|
or composer)
|
|
*/
|
|
bool nsFrameSelection::NodeIsInLimiters(const nsINode* aContainerNode) const {
|
|
return NodeIsInLimiters(aContainerNode, GetIndependentSelectionRootElement(),
|
|
GetAncestorLimiter());
|
|
}
|
|
|
|
// static
|
|
bool nsFrameSelection::NodeIsInLimiters(
|
|
const nsINode* aContainerNode,
|
|
const Element* aIndependentSelectionLimiterElement,
|
|
const Element* aSelectionAncestorLimiter) {
|
|
if (!aContainerNode) {
|
|
return false;
|
|
}
|
|
|
|
// If there is a selection limiter, it must be the anonymous <div> of a text
|
|
// control. The <div> should have only one Text and/or a <br>. Therefore,
|
|
// when it's non-nullptr, selection range containers must be the container or
|
|
// the Text in it.
|
|
if (aIndependentSelectionLimiterElement) {
|
|
MOZ_ASSERT(aIndependentSelectionLimiterElement->GetPseudoElementType() ==
|
|
PseudoStyleType::mozTextControlEditingRoot);
|
|
MOZ_ASSERT(
|
|
aIndependentSelectionLimiterElement->IsHTMLElement(nsGkAtoms::div));
|
|
if (aIndependentSelectionLimiterElement == aContainerNode) {
|
|
return true;
|
|
}
|
|
if (aIndependentSelectionLimiterElement == aContainerNode->GetParent()) {
|
|
NS_WARNING_ASSERTION(aContainerNode->IsText(),
|
|
ToString(*aContainerNode).c_str());
|
|
MOZ_ASSERT(aContainerNode->IsText());
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// XXX We might need to return `false` if aContainerNode is in a native
|
|
// anonymous subtree, but doing it will make it impossible to select the
|
|
// anonymous subtree text in <details>.
|
|
return !aSelectionAncestorLimiter ||
|
|
aContainerNode->IsInclusiveDescendantOf(aSelectionAncestorLimiter);
|
|
}
|
|
|
|
namespace mozilla {
|
|
struct MOZ_RAII AutoPrepareFocusRange {
|
|
AutoPrepareFocusRange(Selection* aSelection,
|
|
const bool aMultiRangeSelection) {
|
|
MOZ_ASSERT(aSelection);
|
|
MOZ_ASSERT(aSelection->GetType() == SelectionType::eNormal);
|
|
|
|
if (aSelection->mStyledRanges.mRanges.Length() <= 1) {
|
|
return;
|
|
}
|
|
|
|
if (aSelection->mFrameSelection->IsUserSelectionReason()) {
|
|
mUserSelect.emplace(aSelection);
|
|
}
|
|
|
|
nsTArray<StyledRange>& ranges = aSelection->mStyledRanges.mRanges;
|
|
if (!aSelection->mUserInitiated || aMultiRangeSelection) {
|
|
// Scripted command or the user is starting a new explicit multi-range
|
|
// selection.
|
|
for (StyledRange& entry : ranges) {
|
|
MOZ_ASSERT(entry.mRange->IsDynamicRange());
|
|
entry.mRange->AsDynamicRange()->SetIsGenerated(false);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!IsAnchorRelativeOperation(
|
|
aSelection->mFrameSelection->mSelectionChangeReasons)) {
|
|
return;
|
|
}
|
|
|
|
// This operation is against the anchor but our current mAnchorFocusRange
|
|
// represents the focus in a multi-range selection. The anchor from a user
|
|
// perspective is the most distant generated range on the opposite side.
|
|
// Find that range and make it the mAnchorFocusRange.
|
|
nsRange* const newAnchorFocusRange =
|
|
FindGeneratedRangeMostDistantFromAnchor(*aSelection);
|
|
|
|
if (!newAnchorFocusRange) {
|
|
// There are no generated ranges - that's fine.
|
|
return;
|
|
}
|
|
|
|
// Setup the new mAnchorFocusRange and mark the old one as generated.
|
|
if (aSelection->mAnchorFocusRange) {
|
|
aSelection->mAnchorFocusRange->SetIsGenerated(true);
|
|
}
|
|
|
|
newAnchorFocusRange->SetIsGenerated(false);
|
|
aSelection->mAnchorFocusRange = newAnchorFocusRange;
|
|
|
|
RemoveGeneratedRanges(*aSelection);
|
|
|
|
if (aSelection->mFrameSelection) {
|
|
aSelection->mFrameSelection->InvalidateDesiredCaretPos();
|
|
}
|
|
}
|
|
|
|
private:
|
|
static nsRange* FindGeneratedRangeMostDistantFromAnchor(
|
|
const Selection& aSelection) {
|
|
const nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
|
|
const size_t len = ranges.Length();
|
|
nsRange* result{nullptr};
|
|
if (aSelection.GetDirection() == eDirNext) {
|
|
for (size_t i = 0; i < len; ++i) {
|
|
// This function is only called for selections with type == eNormal.
|
|
// (see MOZ_ASSERT in constructor).
|
|
// Therefore, all ranges must be dynamic.
|
|
if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) {
|
|
result = ranges[i].mRange->AsDynamicRange();
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
size_t i = len;
|
|
while (i--) {
|
|
if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) {
|
|
result = ranges[i].mRange->AsDynamicRange();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static void RemoveGeneratedRanges(Selection& aSelection) {
|
|
RefPtr<nsPresContext> presContext = aSelection.GetPresContext();
|
|
nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
|
|
size_t i = ranges.Length();
|
|
while (i--) {
|
|
// This function is only called for selections with type == eNormal.
|
|
// (see MOZ_ASSERT in constructor).
|
|
// Therefore, all ranges must be dynamic.
|
|
if (!ranges[i].mRange->IsDynamicRange()) {
|
|
continue;
|
|
}
|
|
nsRange* range = ranges[i].mRange->AsDynamicRange();
|
|
if (range->IsGenerated()) {
|
|
range->UnregisterSelection(aSelection);
|
|
aSelection.SelectFrames(presContext, *range, false);
|
|
ranges.RemoveElementAt(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @aParam aSelectionChangeReasons can be multiple of the reasons defined in
|
|
nsISelectionListener.idl.
|
|
*/
|
|
static bool IsAnchorRelativeOperation(const int16_t aSelectionChangeReasons) {
|
|
return aSelectionChangeReasons &
|
|
(nsISelectionListener::DRAG_REASON |
|
|
nsISelectionListener::MOUSEDOWN_REASON |
|
|
nsISelectionListener::MOUSEUP_REASON |
|
|
nsISelectionListener::COLLAPSETOSTART_REASON);
|
|
}
|
|
|
|
Maybe<Selection::AutoUserInitiated> mUserSelect;
|
|
};
|
|
|
|
} // namespace mozilla
|
|
|
|
////////////BEGIN nsFrameSelection methods
|
|
|
|
template Result<RefPtr<nsRange>, nsresult>
|
|
nsFrameSelection::CreateRangeExtendedToSomewhere(
|
|
PresShell& aPresShell,
|
|
const mozilla::LimitersAndCaretData& aLimitersAndCaretData,
|
|
const AbstractRange& aRange, nsDirection aRangeDirection,
|
|
nsDirection aExtendDirection, nsSelectionAmount aAmount,
|
|
CaretMovementStyle aMovementStyle);
|
|
template Result<RefPtr<StaticRange>, nsresult>
|
|
nsFrameSelection::CreateRangeExtendedToSomewhere(
|
|
PresShell& aPresShell,
|
|
const mozilla::LimitersAndCaretData& aLimitersAndCaretData,
|
|
const AbstractRange& aRange, nsDirection aRangeDirection,
|
|
nsDirection aExtendDirection, nsSelectionAmount aAmount,
|
|
CaretMovementStyle aMovementStyle);
|
|
|
|
nsFrameSelection::nsFrameSelection(
|
|
PresShell* aPresShell, const bool aAccessibleCaretEnabled,
|
|
Element* aEditorRootAnonymousDiv /* = nullptr */) {
|
|
for (size_t i = 0; i < std::size(mDomSelections); i++) {
|
|
mDomSelections[i] = new Selection(kPresentSelectionTypes[i], this);
|
|
}
|
|
|
|
Selection& sel = NormalSelection();
|
|
if (AutoCopyListener::IsEnabled()) {
|
|
sel.NotifyAutoCopy();
|
|
}
|
|
|
|
mPresShell = aPresShell;
|
|
mDragState = false;
|
|
|
|
MOZ_ASSERT_IF(aEditorRootAnonymousDiv,
|
|
aEditorRootAnonymousDiv->GetPseudoElementType() ==
|
|
PseudoStyleType::mozTextControlEditingRoot);
|
|
MOZ_ASSERT_IF(aEditorRootAnonymousDiv,
|
|
aEditorRootAnonymousDiv->IsHTMLElement(nsGkAtoms::div));
|
|
mLimiters.mIndependentSelectionRootElement = aEditorRootAnonymousDiv;
|
|
|
|
// This should only ever be initialized on the main thread, so we are OK here.
|
|
MOZ_ASSERT(NS_IsMainThread());
|
|
|
|
mAccessibleCaretEnabled = aAccessibleCaretEnabled;
|
|
if (mAccessibleCaretEnabled) {
|
|
sel.MaybeNotifyAccessibleCaretEventHub(aPresShell);
|
|
}
|
|
|
|
sel.EnableSelectionChangeEvent();
|
|
}
|
|
|
|
nsFrameSelection::~nsFrameSelection() = default;
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_CLASS(nsFrameSelection)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFrameSelection)
|
|
for (size_t i = 0; i < std::size(tmp->mDomSelections); ++i) {
|
|
tmp->mDomSelections[i] = nullptr;
|
|
}
|
|
tmp->mHighlightSelections.Clear();
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(
|
|
mTableSelection.mClosestInclusiveTableCellAncestor)
|
|
tmp->mTableSelection.mMode = TableSelectionMode::None;
|
|
tmp->mTableSelection.mDragSelectingCells = false;
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mStartSelectedCell)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mEndSelectedCell)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mAppendStartSelectedCell)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mUnselectCellOnMouseUp)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaintainedRange.mRange)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mIndependentSelectionRootElement)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mAncestorLimiter)
|
|
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFrameSelection)
|
|
if (tmp->mPresShell && tmp->mPresShell->GetDocument() &&
|
|
nsCCUncollectableMarker::InGeneration(
|
|
cb, tmp->mPresShell->GetDocument()->GetMarkedCCGeneration())) {
|
|
return NS_SUCCESS_INTERRUPTED_TRAVERSE;
|
|
}
|
|
for (size_t i = 0; i < std::size(tmp->mDomSelections); ++i) {
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDomSelections[i])
|
|
}
|
|
|
|
for (const auto& value : tmp->mHighlightSelections) {
|
|
CycleCollectionNoteChild(cb, value.second().get(),
|
|
"mHighlightSelections[]");
|
|
}
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
|
|
mTableSelection.mClosestInclusiveTableCellAncestor)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mStartSelectedCell)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mEndSelectedCell)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mAppendStartSelectedCell)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mUnselectCellOnMouseUp)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaintainedRange.mRange)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mIndependentSelectionRootElement)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mAncestorLimiter)
|
|
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
|
|
|
|
// static
|
|
bool nsFrameSelection::Caret::IsVisualMovement(
|
|
ExtendSelection aExtendSelection, CaretMovementStyle aMovementStyle) {
|
|
int32_t movementFlag = StaticPrefs::bidi_edit_caret_movement_style();
|
|
return aMovementStyle == eVisual ||
|
|
(aMovementStyle == eUsePrefStyle &&
|
|
(movementFlag == 1 ||
|
|
(movementFlag == 2 && aExtendSelection == ExtendSelection::No)));
|
|
}
|
|
|
|
// Get the x (or y, in vertical writing mode) position requested
|
|
// by the Key Handling for line-up/down
|
|
nsresult nsFrameSelection::DesiredCaretPos::FetchPos(
|
|
nsPoint& aDesiredCaretPos, const PresShell& aPresShell,
|
|
Selection& aNormalSelection) const {
|
|
MOZ_ASSERT(aNormalSelection.GetType() == SelectionType::eNormal);
|
|
|
|
if (mIsSet) {
|
|
aDesiredCaretPos = mValue;
|
|
return NS_OK;
|
|
}
|
|
|
|
RefPtr<nsCaret> caret = aPresShell.GetCaret();
|
|
if (!caret) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
caret->SetSelection(&aNormalSelection);
|
|
|
|
nsRect coord;
|
|
nsIFrame* caretFrame = caret->GetGeometry(&coord);
|
|
if (!caretFrame) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
nsPoint viewOffset(0, 0);
|
|
nsView* view = nullptr;
|
|
caretFrame->GetOffsetFromView(viewOffset, &view);
|
|
if (view) {
|
|
coord += viewOffset;
|
|
}
|
|
aDesiredCaretPos = coord.TopLeft();
|
|
return NS_OK;
|
|
}
|
|
|
|
void nsFrameSelection::InvalidateDesiredCaretPos() // do not listen to
|
|
// mDesiredCaretPos.mValue;
|
|
// you must get another.
|
|
{
|
|
mDesiredCaretPos.Invalidate();
|
|
}
|
|
|
|
void nsFrameSelection::DesiredCaretPos::Invalidate() { mIsSet = false; }
|
|
|
|
void nsFrameSelection::DesiredCaretPos::Set(const nsPoint& aPos) {
|
|
mValue = aPos;
|
|
mIsSet = true;
|
|
}
|
|
|
|
nsresult nsFrameSelection::ConstrainFrameAndPointToAnchorSubtree(
|
|
nsIFrame* aFrame, const nsPoint& aPoint, nsIFrame** aRetFrame,
|
|
nsPoint& aRetPoint) const {
|
|
//
|
|
// The whole point of this method is to return a frame and point that
|
|
// that lie within the same valid subtree as the anchor node's frame,
|
|
// for use with the method GetContentAndOffsetsFromPoint().
|
|
//
|
|
// A valid subtree is defined to be one where all the content nodes in
|
|
// the tree have a valid parent-child relationship.
|
|
//
|
|
// If the anchor frame and aFrame are in the same subtree, aFrame will
|
|
// be returned in aRetFrame. If they are in different subtrees, we
|
|
// return the frame for the root of the subtree.
|
|
//
|
|
|
|
if (!aFrame || !aRetFrame) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
*aRetFrame = aFrame;
|
|
aRetPoint = aPoint;
|
|
|
|
//
|
|
// Get the frame and content for the selection's anchor point!
|
|
//
|
|
|
|
const Selection& sel = NormalSelection();
|
|
|
|
nsCOMPtr<nsIContent> anchorContent =
|
|
nsIContent::FromNodeOrNull(sel.GetMayCrossShadowBoundaryAnchorNode());
|
|
if (!anchorContent) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
//
|
|
// Now find the root of the subtree containing the anchor's content.
|
|
//
|
|
|
|
NS_ENSURE_STATE(mPresShell);
|
|
RefPtr<PresShell> presShell = mPresShell;
|
|
nsIContent* anchorRoot = anchorContent->GetSelectionRootContent(
|
|
presShell, nsINode::IgnoreOwnIndependentSelection::Yes,
|
|
static_cast<nsINode::AllowCrossShadowBoundary>(
|
|
StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()));
|
|
NS_ENSURE_TRUE(anchorRoot, NS_ERROR_UNEXPECTED);
|
|
|
|
//
|
|
// Now find the root of the subtree containing aFrame's content.
|
|
//
|
|
|
|
nsCOMPtr<nsIContent> content = aFrame->GetContent();
|
|
|
|
if (content) {
|
|
nsIContent* contentRoot = content->GetSelectionRootContent(
|
|
presShell, nsINode::IgnoreOwnIndependentSelection::Yes,
|
|
static_cast<nsINode::AllowCrossShadowBoundary>(
|
|
StaticPrefs::dom_shadowdom_selection_across_boundary_enabled()));
|
|
NS_ENSURE_TRUE(contentRoot, NS_ERROR_UNEXPECTED);
|
|
|
|
if (anchorRoot == contentRoot) {
|
|
// If the aFrame's content isn't the capturing content, it should be
|
|
// a descendant. At this time, we can return simply.
|
|
nsIContent* capturedContent = PresShell::GetCapturingContent();
|
|
if (capturedContent != content) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Find the frame under the mouse cursor with the root frame.
|
|
// At this time, don't use the anchor's frame because it may not have
|
|
// fixed positioned frames.
|
|
nsIFrame* rootFrame = presShell->GetRootFrame();
|
|
nsPoint ptInRoot = aPoint + aFrame->GetOffsetTo(rootFrame);
|
|
nsIFrame* cursorFrame =
|
|
nsLayoutUtils::GetFrameForPoint(RelativeTo{rootFrame}, ptInRoot);
|
|
|
|
// If the mouse cursor in on a frame which is descendant of same
|
|
// selection root, we can expand the selection to the frame.
|
|
if (cursorFrame && cursorFrame->PresShell() == presShell) {
|
|
nsCOMPtr<nsIContent> cursorContent = cursorFrame->GetContent();
|
|
NS_ENSURE_TRUE(cursorContent, NS_ERROR_FAILURE);
|
|
nsIContent* cursorContentRoot = cursorContent->GetSelectionRootContent(
|
|
presShell, nsINode::IgnoreOwnIndependentSelection::Yes,
|
|
static_cast<nsINode::AllowCrossShadowBoundary>(
|
|
StaticPrefs::
|
|
dom_shadowdom_selection_across_boundary_enabled()));
|
|
NS_ENSURE_TRUE(cursorContentRoot, NS_ERROR_UNEXPECTED);
|
|
if (cursorContentRoot == anchorRoot) {
|
|
*aRetFrame = cursorFrame;
|
|
aRetPoint = aPoint + aFrame->GetOffsetTo(cursorFrame);
|
|
return NS_OK;
|
|
}
|
|
}
|
|
// Otherwise, e.g., the cursor isn't on any frames (e.g., the mouse
|
|
// cursor is out of the window), we should use the frame of the anchor
|
|
// root.
|
|
}
|
|
}
|
|
|
|
//
|
|
// When we can't find a frame which is under the mouse cursor and has a same
|
|
// selection root as the anchor node's, we should return the selection root
|
|
// frame.
|
|
//
|
|
|
|
*aRetFrame = anchorRoot->GetPrimaryFrame();
|
|
|
|
if (!*aRetFrame) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
//
|
|
// Now make sure that aRetPoint is converted to the same coordinate
|
|
// system used by aRetFrame.
|
|
//
|
|
|
|
aRetPoint = aPoint + aFrame->GetOffsetTo(*aRetFrame);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void nsFrameSelection::SetCaretBidiLevelAndMaybeSchedulePaint(
|
|
mozilla::intl::BidiEmbeddingLevel aLevel) {
|
|
// If the current level is undefined, we have just inserted new text.
|
|
// In this case, we don't want to reset the keyboard language
|
|
mCaret.mBidiLevel = aLevel;
|
|
|
|
RefPtr<nsCaret> caret;
|
|
if (mPresShell && (caret = mPresShell->GetCaret())) {
|
|
caret->SchedulePaint();
|
|
}
|
|
}
|
|
|
|
mozilla::intl::BidiEmbeddingLevel nsFrameSelection::GetCaretBidiLevel() const {
|
|
return mCaret.mBidiLevel;
|
|
}
|
|
|
|
void nsFrameSelection::UndefineCaretBidiLevel() {
|
|
mCaret.mBidiLevel = mozilla::intl::BidiEmbeddingLevel(mCaret.mBidiLevel |
|
|
BIDI_LEVEL_UNDEFINED);
|
|
}
|
|
|
|
#ifdef PRINT_RANGE
|
|
void printRange(nsRange* aDomRange) {
|
|
if (!aDomRange) {
|
|
printf("NULL Range\n");
|
|
}
|
|
nsINode* startNode = aDomRange->GetStartContainer();
|
|
nsINode* endNode = aDomRange->GetEndContainer();
|
|
int32_t startOffset = aDomRange->StartOffset();
|
|
int32_t endOffset = aDomRange->EndOffset();
|
|
|
|
printf("range: 0x%lx\t start: 0x%lx %ld, \t end: 0x%lx,%ld\n",
|
|
(unsigned long)aDomRange, (unsigned long)startNode, (long)startOffset,
|
|
(unsigned long)endNode, (long)endOffset);
|
|
}
|
|
#endif /* PRINT_RANGE */
|
|
|
|
static nsAtom* GetTag(nsINode* aNode) {
|
|
nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
|
|
if (!content) {
|
|
MOZ_ASSERT_UNREACHABLE("bad node passed to GetTag()");
|
|
return nullptr;
|
|
}
|
|
|
|
return content->NodeInfo()->NameAtom();
|
|
}
|
|
|
|
/**
|
|
* https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor.
|
|
*/
|
|
static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode) {
|
|
if (!aDomNode) {
|
|
return nullptr;
|
|
}
|
|
nsINode* current = aDomNode;
|
|
// Start with current node and look for a table cell
|
|
while (current) {
|
|
nsAtom* tag = GetTag(current);
|
|
if (tag == nsGkAtoms::td || tag == nsGkAtoms::th) {
|
|
return current;
|
|
}
|
|
current = current->GetParent();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static nsDirection GetCaretDirection(const nsIFrame& aFrame,
|
|
nsDirection aDirection,
|
|
bool aVisualMovement) {
|
|
const mozilla::intl::BidiDirection paragraphDirection =
|
|
nsBidiPresUtils::ParagraphDirection(&aFrame);
|
|
return (aVisualMovement &&
|
|
paragraphDirection == mozilla::intl::BidiDirection::RTL)
|
|
? nsDirection(1 - aDirection)
|
|
: aDirection;
|
|
}
|
|
|
|
nsresult nsFrameSelection::MoveCaret(nsDirection aDirection,
|
|
ExtendSelection aExtendSelection,
|
|
const nsSelectionAmount aAmount,
|
|
CaretMovementStyle aMovementStyle) {
|
|
NS_ENSURE_STATE(mPresShell);
|
|
// Flush out layout, since we need it to be up to date to do caret
|
|
// positioning.
|
|
OwningNonNull<PresShell> presShell(*mPresShell);
|
|
presShell->FlushPendingNotifications(FlushType::Layout);
|
|
|
|
if (!mPresShell) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsPresContext* context = mPresShell->GetPresContext();
|
|
if (!context) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
const RefPtr<Selection> sel = &NormalSelection();
|
|
|
|
auto scrollFlags = ScrollFlags::None;
|
|
if (sel->IsEditorSelection()) {
|
|
// If caret moves in editor, it should cause scrolling even if it's in
|
|
// overflow: hidden;.
|
|
scrollFlags |= ScrollFlags::ScrollOverflowHidden;
|
|
}
|
|
|
|
const bool doCollapse = [&] {
|
|
if (sel->IsCollapsed() || aExtendSelection == ExtendSelection::Yes) {
|
|
return false;
|
|
}
|
|
if (aAmount > eSelectLine) {
|
|
return false;
|
|
}
|
|
int32_t caretStyle = StaticPrefs::layout_selection_caret_style();
|
|
return caretStyle == 2 || (caretStyle == 0 && aAmount != eSelectLine);
|
|
}();
|
|
|
|
if (doCollapse) {
|
|
if (aDirection == eDirPrevious) {
|
|
SetChangeReasons(nsISelectionListener::COLLAPSETOSTART_REASON);
|
|
mCaret.mHint = CaretAssociationHint::After;
|
|
} else {
|
|
SetChangeReasons(nsISelectionListener::COLLAPSETOEND_REASON);
|
|
mCaret.mHint = CaretAssociationHint::Before;
|
|
}
|
|
} else {
|
|
SetChangeReasons(nsISelectionListener::KEYPRESS_REASON);
|
|
}
|
|
|
|
mCaretMoveAmount = aAmount;
|
|
|
|
AutoPrepareFocusRange prep(sel, false);
|
|
|
|
// we must keep this around and revalidate it when its just UP/DOWN
|
|
nsPoint desiredPos(0, 0);
|
|
|
|
if (aAmount == eSelectLine) {
|
|
nsresult result = mDesiredCaretPos.FetchPos(desiredPos, *mPresShell, *sel);
|
|
if (NS_FAILED(result)) {
|
|
return result;
|
|
}
|
|
mDesiredCaretPos.Set(desiredPos);
|
|
}
|
|
|
|
bool visualMovement =
|
|
Caret::IsVisualMovement(aExtendSelection, aMovementStyle);
|
|
const PrimaryFrameData frameForFocus =
|
|
sel->GetPrimaryFrameForCaretAtFocusNode(visualMovement);
|
|
if (!frameForFocus.mFrame) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
if (visualMovement) {
|
|
// FYI: This was done during a call of GetPrimaryFrameForCaretAtFocusNode.
|
|
// Therefore, this may not be intended by the original author.
|
|
SetHint(frameForFocus.mHint);
|
|
}
|
|
|
|
Result<bool, nsresult> isIntraLineCaretMove =
|
|
SelectionMovementUtils::IsIntraLineCaretMove(aAmount);
|
|
nsDirection direction{aDirection};
|
|
if (isIntraLineCaretMove.isErr()) {
|
|
return isIntraLineCaretMove.unwrapErr();
|
|
}
|
|
if (isIntraLineCaretMove.inspect()) {
|
|
// Forget old caret position for moving caret to different line since
|
|
// caret position may be changed.
|
|
mDesiredCaretPos.Invalidate();
|
|
direction =
|
|
GetCaretDirection(*frameForFocus.mFrame, aDirection, visualMovement);
|
|
}
|
|
|
|
if (doCollapse) {
|
|
const nsRange* anchorFocusRange = sel->GetAnchorFocusRange();
|
|
if (anchorFocusRange) {
|
|
RefPtr<nsINode> node;
|
|
uint32_t offset;
|
|
if (visualMovement &&
|
|
nsBidiPresUtils::IsReversedDirectionFrame(frameForFocus.mFrame)) {
|
|
direction = nsDirection(1 - direction);
|
|
}
|
|
if (direction == eDirPrevious) {
|
|
node = anchorFocusRange->GetStartContainer();
|
|
offset = anchorFocusRange->StartOffset();
|
|
} else {
|
|
node = anchorFocusRange->GetEndContainer();
|
|
offset = anchorFocusRange->EndOffset();
|
|
}
|
|
sel->CollapseInLimiter(node, offset);
|
|
}
|
|
sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
|
|
ScrollAxis(), ScrollAxis(), scrollFlags);
|
|
return NS_OK;
|
|
}
|
|
|
|
CaretAssociationHint tHint(
|
|
mCaret.mHint); // temporary variable so we dont set
|
|
// mCaret.mHint until it is necessary
|
|
|
|
Result<PeekOffsetOptions, nsresult> options =
|
|
CreatePeekOffsetOptionsForCaretMove(sel, aExtendSelection,
|
|
aMovementStyle);
|
|
if (options.isErr()) {
|
|
return options.propagateErr();
|
|
}
|
|
Result<const dom::Element*, nsresult> ancestorLimiter =
|
|
GetAncestorLimiterForCaretMove(sel);
|
|
if (ancestorLimiter.isErr()) {
|
|
return ancestorLimiter.propagateErr();
|
|
}
|
|
nsIContent* content = nsIContent::FromNodeOrNull(sel->GetFocusNode());
|
|
|
|
Result<PeekOffsetStruct, nsresult> result =
|
|
SelectionMovementUtils::PeekOffsetForCaretMove(
|
|
content, sel->FocusOffset(), direction, GetHint(),
|
|
GetCaretBidiLevel(), aAmount, desiredPos, options.unwrap(),
|
|
ancestorLimiter.unwrap());
|
|
nsresult rv;
|
|
if (result.isOk() && result.inspect().mResultContent) {
|
|
const PeekOffsetStruct& pos = result.inspect();
|
|
nsIFrame* theFrame;
|
|
int32_t frameStart, frameEnd;
|
|
|
|
if (aAmount <= eSelectWordNoSpace) {
|
|
// For left/right, PeekOffset() sets pos.mResultFrame correctly, but does
|
|
// not set pos.mAttachForward, so determine the hint here based on the
|
|
// result frame and offset: If we're at the end of a text frame, set the
|
|
// hint to ASSOCIATE_BEFORE to indicate that we want the caret displayed
|
|
// at the end of this frame, not at the beginning of the next one.
|
|
theFrame = pos.mResultFrame;
|
|
std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
|
|
if (frameEnd == pos.mContentOffset &&
|
|
!(frameStart == 0 && frameEnd == 0)) {
|
|
tHint = CaretAssociationHint::Before;
|
|
} else {
|
|
tHint = CaretAssociationHint::After;
|
|
}
|
|
} else {
|
|
// For up/down and home/end, pos.mResultFrame might not be set correctly,
|
|
// or not at all. In these cases, get the frame based on the content and
|
|
// hint returned by PeekOffset().
|
|
tHint = pos.mAttach;
|
|
theFrame = SelectionMovementUtils::GetFrameForNodeOffset(
|
|
pos.mResultContent, pos.mContentOffset, tHint);
|
|
if (!theFrame) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
|
|
}
|
|
|
|
if (context->BidiEnabled()) {
|
|
switch (aAmount) {
|
|
case eSelectBeginLine:
|
|
case eSelectEndLine: {
|
|
// In Bidi contexts, PeekOffset calculates pos.mContentOffset
|
|
// differently depending on whether the movement is visual or logical.
|
|
// For visual movement, pos.mContentOffset depends on the direction-
|
|
// ality of the first/last frame on the line (theFrame), and the caret
|
|
// directionality must correspond.
|
|
FrameBidiData bidiData = theFrame->GetBidiData();
|
|
SetCaretBidiLevelAndMaybeSchedulePaint(
|
|
visualMovement ? bidiData.embeddingLevel : bidiData.baseLevel);
|
|
break;
|
|
}
|
|
default:
|
|
// If the current position is not a frame boundary, it's enough just
|
|
// to take the Bidi level of the current frame
|
|
if ((pos.mContentOffset != frameStart &&
|
|
pos.mContentOffset != frameEnd) ||
|
|
eSelectLine == aAmount) {
|
|
SetCaretBidiLevelAndMaybeSchedulePaint(
|
|
theFrame->GetEmbeddingLevel());
|
|
} else {
|
|
BidiLevelFromMove(mPresShell, pos.mResultContent,
|
|
pos.mContentOffset, aAmount, tHint);
|
|
}
|
|
}
|
|
}
|
|
// "pos" is on the stack, so pos.mResultContent has stack lifetime, so using
|
|
// MOZ_KnownLive is ok.
|
|
const FocusMode focusMode = aExtendSelection == ExtendSelection::Yes
|
|
? FocusMode::kExtendSelection
|
|
: FocusMode::kCollapseToNewPoint;
|
|
rv = TakeFocus(MOZ_KnownLive(*pos.mResultContent), pos.mContentOffset,
|
|
pos.mContentOffset, tHint, focusMode);
|
|
} else if (aAmount <= eSelectWordNoSpace && direction == eDirNext &&
|
|
aExtendSelection == ExtendSelection::No) {
|
|
// Collapse selection if PeekOffset failed, we either
|
|
// 1. bumped into the BRFrame, bug 207623
|
|
// 2. had select-all in a text input (DIV range), bug 352759.
|
|
bool isBRFrame = frameForFocus.mFrame->IsBrFrame();
|
|
RefPtr<nsINode> node = sel->GetFocusNode();
|
|
sel->CollapseInLimiter(node, sel->FocusOffset());
|
|
// Note: 'frameForFocus.mFrame' might be dead here.
|
|
if (!isBRFrame) {
|
|
mCaret.mHint = CaretAssociationHint::Before; // We're now at the end of
|
|
// the frame to the left.
|
|
}
|
|
rv = NS_OK;
|
|
} else {
|
|
rv = result.isErr() ? result.unwrapErr() : NS_OK;
|
|
}
|
|
if (NS_SUCCEEDED(rv)) {
|
|
rv = sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
|
|
ScrollAxis(), ScrollAxis(), scrollFlags);
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
// static
|
|
Result<PeekOffsetOptions, nsresult>
|
|
nsFrameSelection::CreatePeekOffsetOptionsForCaretMove(
|
|
const Element* aSelectionLimiter, ForceEditableRegion aForceEditableRegion,
|
|
ExtendSelection aExtendSelection, CaretMovementStyle aMovementStyle) {
|
|
PeekOffsetOptions options;
|
|
// set data using aSelectionLimiter to stop on scroll views. If we have a
|
|
// limiter then we stop peeking when we hit scrollable views. If no limiter
|
|
// then just let it go ahead
|
|
if (aSelectionLimiter) {
|
|
options += PeekOffsetOption::StopAtScroller;
|
|
}
|
|
const bool visualMovement =
|
|
Caret::IsVisualMovement(aExtendSelection, aMovementStyle);
|
|
if (visualMovement) {
|
|
options += PeekOffsetOption::Visual;
|
|
}
|
|
if (aExtendSelection == ExtendSelection::Yes) {
|
|
options += PeekOffsetOption::Extend;
|
|
}
|
|
if (static_cast<bool>(aForceEditableRegion)) {
|
|
options += PeekOffsetOption::ForceEditableRegion;
|
|
}
|
|
return options;
|
|
}
|
|
|
|
Result<Element*, nsresult> nsFrameSelection::GetAncestorLimiterForCaretMove(
|
|
dom::Selection* aSelection) const {
|
|
if (!mPresShell) {
|
|
return Err(NS_ERROR_NULL_POINTER);
|
|
}
|
|
|
|
MOZ_ASSERT(aSelection);
|
|
nsIContent* content = nsIContent::FromNodeOrNull(aSelection->GetFocusNode());
|
|
if (!content) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
MOZ_ASSERT(mPresShell->GetDocument() == content->GetComposedDoc());
|
|
|
|
Element* ancestorLimiter = GetAncestorLimiter();
|
|
if (aSelection->IsEditorSelection()) {
|
|
// If the editor has not receive `focus` event, it may have not set ancestor
|
|
// limiter. Then, we need to compute it here for the caret move.
|
|
if (!ancestorLimiter) {
|
|
// Editing hosts can be nested. Therefore, computing selection root from
|
|
// selection range may be different from the focused editing host.
|
|
// Therefore, we may need to use a non-closest inclusive ancestor editing
|
|
// host of selection range container. On the other hand, selection ranges
|
|
// may be outside of focused editing host. In such case, we should use
|
|
// the closest editing host as the ancestor limiter instead.
|
|
PresShell* const presShell = aSelection->GetPresShell();
|
|
const Document* const doc =
|
|
presShell ? presShell->GetDocument() : nullptr;
|
|
if (const nsPIDOMWindowInner* const win =
|
|
doc ? doc->GetInnerWindow() : nullptr) {
|
|
Element* const focusedElement = win->GetFocusedElement();
|
|
Element* closestEditingHost = nullptr;
|
|
for (Element* element : content->InclusiveAncestorsOfType<Element>()) {
|
|
if (element->IsEditingHost()) {
|
|
if (!closestEditingHost) {
|
|
closestEditingHost = element;
|
|
}
|
|
if (focusedElement == element) {
|
|
ancestorLimiter = focusedElement;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!ancestorLimiter) {
|
|
ancestorLimiter = closestEditingHost;
|
|
}
|
|
}
|
|
// If it's the root element, we don't need to limit the new caret
|
|
// position.
|
|
if (ancestorLimiter && !ancestorLimiter->GetParent()) {
|
|
ancestorLimiter = nullptr;
|
|
}
|
|
}
|
|
}
|
|
return ancestorLimiter;
|
|
}
|
|
|
|
nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
|
|
nsIContent* aNode, uint32_t aContentOffset, bool aJumpLines) const {
|
|
return SelectionMovementUtils::GetPrevNextBidiLevels(
|
|
aNode, aContentOffset, mCaret.mHint, aJumpLines,
|
|
GetAncestorLimiterOrIndependentSelectionRootElement());
|
|
}
|
|
|
|
nsresult nsFrameSelection::MaintainSelection(nsSelectionAmount aAmount) {
|
|
const Selection& sel = NormalSelection();
|
|
|
|
mMaintainedRange.MaintainAnchorFocusRange(sel, aAmount);
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void nsFrameSelection::BidiLevelFromMove(PresShell* aPresShell,
|
|
nsIContent* aNode,
|
|
uint32_t aContentOffset,
|
|
nsSelectionAmount aAmount,
|
|
CaretAssociationHint aHint) {
|
|
switch (aAmount) {
|
|
// Movement within the line: the new cursor Bidi level is the level of the
|
|
// last character moved over
|
|
case eSelectCharacter:
|
|
case eSelectCluster:
|
|
case eSelectWord:
|
|
case eSelectWordNoSpace:
|
|
case eSelectBeginLine:
|
|
case eSelectEndLine:
|
|
case eSelectNoAmount: {
|
|
nsPrevNextBidiLevels levels =
|
|
SelectionMovementUtils::GetPrevNextBidiLevels(
|
|
aNode, aContentOffset, aHint, false,
|
|
GetAncestorLimiterOrIndependentSelectionRootElement());
|
|
|
|
SetCaretBidiLevelAndMaybeSchedulePaint(
|
|
aHint == CaretAssociationHint::Before ? levels.mLevelBefore
|
|
: levels.mLevelAfter);
|
|
break;
|
|
}
|
|
/*
|
|
// Up and Down: the new cursor Bidi level is the smaller of the two
|
|
surrounding characters case eSelectLine: case eSelectParagraph:
|
|
GetPrevNextBidiLevels(aContext, aNode, aContentOffset, &firstFrame,
|
|
&secondFrame, &firstLevel, &secondLevel);
|
|
aPresShell->SetCaretBidiLevelAndMaybeSchedulePaint(std::min(firstLevel,
|
|
secondLevel)); break;
|
|
*/
|
|
|
|
default:
|
|
UndefineCaretBidiLevel();
|
|
}
|
|
}
|
|
|
|
void nsFrameSelection::BidiLevelFromClick(nsIContent* aNode,
|
|
uint32_t aContentOffset) {
|
|
nsIFrame* clickInFrame = nullptr;
|
|
clickInFrame = SelectionMovementUtils::GetFrameForNodeOffset(
|
|
aNode, aContentOffset, mCaret.mHint);
|
|
if (!clickInFrame) {
|
|
return;
|
|
}
|
|
|
|
SetCaretBidiLevelAndMaybeSchedulePaint(clickInFrame->GetEmbeddingLevel());
|
|
}
|
|
|
|
void nsFrameSelection::MaintainedRange::AdjustNormalSelection(
|
|
const nsIContent* aContent, const int32_t aOffset,
|
|
Selection& aNormalSelection) const {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
if (!mRange || !aContent) {
|
|
return;
|
|
}
|
|
|
|
nsINode* rangeStartNode = mRange->GetStartContainer();
|
|
nsINode* rangeEndNode = mRange->GetEndContainer();
|
|
const uint32_t rangeStartOffset = mRange->StartOffset();
|
|
const uint32_t rangeEndOffset = mRange->EndOffset();
|
|
|
|
NS_ASSERTION(aOffset >= 0, "aOffset should not be negative");
|
|
const Maybe<int32_t> relToStart =
|
|
nsContentUtils::ComparePoints_AllowNegativeOffsets(
|
|
rangeStartNode, rangeStartOffset, aContent, aOffset);
|
|
if (NS_WARN_IF(!relToStart)) {
|
|
// Potentially handle this properly when Selection across Shadow DOM
|
|
// boundary is implemented
|
|
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
|
|
return;
|
|
}
|
|
|
|
const Maybe<int32_t> relToEnd =
|
|
nsContentUtils::ComparePoints_AllowNegativeOffsets(
|
|
rangeEndNode, rangeEndOffset, aContent, aOffset);
|
|
if (NS_WARN_IF(!relToEnd)) {
|
|
// Potentially handle this properly when Selection across Shadow DOM
|
|
// boundary is implemented
|
|
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
|
|
return;
|
|
}
|
|
|
|
// If aContent/aOffset is inside (or at the edge of) the maintained
|
|
// selection, or if it is on the "anchor" side of the maintained selection,
|
|
// we need to do something.
|
|
if ((*relToStart <= 0 && *relToEnd >= 0) ||
|
|
(*relToStart > 0 && aNormalSelection.GetDirection() == eDirNext) ||
|
|
(*relToEnd < 0 && aNormalSelection.GetDirection() == eDirPrevious)) {
|
|
// Set the current range to the maintained range.
|
|
aNormalSelection.ReplaceAnchorFocusRange(mRange);
|
|
// Set the direction of the selection so that the anchor will be on the
|
|
// far side of the maintained selection, relative to aContent/aOffset.
|
|
aNormalSelection.SetDirection(*relToStart > 0 ? eDirPrevious : eDirNext);
|
|
}
|
|
}
|
|
|
|
void nsFrameSelection::MaintainedRange::AdjustContentOffsets(
|
|
nsIFrame::ContentOffsets& aOffsets, StopAtScroller aStopAtScroller) const {
|
|
// Adjust offsets according to maintained amount
|
|
if (mRange && mAmount != eSelectNoAmount) {
|
|
const Maybe<int32_t> relativePosition = nsContentUtils::ComparePoints(
|
|
mRange->StartRef(),
|
|
RawRangeBoundary(aOffsets.content, aOffsets.offset,
|
|
RangeBoundaryIsMutationObserved::No));
|
|
if (NS_WARN_IF(!relativePosition)) {
|
|
// Potentially handle this properly when Selection across Shadow DOM
|
|
// boundary is implemented
|
|
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
|
|
return;
|
|
}
|
|
|
|
nsDirection direction = *relativePosition > 0 ? eDirPrevious : eDirNext;
|
|
nsSelectionAmount amount = mAmount;
|
|
if (amount == eSelectBeginLine && direction == eDirNext) {
|
|
amount = eSelectEndLine;
|
|
}
|
|
|
|
uint32_t offset;
|
|
nsIFrame* frame = SelectionMovementUtils::GetFrameForNodeOffset(
|
|
aOffsets.content, aOffsets.offset, CaretAssociationHint::After,
|
|
&offset);
|
|
|
|
PeekOffsetOptions peekOffsetOptions{};
|
|
if (aStopAtScroller == StopAtScroller::Yes) {
|
|
peekOffsetOptions += PeekOffsetOption::StopAtScroller;
|
|
}
|
|
if (frame && amount == eSelectWord && direction == eDirPrevious) {
|
|
// To avoid selecting the previous word when at start of word,
|
|
// first move one character forward.
|
|
PeekOffsetStruct charPos(eSelectCharacter, eDirNext,
|
|
static_cast<int32_t>(offset), nsPoint(0, 0),
|
|
peekOffsetOptions);
|
|
if (NS_SUCCEEDED(frame->PeekOffset(&charPos))) {
|
|
frame = charPos.mResultFrame;
|
|
offset = charPos.mContentOffset;
|
|
}
|
|
}
|
|
|
|
PeekOffsetStruct pos(amount, direction, static_cast<int32_t>(offset),
|
|
nsPoint(0, 0), peekOffsetOptions);
|
|
if (frame && NS_SUCCEEDED(frame->PeekOffset(&pos)) && pos.mResultContent) {
|
|
aOffsets.content = pos.mResultContent;
|
|
aOffsets.offset = pos.mContentOffset;
|
|
}
|
|
}
|
|
}
|
|
|
|
void nsFrameSelection::MaintainedRange::MaintainAnchorFocusRange(
|
|
const Selection& aNormalSelection, const nsSelectionAmount aAmount) {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
mAmount = aAmount;
|
|
|
|
const nsRange* anchorFocusRange = aNormalSelection.GetAnchorFocusRange();
|
|
if (anchorFocusRange && aAmount != eSelectNoAmount) {
|
|
mRange = anchorFocusRange->CloneRange();
|
|
return;
|
|
}
|
|
|
|
mRange = nullptr;
|
|
}
|
|
|
|
nsresult nsFrameSelection::HandleClick(nsIContent* aNewFocus,
|
|
uint32_t aContentOffset,
|
|
uint32_t aContentEndOffset,
|
|
const FocusMode aFocusMode,
|
|
CaretAssociationHint aHint) {
|
|
if (!aNewFocus) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
if (MOZ_LOG_TEST(sFrameSelectionLog, LogLevel::Debug)) {
|
|
const Selection& sel = NormalSelection();
|
|
MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
|
|
("%s: selection=%p, new focus=%p, offsets=(%u,%u), focus mode=%i",
|
|
__FUNCTION__, &sel, aNewFocus, aContentOffset, aContentEndOffset,
|
|
static_cast<int>(aFocusMode)));
|
|
}
|
|
|
|
mDesiredCaretPos.Invalidate();
|
|
|
|
if (aFocusMode != FocusMode::kExtendSelection) {
|
|
mMaintainedRange.mRange = nullptr;
|
|
if (!NodeIsInLimiters(aNewFocus)) {
|
|
mLimiters.mAncestorLimiter = nullptr;
|
|
}
|
|
}
|
|
|
|
// Don't take focus when dragging off of a table
|
|
if (!mTableSelection.mDragSelectingCells) {
|
|
BidiLevelFromClick(aNewFocus, aContentOffset);
|
|
SetChangeReasons(nsISelectionListener::MOUSEDOWN_REASON +
|
|
nsISelectionListener::DRAG_REASON);
|
|
|
|
RefPtr<Selection> selection = &NormalSelection();
|
|
|
|
if (aFocusMode == FocusMode::kExtendSelection) {
|
|
mMaintainedRange.AdjustNormalSelection(aNewFocus, aContentOffset,
|
|
*selection);
|
|
}
|
|
|
|
AutoPrepareFocusRange prep(selection,
|
|
aFocusMode == FocusMode::kMultiRangeSelection);
|
|
return TakeFocus(*aNewFocus, aContentOffset, aContentEndOffset, aHint,
|
|
aFocusMode);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void nsFrameSelection::HandleDrag(nsIFrame* aFrame, const nsPoint& aPoint) {
|
|
if (!aFrame || !mPresShell) {
|
|
return;
|
|
}
|
|
|
|
nsresult result;
|
|
nsIFrame* newFrame = 0;
|
|
nsPoint newPoint;
|
|
|
|
result = ConstrainFrameAndPointToAnchorSubtree(aFrame, aPoint, &newFrame,
|
|
newPoint);
|
|
if (NS_FAILED(result)) {
|
|
return;
|
|
}
|
|
if (!newFrame) {
|
|
return;
|
|
}
|
|
|
|
nsIFrame::ContentOffsets offsets =
|
|
newFrame->GetContentOffsetsFromPoint(newPoint);
|
|
if (!offsets.content) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<Selection> selection = &NormalSelection();
|
|
if (newFrame->IsSelected()) {
|
|
// `MOZ_KnownLive` required because of
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1636889.
|
|
mMaintainedRange.AdjustNormalSelection(MOZ_KnownLive(offsets.content),
|
|
offsets.offset, *selection);
|
|
}
|
|
|
|
mMaintainedRange.AdjustContentOffsets(
|
|
offsets, mLimiters.mIndependentSelectionRootElement
|
|
? MaintainedRange::StopAtScroller::Yes
|
|
: MaintainedRange::StopAtScroller::No);
|
|
|
|
// TODO: no click has happened, rename `HandleClick`.
|
|
HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */, offsets.offset,
|
|
offsets.offset, FocusMode::kExtendSelection, offsets.associate);
|
|
}
|
|
|
|
nsresult nsFrameSelection::StartAutoScrollTimer(nsIFrame* aFrame,
|
|
const nsPoint& aPoint,
|
|
uint32_t aDelay) {
|
|
RefPtr<Selection> selection = &NormalSelection();
|
|
return selection->StartAutoScrollTimer(aFrame, aPoint, aDelay);
|
|
}
|
|
|
|
void nsFrameSelection::StopAutoScrollTimer() {
|
|
Selection& sel = NormalSelection();
|
|
sel.StopAutoScrollTimer();
|
|
}
|
|
|
|
// static
|
|
nsINode* nsFrameSelection::TableSelection::IsContentInActivelyEditableTableCell(
|
|
nsPresContext* aContext, nsIContent* aContent) {
|
|
if (!aContext) {
|
|
return nullptr;
|
|
}
|
|
|
|
RefPtr<HTMLEditor> htmlEditor = nsContentUtils::GetHTMLEditor(aContext);
|
|
if (!htmlEditor) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsINode* inclusiveTableCellAncestor =
|
|
GetClosestInclusiveTableCellAncestor(aContent);
|
|
if (!inclusiveTableCellAncestor) {
|
|
return nullptr;
|
|
}
|
|
|
|
const Element* editingHost = htmlEditor->ComputeEditingHost();
|
|
if (!editingHost) {
|
|
return nullptr;
|
|
}
|
|
|
|
const bool editableCell =
|
|
inclusiveTableCellAncestor->IsInclusiveDescendantOf(editingHost);
|
|
return editableCell ? inclusiveTableCellAncestor : nullptr;
|
|
}
|
|
|
|
namespace {
|
|
struct ParentAndOffset {
|
|
explicit ParentAndOffset(const nsINode& aNode)
|
|
: mParent{aNode.GetParent()},
|
|
mOffset{mParent ? mParent->ComputeIndexOf_Deprecated(&aNode) : 0} {}
|
|
|
|
nsINode* mParent;
|
|
|
|
// 0, if there's no parent.
|
|
int32_t mOffset;
|
|
};
|
|
|
|
} // namespace
|
|
/**
|
|
hard to go from nodes to frames, easy the other way!
|
|
*/
|
|
nsresult nsFrameSelection::TakeFocus(nsIContent& aNewFocus,
|
|
uint32_t aContentOffset,
|
|
uint32_t aContentEndOffset,
|
|
CaretAssociationHint aHint,
|
|
const FocusMode aFocusMode) {
|
|
NS_ENSURE_STATE(mPresShell);
|
|
|
|
if (!NodeIsInLimiters(&aNewFocus)) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
MOZ_LOG(sFrameSelectionLog, LogLevel::Verbose,
|
|
("%s: new focus=%p, offsets=(%u, %u), hint=%i, focusMode=%i",
|
|
__FUNCTION__, &aNewFocus, aContentOffset, aContentEndOffset,
|
|
static_cast<int>(aHint), static_cast<int>(aFocusMode)));
|
|
|
|
mPresShell->FrameSelectionWillTakeFocus(
|
|
*this, aNewFocus.CanStartSelectionAsWebCompatHack()
|
|
? PresShell::CanMoveLastSelectionForToString::Yes
|
|
: PresShell::CanMoveLastSelectionForToString::No);
|
|
|
|
// Clear all table selection data
|
|
mTableSelection.mMode = TableSelectionMode::None;
|
|
mTableSelection.mDragSelectingCells = false;
|
|
mTableSelection.mStartSelectedCell = nullptr;
|
|
mTableSelection.mEndSelectedCell = nullptr;
|
|
mTableSelection.mAppendStartSelectedCell = nullptr;
|
|
mCaret.mHint = aHint;
|
|
|
|
RefPtr<Selection> selection = &NormalSelection();
|
|
|
|
Maybe<Selection::AutoUserInitiated> userSelect;
|
|
if (IsUserSelectionReason()) {
|
|
userSelect.emplace(selection);
|
|
}
|
|
|
|
// traverse through document and unselect crap here
|
|
switch (aFocusMode) {
|
|
case FocusMode::kCollapseToNewPoint:
|
|
[[fallthrough]];
|
|
case FocusMode::kMultiRangeSelection: {
|
|
// single click? setting cursor down
|
|
const Batching saveBatching =
|
|
mBatching; // hack to use the collapse code.
|
|
mBatching.mCounter = 1;
|
|
|
|
if (aFocusMode == FocusMode::kMultiRangeSelection) {
|
|
// Remove existing collapsed ranges as there's no point in having
|
|
// non-anchor/focus collapsed ranges.
|
|
selection->RemoveCollapsedRanges();
|
|
|
|
ErrorResult error;
|
|
RefPtr<nsRange> newRange = nsRange::Create(
|
|
&aNewFocus, aContentOffset, &aNewFocus, aContentOffset, error);
|
|
if (NS_WARN_IF(error.Failed())) {
|
|
return error.StealNSResult();
|
|
}
|
|
MOZ_ASSERT(newRange);
|
|
selection->AddRangeAndSelectFramesAndNotifyListeners(*newRange,
|
|
IgnoreErrors());
|
|
} else {
|
|
bool oldDesiredPosSet =
|
|
mDesiredCaretPos.mIsSet; // need to keep old desired
|
|
// position if it was set.
|
|
selection->CollapseInLimiter(&aNewFocus, aContentOffset);
|
|
mDesiredCaretPos.mIsSet =
|
|
oldDesiredPosSet; // now reset desired pos back.
|
|
}
|
|
|
|
mBatching = saveBatching;
|
|
|
|
if (aContentEndOffset != aContentOffset) {
|
|
selection->Extend(&aNewFocus, aContentEndOffset);
|
|
}
|
|
|
|
// find out if we are inside a table. if so, find out which one and which
|
|
// cell once we do that, the next time we get a takefocus, check the
|
|
// parent tree. if we are no longer inside same table ,cell then switch to
|
|
// table selection mode. BUT only do this in an editor
|
|
|
|
NS_ENSURE_STATE(mPresShell);
|
|
RefPtr<nsPresContext> context = mPresShell->GetPresContext();
|
|
mTableSelection.mClosestInclusiveTableCellAncestor = nullptr;
|
|
if (nsINode* inclusiveTableCellAncestor =
|
|
TableSelection::IsContentInActivelyEditableTableCell(
|
|
context, &aNewFocus)) {
|
|
mTableSelection.mClosestInclusiveTableCellAncestor =
|
|
inclusiveTableCellAncestor;
|
|
MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
|
|
("%s: Collapsing into new cell", __FUNCTION__));
|
|
}
|
|
|
|
break;
|
|
}
|
|
case FocusMode::kExtendSelection: {
|
|
// Now update the range list:
|
|
nsINode* inclusiveTableCellAncestor =
|
|
GetClosestInclusiveTableCellAncestor(&aNewFocus);
|
|
if (mTableSelection.mClosestInclusiveTableCellAncestor &&
|
|
inclusiveTableCellAncestor &&
|
|
inclusiveTableCellAncestor !=
|
|
mTableSelection
|
|
.mClosestInclusiveTableCellAncestor) // switch to cell
|
|
// selection mode
|
|
{
|
|
MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
|
|
("%s: moving into new cell", __FUNCTION__));
|
|
|
|
WidgetMouseEvent event(false, eVoidEvent, nullptr,
|
|
WidgetMouseEvent::eReal);
|
|
|
|
// Start selecting in the cell we were in before
|
|
ParentAndOffset parentAndOffset{
|
|
*mTableSelection.mClosestInclusiveTableCellAncestor};
|
|
if (const nsCOMPtr<nsINode> previousParent = parentAndOffset.mParent) {
|
|
const nsresult result =
|
|
HandleTableSelection(previousParent, parentAndOffset.mOffset,
|
|
TableSelectionMode::Cell, &event);
|
|
if (NS_WARN_IF(NS_FAILED(result))) {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Find the parent of this new cell and extend selection to it
|
|
parentAndOffset = ParentAndOffset{*inclusiveTableCellAncestor};
|
|
|
|
// XXXX We need to REALLY get the current key shift state
|
|
// (we'd need to add event listener -- let's not bother for now)
|
|
event.mModifiers &= ~MODIFIER_SHIFT; // aExtendSelection;
|
|
if (const nsCOMPtr<nsINode> newParent = parentAndOffset.mParent) {
|
|
mTableSelection.mClosestInclusiveTableCellAncestor =
|
|
inclusiveTableCellAncestor;
|
|
// Continue selection into next cell
|
|
const nsresult result =
|
|
HandleTableSelection(newParent, parentAndOffset.mOffset,
|
|
TableSelectionMode::Cell, &event);
|
|
if (NS_WARN_IF(NS_FAILED(result))) {
|
|
return result;
|
|
}
|
|
}
|
|
} else {
|
|
// XXXX Problem: Shift+click in browser is appending text selection to
|
|
// selected table!!!
|
|
// is this the place to erase selected cells ?????
|
|
uint32_t offset =
|
|
(selection->GetDirection() == eDirNext &&
|
|
aContentEndOffset > aContentOffset) // didn't go far enough
|
|
? aContentEndOffset // this will only redraw the diff
|
|
: aContentOffset;
|
|
selection->Extend(&aNewFocus, offset);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Be aware, the Selection instance may be destroyed after this call.
|
|
return NotifySelectionListeners(SelectionType::eNormal);
|
|
}
|
|
|
|
UniquePtr<SelectionDetails> nsFrameSelection::LookUpSelection(
|
|
nsIContent* aContent, int32_t aContentOffset, int32_t aContentLength,
|
|
bool aSlowCheck) const {
|
|
if (!aContent || !mPresShell) {
|
|
return nullptr;
|
|
}
|
|
|
|
// TODO: Layout should use `uint32_t` for handling offset in DOM nodes
|
|
// (for example: bug 1735262)
|
|
MOZ_ASSERT(aContentOffset >= 0);
|
|
MOZ_ASSERT(aContentLength >= 0);
|
|
if (MOZ_UNLIKELY(aContentOffset < 0) || MOZ_UNLIKELY(aContentLength < 0)) {
|
|
return nullptr;
|
|
}
|
|
|
|
UniquePtr<SelectionDetails> details;
|
|
|
|
for (size_t j = 0; j < std::size(mDomSelections); j++) {
|
|
MOZ_ASSERT(mDomSelections[j]);
|
|
details = mDomSelections[j]->LookUpSelection(
|
|
aContent, static_cast<uint32_t>(aContentOffset),
|
|
static_cast<uint32_t>(aContentLength), std::move(details),
|
|
kPresentSelectionTypes[j], aSlowCheck);
|
|
}
|
|
|
|
// This may seem counter intuitive at first. Highlight selections need to be
|
|
// iterated from back to front:
|
|
//
|
|
// - `mHighlightSelections` is ordered by insertion, i.e. if two or more
|
|
// highlights overlap, the latest must take precedence.
|
|
// - however, the `LookupSelection()` algorithm reverses the order by setting
|
|
// the current `details` as `mNext`.
|
|
for (const auto& iter : Reversed(mHighlightSelections)) {
|
|
details = iter.second()->LookUpSelection(
|
|
aContent, static_cast<uint32_t>(aContentOffset),
|
|
static_cast<uint32_t>(aContentLength), std::move(details),
|
|
SelectionType::eHighlight, aSlowCheck);
|
|
}
|
|
|
|
return details;
|
|
}
|
|
|
|
void nsFrameSelection::SetDragState(bool aState) {
|
|
if (mDragState == aState) {
|
|
return;
|
|
}
|
|
|
|
mDragState = aState;
|
|
|
|
if (!mDragState) {
|
|
mTableSelection.mDragSelectingCells = false;
|
|
// Notify that reason is mouse up.
|
|
SetChangeReasons(nsISelectionListener::MOUSEUP_REASON);
|
|
|
|
// flag is set to NotApplicable in `Selection::NotifySelectionListeners`.
|
|
// since this function call is part of click event, this would immediately
|
|
// reset the flag, rendering it useless.
|
|
AutoRestore<ClickSelectionType> restoreClickSelectionType(
|
|
mClickSelectionType);
|
|
// Be aware, the Selection instance may be destroyed after this call.
|
|
NotifySelectionListeners(SelectionType::eNormal);
|
|
}
|
|
}
|
|
|
|
Selection* nsFrameSelection::GetSelection(SelectionType aSelectionType) const {
|
|
int8_t index = GetIndexFromSelectionType(aSelectionType);
|
|
if (index < 0) {
|
|
return nullptr;
|
|
}
|
|
MOZ_ASSERT(mDomSelections[index]);
|
|
return mDomSelections[index];
|
|
}
|
|
|
|
void nsFrameSelection::AddHighlightSelection(
|
|
nsAtom* aHighlightName, mozilla::dom::Highlight& aHighlight) {
|
|
RefPtr<Selection> selection =
|
|
aHighlight.CreateHighlightSelection(aHighlightName, this);
|
|
if (auto iter =
|
|
std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
|
|
[&aHighlightName](auto const& aElm) {
|
|
return aElm.first() == aHighlightName;
|
|
});
|
|
iter != mHighlightSelections.end()) {
|
|
iter->second() = std::move(selection);
|
|
} else {
|
|
mHighlightSelections.AppendElement(
|
|
CompactPair<RefPtr<nsAtom>, RefPtr<Selection>>(aHighlightName,
|
|
std::move(selection)));
|
|
}
|
|
}
|
|
|
|
void nsFrameSelection::RepaintHighlightSelection(
|
|
nsAtom* aHighlightName) {
|
|
if (auto iter =
|
|
std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
|
|
[&aHighlightName](auto const& aElm) {
|
|
return aElm.first() == aHighlightName;
|
|
});
|
|
iter != mHighlightSelections.end()) {
|
|
RefPtr selection = iter->second();
|
|
selection->Repaint(mPresShell->GetPresContext());
|
|
}
|
|
}
|
|
|
|
void nsFrameSelection::RemoveHighlightSelection(nsAtom* aHighlightName) {
|
|
if (auto iter =
|
|
std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
|
|
[&aHighlightName](auto const& aElm) {
|
|
return aElm.first() == aHighlightName;
|
|
});
|
|
iter != mHighlightSelections.end()) {
|
|
RefPtr<Selection> selection = iter->second();
|
|
selection->RemoveAllRanges(IgnoreErrors());
|
|
mHighlightSelections.RemoveElementAt(iter);
|
|
}
|
|
}
|
|
|
|
void nsFrameSelection::AddHighlightSelectionRange(
|
|
nsAtom* aHighlightName, mozilla::dom::Highlight& aHighlight,
|
|
mozilla::dom::AbstractRange& aRange) {
|
|
if (auto iter =
|
|
std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
|
|
[&aHighlightName](auto const& aElm) {
|
|
return aElm.first() == aHighlightName;
|
|
});
|
|
iter != mHighlightSelections.end()) {
|
|
RefPtr<Selection> selection = iter->second();
|
|
selection->AddHighlightRangeAndSelectFramesAndNotifyListeners(aRange);
|
|
} else {
|
|
// if the selection does not exist yet, add all of its ranges and exit.
|
|
RefPtr<Selection> selection =
|
|
aHighlight.CreateHighlightSelection(aHighlightName, this);
|
|
mHighlightSelections.AppendElement(
|
|
CompactPair<RefPtr<nsAtom>, RefPtr<Selection>>(aHighlightName,
|
|
std::move(selection)));
|
|
}
|
|
}
|
|
|
|
void nsFrameSelection::RemoveHighlightSelectionRange(
|
|
nsAtom* aHighlightName, mozilla::dom::AbstractRange& aRange) {
|
|
if (auto iter =
|
|
std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
|
|
[&aHighlightName](auto const& aElm) {
|
|
return aElm.first() == aHighlightName;
|
|
});
|
|
iter != mHighlightSelections.end()) {
|
|
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization)
|
|
RefPtr<Selection> selection = iter->second();
|
|
selection->RemoveRangeAndUnselectFramesAndNotifyListeners(aRange,
|
|
IgnoreErrors());
|
|
}
|
|
}
|
|
|
|
nsresult nsFrameSelection::ScrollSelectionIntoView(SelectionType aSelectionType,
|
|
SelectionRegion aRegion,
|
|
int16_t aFlags) const {
|
|
RefPtr<Selection> sel = GetSelection(aSelectionType);
|
|
if (!sel) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
const auto vScroll = [&]() -> WhereToScroll {
|
|
if (aFlags & nsISelectionController::SCROLL_VERTICAL_START) {
|
|
return WhereToScroll::Start;
|
|
}
|
|
if (aFlags & nsISelectionController::SCROLL_VERTICAL_END) {
|
|
return WhereToScroll::End;
|
|
}
|
|
if (aFlags & nsISelectionController::SCROLL_VERTICAL_CENTER) {
|
|
return WhereToScroll::Center;
|
|
}
|
|
return WhereToScroll::Nearest;
|
|
}();
|
|
|
|
auto mode = aFlags & nsISelectionController::SCROLL_SYNCHRONOUS
|
|
? SelectionScrollMode::SyncFlush
|
|
: SelectionScrollMode::Async;
|
|
|
|
auto scrollFlags = ScrollFlags::None;
|
|
if (aFlags & nsISelectionController::SCROLL_OVERFLOW_HIDDEN) {
|
|
scrollFlags |= ScrollFlags::ScrollOverflowHidden;
|
|
}
|
|
|
|
// After ScrollSelectionIntoView(), the pending notifications might be
|
|
// flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
|
|
return sel->ScrollIntoView(aRegion, ScrollAxis(vScroll), ScrollAxis(),
|
|
scrollFlags, mode);
|
|
}
|
|
|
|
nsresult nsFrameSelection::RepaintSelection(SelectionType aSelectionType) {
|
|
RefPtr<Selection> sel = GetSelection(aSelectionType);
|
|
if (!sel) {
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
NS_ENSURE_STATE(mPresShell);
|
|
|
|
// On macOS, update the selection cache to the new active selection
|
|
// aka the current selection.
|
|
#ifdef XP_MACOSX
|
|
// Check that we're in the an active window and, if this is Web content,
|
|
// in the frontmost tab.
|
|
Document* doc = mPresShell->GetDocument();
|
|
if (doc && IsInActiveTab(doc) && aSelectionType == SelectionType::eNormal) {
|
|
UpdateSelectionCacheOnRepaintSelection(sel);
|
|
}
|
|
#endif
|
|
return sel->Repaint(mPresShell->GetPresContext());
|
|
}
|
|
|
|
nsIFrame* nsFrameSelection::GetFrameToPageSelect() const {
|
|
if (NS_WARN_IF(!mPresShell)) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsIFrame* rootFrameToSelect;
|
|
if (mLimiters.mIndependentSelectionRootElement) {
|
|
rootFrameToSelect =
|
|
mLimiters.mIndependentSelectionRootElement->GetPrimaryFrame();
|
|
if (NS_WARN_IF(!rootFrameToSelect)) {
|
|
return nullptr;
|
|
}
|
|
} else if (mLimiters.mAncestorLimiter) {
|
|
rootFrameToSelect = mLimiters.mAncestorLimiter->GetPrimaryFrame();
|
|
if (NS_WARN_IF(!rootFrameToSelect)) {
|
|
return nullptr;
|
|
}
|
|
} else {
|
|
rootFrameToSelect = mPresShell->GetRootScrollContainerFrame();
|
|
if (NS_WARN_IF(!rootFrameToSelect)) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
nsCOMPtr<nsIContent> contentToSelect = mPresShell->GetContentForScrolling();
|
|
if (contentToSelect) {
|
|
// If there is selected content, look for nearest and vertical scrollable
|
|
// parent under the root frame.
|
|
for (nsIFrame* frame = contentToSelect->GetPrimaryFrame();
|
|
frame && frame != rootFrameToSelect; frame = frame->GetParent()) {
|
|
ScrollContainerFrame* scrollContainerFrame = do_QueryFrame(frame);
|
|
if (!scrollContainerFrame) {
|
|
continue;
|
|
}
|
|
ScrollStyles scrollStyles = scrollContainerFrame->GetScrollStyles();
|
|
if (scrollStyles.mVertical == StyleOverflow::Hidden) {
|
|
continue;
|
|
}
|
|
layers::ScrollDirections directions =
|
|
scrollContainerFrame->GetAvailableScrollingDirections();
|
|
if (directions.contains(layers::ScrollDirection::eVertical)) {
|
|
// If there is sub scrollable frame, let's use its page size to select.
|
|
return frame;
|
|
}
|
|
}
|
|
}
|
|
// Otherwise, i.e., there is no scrollable frame or only the root frame is
|
|
// scrollable, let's return the root frame because Shift + PageUp/PageDown
|
|
// should expand the selection in the root content even if it's not
|
|
// scrollable.
|
|
return rootFrameToSelect;
|
|
}
|
|
|
|
nsresult nsFrameSelection::PageMove(bool aForward, bool aExtend,
|
|
nsIFrame* aFrame,
|
|
SelectionIntoView aSelectionIntoView) {
|
|
MOZ_ASSERT(aFrame);
|
|
|
|
// expected behavior for PageMove is to scroll AND move the caret
|
|
// and remain relative position of the caret in view. see Bug 4302.
|
|
|
|
// Get the scroll container frame. If aFrame is not scrollable, this is
|
|
// nullptr.
|
|
ScrollContainerFrame* scrollContainerFrame = aFrame->GetScrollTargetFrame();
|
|
// Get the scrolled frame. If aFrame is not scrollable, this is aFrame
|
|
// itself.
|
|
nsIFrame* scrolledFrame =
|
|
scrollContainerFrame ? scrollContainerFrame->GetScrolledFrame() : aFrame;
|
|
if (!scrolledFrame) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// find out where the caret is.
|
|
// we should know mDesiredCaretPos.mValue value of nsFrameSelection, but I
|
|
// havent seen that behavior in other windows applications yet.
|
|
RefPtr<Selection> selection = &NormalSelection();
|
|
if (!selection) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsRect caretPos;
|
|
nsIFrame* caretFrame = nsCaret::GetGeometry(selection, &caretPos);
|
|
if (!caretFrame) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// If the scrolled frame is outside of current selection limiter,
|
|
// we need to scroll the frame but keep moving selection in the limiter.
|
|
nsIFrame* frameToClick = scrolledFrame;
|
|
if (!NodeIsInLimiters(scrolledFrame->GetContent())) {
|
|
frameToClick = GetFrameToPageSelect();
|
|
if (NS_WARN_IF(!frameToClick)) {
|
|
return NS_OK;
|
|
}
|
|
}
|
|
|
|
if (scrollContainerFrame) {
|
|
// If there is a scrollable frame, adjust pseudo-click position with page
|
|
// scroll amount.
|
|
// XXX This may scroll more than one page if ScrollSelectionIntoView is
|
|
// called later because caret may not fully visible. E.g., if
|
|
// clicking line will be visible only half height with scrolling
|
|
// the frame, ScrollSelectionIntoView additionally scrolls to show
|
|
// the caret entirely.
|
|
if (aForward) {
|
|
caretPos.y += scrollContainerFrame->GetPageScrollAmount().height;
|
|
} else {
|
|
caretPos.y -= scrollContainerFrame->GetPageScrollAmount().height;
|
|
}
|
|
} else {
|
|
// Otherwise, adjust pseudo-click position with the frame size.
|
|
if (aForward) {
|
|
caretPos.y += frameToClick->GetSize().height;
|
|
} else {
|
|
caretPos.y -= frameToClick->GetSize().height;
|
|
}
|
|
}
|
|
|
|
caretPos += caretFrame->GetOffsetTo(frameToClick);
|
|
|
|
// get a content at desired location
|
|
nsPoint desiredPoint;
|
|
desiredPoint.x = caretPos.x;
|
|
desiredPoint.y = caretPos.y + caretPos.height / 2;
|
|
nsIFrame::ContentOffsets offsets =
|
|
frameToClick->GetContentOffsetsFromPoint(desiredPoint);
|
|
|
|
if (!offsets.content) {
|
|
// XXX Do we need to handle ScrollSelectionIntoView in this case?
|
|
return NS_OK;
|
|
}
|
|
|
|
// First, place the caret.
|
|
bool selectionChanged;
|
|
{
|
|
// We don't want any script to run until we check whether selection is
|
|
// modified by HandleClick.
|
|
SelectionBatcher ensureNoSelectionChangeNotifications(selection,
|
|
__FUNCTION__);
|
|
|
|
RangeBoundary oldAnchor = selection->AnchorRef();
|
|
RangeBoundary oldFocus = selection->FocusRef();
|
|
|
|
const FocusMode focusMode =
|
|
aExtend ? FocusMode::kExtendSelection : FocusMode::kCollapseToNewPoint;
|
|
HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
|
|
offsets.offset, offsets.offset, focusMode,
|
|
CaretAssociationHint::After);
|
|
|
|
selectionChanged = selection->AnchorRef() != oldAnchor ||
|
|
selection->FocusRef() != oldFocus;
|
|
}
|
|
|
|
bool doScrollSelectionIntoView = !(
|
|
aSelectionIntoView == SelectionIntoView::IfChanged && !selectionChanged);
|
|
|
|
// Then, scroll the given frame one page.
|
|
if (scrollContainerFrame) {
|
|
// If we'll call ScrollSelectionIntoView later and selection wasn't
|
|
// changed and we scroll outside of selection limiter, we shouldn't use
|
|
// smooth scroll here because ScrollContainerFrame uses normal runnable,
|
|
// but ScrollSelectionIntoView uses early runner and it cancels the
|
|
// pending smooth scroll. Therefore, if we used smooth scroll in such
|
|
// case, ScrollSelectionIntoView would scroll to show caret instead of
|
|
// page scroll of an element outside selection limiter.
|
|
ScrollMode scrollMode = doScrollSelectionIntoView && !selectionChanged &&
|
|
scrolledFrame != frameToClick
|
|
? ScrollMode::Instant
|
|
: ScrollMode::Smooth;
|
|
scrollContainerFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1),
|
|
ScrollUnit::PAGES, scrollMode);
|
|
}
|
|
|
|
// Finally, scroll selection into view if requested.
|
|
if (!doScrollSelectionIntoView) {
|
|
return NS_OK;
|
|
}
|
|
return ScrollSelectionIntoView(SelectionType::eNormal,
|
|
nsISelectionController::SELECTION_FOCUS_REGION,
|
|
nsISelectionController::SCROLL_SYNCHRONOUS);
|
|
}
|
|
|
|
nsresult nsFrameSelection::PhysicalMove(int16_t aDirection, int16_t aAmount,
|
|
bool aExtend) {
|
|
NS_ENSURE_STATE(mPresShell);
|
|
// Flush out layout, since we need it to be up to date to do caret
|
|
// positioning.
|
|
OwningNonNull<PresShell> presShell(*mPresShell);
|
|
presShell->FlushPendingNotifications(FlushType::Layout);
|
|
|
|
if (!mPresShell) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Check that parameters are safe
|
|
if (aDirection < 0 || aDirection > 3 || aAmount < 0 || aAmount > 1) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
nsPresContext* context = mPresShell->GetPresContext();
|
|
if (!context) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
RefPtr<Selection> sel = &NormalSelection();
|
|
|
|
// Map the abstract movement amounts (0-1) to direction-specific
|
|
// selection units.
|
|
static const nsSelectionAmount inlineAmount[] = {eSelectCluster, eSelectWord};
|
|
static const nsSelectionAmount blockPrevAmount[] = {eSelectLine,
|
|
eSelectBeginLine};
|
|
static const nsSelectionAmount blockNextAmount[] = {eSelectLine,
|
|
eSelectEndLine};
|
|
|
|
struct PhysicalToLogicalMapping {
|
|
nsDirection direction;
|
|
const nsSelectionAmount* amounts;
|
|
};
|
|
static const PhysicalToLogicalMapping verticalLR[4] = {
|
|
{eDirPrevious, blockPrevAmount}, // left
|
|
{eDirNext, blockNextAmount}, // right
|
|
{eDirPrevious, inlineAmount}, // up
|
|
{eDirNext, inlineAmount} // down
|
|
};
|
|
static const PhysicalToLogicalMapping verticalRL[4] = {
|
|
{eDirNext, blockNextAmount},
|
|
{eDirPrevious, blockPrevAmount},
|
|
{eDirPrevious, inlineAmount},
|
|
{eDirNext, inlineAmount}};
|
|
static const PhysicalToLogicalMapping horizontal[4] = {
|
|
{eDirPrevious, inlineAmount},
|
|
{eDirNext, inlineAmount},
|
|
{eDirPrevious, blockPrevAmount},
|
|
{eDirNext, blockNextAmount}};
|
|
|
|
WritingMode wm;
|
|
const PrimaryFrameData frameForFocus =
|
|
sel->GetPrimaryFrameForCaretAtFocusNode(true);
|
|
if (frameForFocus.mFrame) {
|
|
// FYI: Setting the caret association hint was done during a call of
|
|
// GetPrimaryFrameForCaretAtFocusNode. Therefore, this may not be intended
|
|
// by the original author.
|
|
sel->GetFrameSelection()->SetHint(frameForFocus.mHint);
|
|
|
|
if (!frameForFocus.mFrame->Style()->IsTextCombined()) {
|
|
wm = frameForFocus.mFrame->GetWritingMode();
|
|
} else {
|
|
// Using different direction for horizontal-in-vertical would
|
|
// make it hard to navigate via keyboard. Inherit the moving
|
|
// direction from its parent.
|
|
MOZ_ASSERT(frameForFocus.mFrame->IsTextFrame());
|
|
wm = frameForFocus.mFrame->GetParent()->GetWritingMode();
|
|
MOZ_ASSERT(wm.IsVertical(),
|
|
"Text combined "
|
|
"can only appear in vertical text");
|
|
}
|
|
}
|
|
|
|
const PhysicalToLogicalMapping& mapping =
|
|
wm.IsVertical()
|
|
? wm.IsVerticalLR() ? verticalLR[aDirection] : verticalRL[aDirection]
|
|
: horizontal[aDirection];
|
|
|
|
nsresult rv = MoveCaret(mapping.direction, ExtendSelection(aExtend),
|
|
mapping.amounts[aAmount], eVisual);
|
|
if (NS_FAILED(rv)) {
|
|
// If we tried to do a line move, but couldn't move in the given direction,
|
|
// then we'll "promote" this to a line-edge move instead.
|
|
if (mapping.amounts[aAmount] == eSelectLine) {
|
|
rv = MoveCaret(mapping.direction, ExtendSelection(aExtend),
|
|
mapping.amounts[aAmount + 1], eVisual);
|
|
}
|
|
// And if it was a next-word move that failed (which can happen when
|
|
// eat_space_to_next_word is true, see bug 1153237), then just move forward
|
|
// to the line-edge.
|
|
else if (mapping.amounts[aAmount] == eSelectWord &&
|
|
mapping.direction == eDirNext) {
|
|
rv = MoveCaret(eDirNext, ExtendSelection(aExtend), eSelectEndLine,
|
|
eVisual);
|
|
}
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
nsresult nsFrameSelection::CharacterMove(bool aForward, bool aExtend) {
|
|
return MoveCaret(aForward ? eDirNext : eDirPrevious, ExtendSelection(aExtend),
|
|
eSelectCluster, eUsePrefStyle);
|
|
}
|
|
|
|
nsresult nsFrameSelection::WordMove(bool aForward, bool aExtend) {
|
|
return MoveCaret(aForward ? eDirNext : eDirPrevious, ExtendSelection(aExtend),
|
|
eSelectWord, eUsePrefStyle);
|
|
}
|
|
|
|
nsresult nsFrameSelection::LineMove(bool aForward, bool aExtend) {
|
|
return MoveCaret(aForward ? eDirNext : eDirPrevious, ExtendSelection(aExtend),
|
|
eSelectLine, eUsePrefStyle);
|
|
}
|
|
|
|
nsresult nsFrameSelection::IntraLineMove(bool aForward, bool aExtend) {
|
|
if (aForward) {
|
|
return MoveCaret(eDirNext, ExtendSelection(aExtend), eSelectEndLine,
|
|
eLogical);
|
|
}
|
|
return MoveCaret(eDirPrevious, ExtendSelection(aExtend), eSelectBeginLine,
|
|
eLogical);
|
|
}
|
|
|
|
// static
|
|
template <typename RangeType>
|
|
Result<RefPtr<RangeType>, nsresult>
|
|
nsFrameSelection::CreateRangeExtendedToSomewhere(
|
|
PresShell& aPresShell,
|
|
const mozilla::LimitersAndCaretData& aLimitersAndCaretData,
|
|
const AbstractRange& aRange, nsDirection aRangeDirection,
|
|
nsDirection aExtendDirection, nsSelectionAmount aAmount,
|
|
CaretMovementStyle aMovementStyle) {
|
|
MOZ_ASSERT(aRangeDirection == eDirNext || aRangeDirection == eDirPrevious);
|
|
MOZ_ASSERT(aExtendDirection == eDirNext || aExtendDirection == eDirPrevious);
|
|
MOZ_ASSERT(aAmount == eSelectCharacter || aAmount == eSelectCluster ||
|
|
aAmount == eSelectWord || aAmount == eSelectBeginLine ||
|
|
aAmount == eSelectEndLine);
|
|
MOZ_ASSERT(aMovementStyle == eLogical || aMovementStyle == eVisual ||
|
|
aMovementStyle == eUsePrefStyle);
|
|
|
|
aPresShell.FlushPendingNotifications(FlushType::Layout);
|
|
if (aPresShell.IsDestroying()) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
if (!aRange.IsPositioned()) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
const ForceEditableRegion forceEditableRegion = [&]() {
|
|
if (aRange.GetStartContainer()->IsEditable()) {
|
|
return ForceEditableRegion::Yes;
|
|
}
|
|
const auto* const element = Element::FromNode(aRange.GetStartContainer());
|
|
return element && element->State().HasState(ElementState::READWRITE)
|
|
? ForceEditableRegion::Yes
|
|
: ForceEditableRegion::No;
|
|
}();
|
|
Result<PeekOffsetOptions, nsresult> options =
|
|
CreatePeekOffsetOptionsForCaretMove(
|
|
aLimitersAndCaretData.mIndependentSelectionRootElement,
|
|
forceEditableRegion, ExtendSelection::Yes, aMovementStyle);
|
|
if (MOZ_UNLIKELY(options.isErr())) {
|
|
return options.propagateErr();
|
|
}
|
|
Result<RawRangeBoundary, nsresult> result =
|
|
SelectionMovementUtils::MoveRangeBoundaryToSomewhere(
|
|
aRangeDirection == eDirNext ? aRange.StartRef().AsRaw()
|
|
: aRange.EndRef().AsRaw(),
|
|
aExtendDirection, aLimitersAndCaretData.mCaretAssociationHint,
|
|
aLimitersAndCaretData.mCaretBidiLevel, aAmount, options.unwrap(),
|
|
aLimitersAndCaretData.mAncestorLimiter);
|
|
if (result.isErr()) {
|
|
return result.propagateErr();
|
|
}
|
|
RefPtr<RangeType> range;
|
|
RawRangeBoundary rangeBoundary = result.unwrap();
|
|
if (!rangeBoundary.IsSetAndValid()) {
|
|
return range;
|
|
}
|
|
if (aExtendDirection == eDirPrevious) {
|
|
range = RangeType::Create(rangeBoundary, aRange.EndRef(), IgnoreErrors());
|
|
} else {
|
|
range = RangeType::Create(aRange.StartRef(), rangeBoundary, IgnoreErrors());
|
|
}
|
|
return range;
|
|
}
|
|
|
|
//////////END FRAMESELECTION
|
|
|
|
LazyLogModule gBatchLog("SelectionBatch");
|
|
|
|
void nsFrameSelection::StartBatchChanges(const char* aRequesterFuncName) {
|
|
MOZ_LOG(gBatchLog, LogLevel::Info,
|
|
("%p%snsFrameSelection::StartBatchChanges(%s)", this,
|
|
std::string((mBatching.mCounter + 1) * 2, ' ').c_str(),
|
|
aRequesterFuncName));
|
|
mBatching.mCounter++;
|
|
}
|
|
|
|
void nsFrameSelection::EndBatchChanges(const char* aRequesterFuncName,
|
|
int16_t aReasons) {
|
|
MOZ_LOG(gBatchLog, LogLevel::Info,
|
|
("%p%snsFrameSelection::EndBatchChanges (%s, %s)", this,
|
|
std::string(mBatching.mCounter * 2, ' ').c_str(), aRequesterFuncName,
|
|
SelectionChangeReasonsToCString(aReasons).get()));
|
|
MOZ_ASSERT(mBatching.mCounter > 0, "Bad mBatching.mCounter");
|
|
mBatching.mCounter--;
|
|
|
|
if (mBatching.mCounter == 0) {
|
|
AddChangeReasons(aReasons);
|
|
mCaretMoveAmount = eSelectNoAmount;
|
|
// Be aware, the Selection instance may be destroyed after this call,
|
|
// hence make sure that this instance remains until the end of this call.
|
|
RefPtr frameSelection = this;
|
|
for (auto selectionType : kPresentSelectionTypes) {
|
|
// This returns NS_ERROR_FAILURE if being called for a selection that is
|
|
// not present. We don't care about that here, so we silently ignore it
|
|
// and continue.
|
|
Unused << NotifySelectionListeners(selectionType, IsBatchingEnd::Yes);
|
|
}
|
|
}
|
|
}
|
|
|
|
nsresult nsFrameSelection::NotifySelectionListeners(
|
|
SelectionType aSelectionType, IsBatchingEnd aEndBatching) {
|
|
if (RefPtr<Selection> selection = GetSelection(aSelectionType)) {
|
|
if (aEndBatching == IsBatchingEnd::Yes &&
|
|
!selection->ChangesDuringBatching()) {
|
|
return NS_OK;
|
|
}
|
|
selection->NotifySelectionListeners();
|
|
mCaretMoveAmount = eSelectNoAmount;
|
|
return NS_OK;
|
|
}
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// Start of Table Selection methods
|
|
|
|
static bool IsCell(nsIContent* aContent) {
|
|
return aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
|
|
}
|
|
|
|
// static
|
|
nsITableCellLayout* nsFrameSelection::GetCellLayout(
|
|
const nsIContent* aCellContent) {
|
|
nsITableCellLayout* cellLayoutObject =
|
|
do_QueryFrame(aCellContent->GetPrimaryFrame());
|
|
return cellLayoutObject;
|
|
}
|
|
|
|
nsresult nsFrameSelection::ClearNormalSelection() {
|
|
RefPtr<Selection> selection = &NormalSelection();
|
|
ErrorResult err;
|
|
selection->RemoveAllRanges(err);
|
|
return err.StealNSResult();
|
|
}
|
|
|
|
static nsIContent* GetFirstSelectedContent(const nsRange* aRange) {
|
|
if (!aRange) {
|
|
return nullptr;
|
|
}
|
|
|
|
MOZ_ASSERT(aRange->GetStartContainer(), "Must have start parent!");
|
|
MOZ_ASSERT(aRange->GetStartContainer()->IsElement(), "Unexpected parent");
|
|
|
|
return aRange->GetChildAtStartOffset();
|
|
}
|
|
|
|
// Table selection support.
|
|
// TODO: Separate table methods into a separate nsITableSelection interface
|
|
nsresult nsFrameSelection::HandleTableSelection(nsINode* aParentContent,
|
|
int32_t aContentOffset,
|
|
TableSelectionMode aTarget,
|
|
WidgetMouseEvent* aMouseEvent) {
|
|
RefPtr<Selection> selection = &NormalSelection();
|
|
return mTableSelection.HandleSelection(aParentContent, aContentOffset,
|
|
aTarget, aMouseEvent, mDragState,
|
|
*selection);
|
|
}
|
|
|
|
nsresult nsFrameSelection::TableSelection::HandleSelection(
|
|
nsINode* aParentContent, int32_t aContentOffset, TableSelectionMode aTarget,
|
|
WidgetMouseEvent* aMouseEvent, bool aDragState,
|
|
Selection& aNormalSelection) {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
NS_ENSURE_TRUE(aParentContent, NS_ERROR_NULL_POINTER);
|
|
NS_ENSURE_TRUE(aMouseEvent, NS_ERROR_NULL_POINTER);
|
|
|
|
if (aDragState && mDragSelectingCells &&
|
|
aTarget == TableSelectionMode::Table) {
|
|
// We were selecting cells and user drags mouse in table border or inbetween
|
|
// cells,
|
|
// just do nothing
|
|
return NS_OK;
|
|
}
|
|
|
|
RefPtr<nsIContent> childContent =
|
|
aParentContent->GetChildAt_Deprecated(aContentOffset);
|
|
|
|
// When doing table selection, always set the direction to next so
|
|
// we can be sure that anchorNode's offset always points to the
|
|
// selected cell
|
|
aNormalSelection.SetDirection(eDirNext);
|
|
|
|
// Stack-class to wrap all table selection changes in
|
|
// BeginBatchChanges() / EndBatchChanges()
|
|
SelectionBatcher selectionBatcher(&aNormalSelection, __FUNCTION__);
|
|
|
|
if (aDragState && mDragSelectingCells) {
|
|
return HandleDragSelecting(aTarget, childContent, aMouseEvent,
|
|
aNormalSelection);
|
|
}
|
|
|
|
return HandleMouseUpOrDown(aTarget, aDragState, childContent, aParentContent,
|
|
aContentOffset, aMouseEvent, aNormalSelection);
|
|
}
|
|
|
|
class nsFrameSelection::TableSelection::RowAndColumnRelation {
|
|
public:
|
|
static Result<RowAndColumnRelation, nsresult> Create(
|
|
const nsIContent* aFirst, const nsIContent* aSecond) {
|
|
RowAndColumnRelation result;
|
|
|
|
nsresult errorResult =
|
|
GetCellIndexes(aFirst, result.mFirst.mRow, result.mFirst.mColumn);
|
|
if (NS_FAILED(errorResult)) {
|
|
return Err(errorResult);
|
|
}
|
|
|
|
errorResult =
|
|
GetCellIndexes(aSecond, result.mSecond.mRow, result.mSecond.mColumn);
|
|
if (NS_FAILED(errorResult)) {
|
|
return Err(errorResult);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool IsSameColumn() const { return mFirst.mColumn == mSecond.mColumn; }
|
|
|
|
bool IsSameRow() const { return mFirst.mRow == mSecond.mRow; }
|
|
|
|
private:
|
|
RowAndColumnRelation() = default;
|
|
|
|
struct RowAndColumn {
|
|
int32_t mRow = 0;
|
|
int32_t mColumn = 0;
|
|
};
|
|
|
|
RowAndColumn mFirst;
|
|
RowAndColumn mSecond;
|
|
};
|
|
|
|
nsresult nsFrameSelection::TableSelection::HandleDragSelecting(
|
|
TableSelectionMode aTarget, nsIContent* aChildContent,
|
|
const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
|
|
// We are drag-selecting
|
|
if (aTarget != TableSelectionMode::Table) {
|
|
// If dragging in the same cell as last event, do nothing
|
|
if (mEndSelectedCell == aChildContent) {
|
|
return NS_OK;
|
|
}
|
|
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf(
|
|
" mStartSelectedCell = %p, "
|
|
"mEndSelectedCell = %p, aChildContent = %p "
|
|
"\n",
|
|
mStartSelectedCell.get(), mEndSelectedCell.get(), aChildContent);
|
|
#endif
|
|
// aTarget can be any "cell mode",
|
|
// so we can easily drag-select rows and columns
|
|
// Once we are in row or column mode,
|
|
// we can drift into any cell to stay in that mode
|
|
// even if aTarget = TableSelectionMode::Cell
|
|
|
|
if (mMode == TableSelectionMode::Row ||
|
|
mMode == TableSelectionMode::Column) {
|
|
if (mEndSelectedCell) {
|
|
Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
|
|
RowAndColumnRelation::Create(mEndSelectedCell, aChildContent);
|
|
|
|
if (rowAndColumnRelation.isErr()) {
|
|
return rowAndColumnRelation.unwrapErr();
|
|
}
|
|
|
|
if ((mMode == TableSelectionMode::Row &&
|
|
rowAndColumnRelation.inspect().IsSameRow()) ||
|
|
(mMode == TableSelectionMode::Column &&
|
|
rowAndColumnRelation.inspect().IsSameColumn())) {
|
|
return NS_OK;
|
|
}
|
|
}
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf(" Dragged into a new column or row\n");
|
|
#endif
|
|
// Continue dragging row or column selection
|
|
|
|
return SelectRowOrColumn(aChildContent, aNormalSelection);
|
|
}
|
|
if (mMode == TableSelectionMode::Cell) {
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf("HandleTableSelection: Dragged into a new cell\n");
|
|
#endif
|
|
// Trick for quick selection of rows and columns
|
|
// Hold down shift, then start selecting in one direction
|
|
// If next cell dragged into is in same row, select entire row,
|
|
// if next cell is in same column, select entire column
|
|
if (mStartSelectedCell && aMouseEvent->IsShift()) {
|
|
Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
|
|
RowAndColumnRelation::Create(mStartSelectedCell, aChildContent);
|
|
if (rowAndColumnRelation.isErr()) {
|
|
return rowAndColumnRelation.unwrapErr();
|
|
}
|
|
|
|
if (rowAndColumnRelation.inspect().IsSameRow() ||
|
|
rowAndColumnRelation.inspect().IsSameColumn()) {
|
|
// Force new selection block
|
|
mStartSelectedCell = nullptr;
|
|
aNormalSelection.RemoveAllRanges(IgnoreErrors());
|
|
|
|
if (rowAndColumnRelation.inspect().IsSameRow()) {
|
|
mMode = TableSelectionMode::Row;
|
|
} else {
|
|
mMode = TableSelectionMode::Column;
|
|
}
|
|
|
|
return SelectRowOrColumn(aChildContent, aNormalSelection);
|
|
}
|
|
}
|
|
|
|
// Reselect block of cells to new end location
|
|
const nsCOMPtr<nsIContent> startSelectedCell = mStartSelectedCell;
|
|
return SelectBlockOfCells(startSelectedCell, aChildContent,
|
|
aNormalSelection);
|
|
}
|
|
}
|
|
// Do nothing if dragging in table, but outside a cell
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult nsFrameSelection::TableSelection::HandleMouseUpOrDown(
|
|
TableSelectionMode aTarget, bool aDragState, nsIContent* aChildContent,
|
|
nsINode* aParentContent, int32_t aContentOffset,
|
|
const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
|
|
nsresult result = NS_OK;
|
|
// Not dragging -- mouse event is down or up
|
|
if (aDragState) {
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf("HandleTableSelection: Mouse down event\n");
|
|
#endif
|
|
// Clear cell we stored in mouse-down
|
|
mUnselectCellOnMouseUp = nullptr;
|
|
|
|
if (aTarget == TableSelectionMode::Cell) {
|
|
bool isSelected = false;
|
|
|
|
// Check if we have other selected cells
|
|
nsIContent* previousCellNode =
|
|
GetFirstSelectedContent(GetFirstCellRange(aNormalSelection));
|
|
if (previousCellNode) {
|
|
// We have at least 1 other selected cell
|
|
|
|
// Check if new cell is already selected
|
|
nsIFrame* cellFrame = aChildContent->GetPrimaryFrame();
|
|
if (!cellFrame) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
isSelected = cellFrame->IsSelected();
|
|
} else {
|
|
// No cells selected -- remove non-cell selection
|
|
aNormalSelection.RemoveAllRanges(IgnoreErrors());
|
|
}
|
|
mDragSelectingCells = true; // Signal to start drag-cell-selection
|
|
mMode = aTarget;
|
|
// Set start for new drag-selection block (not appended)
|
|
mStartSelectedCell = aChildContent;
|
|
// The initial block end is same as the start
|
|
mEndSelectedCell = aChildContent;
|
|
|
|
if (isSelected) {
|
|
// Remember this cell to (possibly) unselect it on mouseup
|
|
mUnselectCellOnMouseUp = aChildContent;
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf(
|
|
"HandleTableSelection: Saving "
|
|
"mUnselectCellOnMouseUp\n");
|
|
#endif
|
|
} else {
|
|
// Select an unselected cell
|
|
// but first remove existing selection if not in same table
|
|
if (previousCellNode &&
|
|
!IsInSameTable(previousCellNode, aChildContent)) {
|
|
aNormalSelection.RemoveAllRanges(IgnoreErrors());
|
|
// Reset selection mode that is cleared in RemoveAllRanges
|
|
mMode = aTarget;
|
|
}
|
|
|
|
return ::SelectCellElement(aChildContent, aNormalSelection);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
if (aTarget == TableSelectionMode::Table) {
|
|
// TODO: We currently select entire table when clicked between cells,
|
|
// should we restrict to only around border?
|
|
// *** How do we get location data for cell and click?
|
|
mDragSelectingCells = false;
|
|
mStartSelectedCell = nullptr;
|
|
mEndSelectedCell = nullptr;
|
|
|
|
// Remove existing selection and select the table
|
|
aNormalSelection.RemoveAllRanges(IgnoreErrors());
|
|
return CreateAndAddRange(aParentContent, aContentOffset,
|
|
aNormalSelection);
|
|
}
|
|
if (aTarget == TableSelectionMode::Row ||
|
|
aTarget == TableSelectionMode::Column) {
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf("aTarget == %d\n", aTarget);
|
|
#endif
|
|
|
|
// Start drag-selecting mode so multiple rows/cols can be selected
|
|
// Note: Currently, nsIFrame::GetDataForTableSelection
|
|
// will never call us for row or column selection on mouse down
|
|
mDragSelectingCells = true;
|
|
|
|
// Force new selection block
|
|
mStartSelectedCell = nullptr;
|
|
aNormalSelection.RemoveAllRanges(IgnoreErrors());
|
|
// Always do this AFTER RemoveAllRanges
|
|
mMode = aTarget;
|
|
|
|
return SelectRowOrColumn(aChildContent, aNormalSelection);
|
|
}
|
|
} else {
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf(
|
|
"HandleTableSelection: Mouse UP event. "
|
|
"mDragSelectingCells=%d, "
|
|
"mStartSelectedCell=%p\n",
|
|
mDragSelectingCells, mStartSelectedCell.get());
|
|
#endif
|
|
// First check if we are extending a block selection
|
|
const uint32_t rangeCount = aNormalSelection.RangeCount();
|
|
|
|
if (rangeCount > 0 && aMouseEvent->IsShift() && mAppendStartSelectedCell &&
|
|
mAppendStartSelectedCell != aChildContent) {
|
|
// Shift key is down: append a block selection
|
|
mDragSelectingCells = false;
|
|
|
|
const OwningNonNull<nsIContent> appendStartSelectedCell =
|
|
*mAppendStartSelectedCell;
|
|
return SelectBlockOfCells(appendStartSelectedCell, aChildContent,
|
|
aNormalSelection);
|
|
}
|
|
|
|
if (mDragSelectingCells) {
|
|
mAppendStartSelectedCell = mStartSelectedCell;
|
|
}
|
|
|
|
mDragSelectingCells = false;
|
|
mStartSelectedCell = nullptr;
|
|
mEndSelectedCell = nullptr;
|
|
|
|
// Any other mouseup actions require that Ctrl or Cmd key is pressed
|
|
// else stop table selection mode
|
|
bool doMouseUpAction = false;
|
|
#ifdef XP_MACOSX
|
|
doMouseUpAction = aMouseEvent->IsMeta();
|
|
#else
|
|
doMouseUpAction = aMouseEvent->IsControl();
|
|
#endif
|
|
if (!doMouseUpAction) {
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf(
|
|
"HandleTableSelection: Ending cell selection on mouseup: "
|
|
"mAppendStartSelectedCell=%p\n",
|
|
mAppendStartSelectedCell.get());
|
|
#endif
|
|
return NS_OK;
|
|
}
|
|
// Unselect a cell only if it wasn't
|
|
// just selected on mousedown
|
|
if (aChildContent == mUnselectCellOnMouseUp) {
|
|
// Scan ranges to find the cell to unselect (the selection range to
|
|
// remove)
|
|
// XXXbz it's really weird that this lives outside the loop, so once we
|
|
// find one we keep looking at it even if we find no more cells...
|
|
nsINode* previousCellParent = nullptr;
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf(
|
|
"HandleTableSelection: Unselecting "
|
|
"mUnselectCellOnMouseUp; "
|
|
"rangeCount=%d\n",
|
|
rangeCount);
|
|
#endif
|
|
for (const uint32_t i : IntegerRange(rangeCount)) {
|
|
MOZ_ASSERT(aNormalSelection.RangeCount() == rangeCount);
|
|
// Strong reference, because sometimes we want to remove
|
|
// this range, and then we might be the only owner.
|
|
RefPtr<nsRange> range = aNormalSelection.GetRangeAt(i);
|
|
if (MOZ_UNLIKELY(!range)) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
nsINode* container = range->GetStartContainer();
|
|
if (!container) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
int32_t offset = range->StartOffset();
|
|
// Be sure previous selection is a table cell
|
|
nsIContent* child = range->GetChildAtStartOffset();
|
|
if (child && IsCell(child)) {
|
|
previousCellParent = container;
|
|
}
|
|
|
|
// We're done if we didn't find parent of a previously-selected cell
|
|
if (!previousCellParent) {
|
|
break;
|
|
}
|
|
|
|
if (previousCellParent == aParentContent && offset == aContentOffset) {
|
|
// Cell is already selected
|
|
if (rangeCount == 1) {
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf("HandleTableSelection: Unselecting single selected cell\n");
|
|
#endif
|
|
// This was the only cell selected.
|
|
// Collapse to "normal" selection inside the cell
|
|
mStartSelectedCell = nullptr;
|
|
mEndSelectedCell = nullptr;
|
|
mAppendStartSelectedCell = nullptr;
|
|
// TODO: We need a "Collapse to just before deepest child" routine
|
|
// Even better, should we collapse to just after the LAST deepest
|
|
// child
|
|
// (i.e., at the end of the cell's contents)?
|
|
return aNormalSelection.CollapseInLimiter(aChildContent, 0);
|
|
}
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
printf(
|
|
"HandleTableSelection: Removing cell from multi-cell "
|
|
"selection\n");
|
|
#endif
|
|
// Unselecting the start of previous block
|
|
// XXX What do we use now!
|
|
if (aChildContent == mAppendStartSelectedCell) {
|
|
mAppendStartSelectedCell = nullptr;
|
|
}
|
|
|
|
// Deselect cell by removing its range from selection
|
|
ErrorResult err;
|
|
aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
|
|
*range, err);
|
|
return err.StealNSResult();
|
|
}
|
|
}
|
|
mUnselectCellOnMouseUp = nullptr;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
nsresult nsFrameSelection::TableSelection::SelectBlockOfCells(
|
|
nsIContent* aStartCell, nsIContent* aEndCell, Selection& aNormalSelection) {
|
|
NS_ENSURE_TRUE(aStartCell, NS_ERROR_NULL_POINTER);
|
|
NS_ENSURE_TRUE(aEndCell, NS_ERROR_NULL_POINTER);
|
|
mEndSelectedCell = aEndCell;
|
|
|
|
nsresult result = NS_OK;
|
|
|
|
// If new end cell is in a different table, do nothing
|
|
const RefPtr<const nsIContent> table = IsInSameTable(aStartCell, aEndCell);
|
|
if (!table) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Get starting and ending cells' location in the cellmap
|
|
int32_t startRowIndex, startColIndex, endRowIndex, endColIndex;
|
|
result = GetCellIndexes(aStartCell, startRowIndex, startColIndex);
|
|
if (NS_FAILED(result)) {
|
|
return result;
|
|
}
|
|
result = GetCellIndexes(aEndCell, endRowIndex, endColIndex);
|
|
if (NS_FAILED(result)) {
|
|
return result;
|
|
}
|
|
|
|
if (mDragSelectingCells) {
|
|
// Drag selecting: remove selected cells outside of new block limits
|
|
// TODO: `UnselectCells`'s return value shouldn't be ignored.
|
|
UnselectCells(table, startRowIndex, startColIndex, endRowIndex, endColIndex,
|
|
true, aNormalSelection);
|
|
}
|
|
|
|
// Note that we select block in the direction of user's mouse dragging,
|
|
// which means start cell may be after the end cell in either row or column
|
|
return AddCellsToSelection(table, startRowIndex, startColIndex, endRowIndex,
|
|
endColIndex, aNormalSelection);
|
|
}
|
|
|
|
nsresult nsFrameSelection::TableSelection::UnselectCells(
|
|
const nsIContent* aTableContent, int32_t aStartRowIndex,
|
|
int32_t aStartColumnIndex, int32_t aEndRowIndex, int32_t aEndColumnIndex,
|
|
bool aRemoveOutsideOfCellRange, mozilla::dom::Selection& aNormalSelection) {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
nsTableWrapperFrame* tableFrame =
|
|
do_QueryFrame(aTableContent->GetPrimaryFrame());
|
|
if (!tableFrame) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
int32_t minRowIndex = std::min(aStartRowIndex, aEndRowIndex);
|
|
int32_t maxRowIndex = std::max(aStartRowIndex, aEndRowIndex);
|
|
int32_t minColIndex = std::min(aStartColumnIndex, aEndColumnIndex);
|
|
int32_t maxColIndex = std::max(aStartColumnIndex, aEndColumnIndex);
|
|
|
|
// Strong reference because we sometimes remove the range
|
|
RefPtr<nsRange> range = GetFirstCellRange(aNormalSelection);
|
|
nsIContent* cellNode = GetFirstSelectedContent(range);
|
|
MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
|
|
|
|
int32_t curRowIndex, curColIndex;
|
|
while (cellNode) {
|
|
nsresult result = GetCellIndexes(cellNode, curRowIndex, curColIndex);
|
|
if (NS_FAILED(result)) {
|
|
return result;
|
|
}
|
|
|
|
#ifdef DEBUG_TABLE_SELECTION
|
|
if (!range) printf("RemoveCellsToSelection -- range is null\n");
|
|
#endif
|
|
|
|
if (range) {
|
|
if (aRemoveOutsideOfCellRange) {
|
|
if (curRowIndex < minRowIndex || curRowIndex > maxRowIndex ||
|
|
curColIndex < minColIndex || curColIndex > maxColIndex) {
|
|
aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
|
|
*range, IgnoreErrors());
|
|
// Since we've removed the range, decrement pointer to next range
|
|
mSelectedCellIndex--;
|
|
}
|
|
|
|
} else {
|
|
// Remove cell from selection if it belongs to the given cells range or
|
|
// it is spanned onto the cells range.
|
|
nsTableCellFrame* cellFrame =
|
|
tableFrame->GetCellFrameAt(curRowIndex, curColIndex);
|
|
|
|
uint32_t origRowIndex = cellFrame->RowIndex();
|
|
uint32_t origColIndex = cellFrame->ColIndex();
|
|
uint32_t actualRowSpan =
|
|
tableFrame->GetEffectiveRowSpanAt(origRowIndex, origColIndex);
|
|
uint32_t actualColSpan =
|
|
tableFrame->GetEffectiveColSpanAt(curRowIndex, curColIndex);
|
|
if (origRowIndex <= static_cast<uint32_t>(maxRowIndex) &&
|
|
maxRowIndex >= 0 &&
|
|
origRowIndex + actualRowSpan - 1 >=
|
|
static_cast<uint32_t>(minRowIndex) &&
|
|
origColIndex <= static_cast<uint32_t>(maxColIndex) &&
|
|
maxColIndex >= 0 &&
|
|
origColIndex + actualColSpan - 1 >=
|
|
static_cast<uint32_t>(minColIndex)) {
|
|
aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
|
|
*range, IgnoreErrors());
|
|
// Since we've removed the range, decrement pointer to next range
|
|
mSelectedCellIndex--;
|
|
}
|
|
}
|
|
}
|
|
|
|
range = GetNextCellRange(aNormalSelection);
|
|
cellNode = GetFirstSelectedContent(range);
|
|
MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult SelectCellElement(nsIContent* aCellElement,
|
|
Selection& aNormalSelection) {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
nsIContent* parent = aCellElement->GetParent();
|
|
|
|
// Get child offset
|
|
const int32_t offset = parent->ComputeIndexOf_Deprecated(aCellElement);
|
|
|
|
return CreateAndAddRange(parent, offset, aNormalSelection);
|
|
}
|
|
|
|
static nsresult AddCellsToSelection(const nsIContent* aTableContent,
|
|
int32_t aStartRowIndex,
|
|
int32_t aStartColumnIndex,
|
|
int32_t aEndRowIndex,
|
|
int32_t aEndColumnIndex,
|
|
Selection& aNormalSelection) {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
nsTableWrapperFrame* tableFrame =
|
|
do_QueryFrame(aTableContent->GetPrimaryFrame());
|
|
if (!tableFrame) { // Check that |table| is a table.
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
nsresult result = NS_OK;
|
|
uint32_t row = aStartRowIndex;
|
|
while (true) {
|
|
uint32_t col = aStartColumnIndex;
|
|
while (true) {
|
|
nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(row, col);
|
|
|
|
// Skip cells that are spanned from previous locations or are already
|
|
// selected
|
|
if (cellFrame) {
|
|
uint32_t origRow = cellFrame->RowIndex();
|
|
uint32_t origCol = cellFrame->ColIndex();
|
|
if (origRow == row && origCol == col && !cellFrame->IsSelected()) {
|
|
result = SelectCellElement(cellFrame->GetContent(), aNormalSelection);
|
|
if (NS_FAILED(result)) {
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
// Done when we reach end column
|
|
if (col == static_cast<uint32_t>(aEndColumnIndex)) {
|
|
break;
|
|
}
|
|
|
|
if (aStartColumnIndex < aEndColumnIndex) {
|
|
col++;
|
|
} else {
|
|
col--;
|
|
}
|
|
}
|
|
if (row == static_cast<uint32_t>(aEndRowIndex)) {
|
|
break;
|
|
}
|
|
|
|
if (aStartRowIndex < aEndRowIndex) {
|
|
row++;
|
|
} else {
|
|
row--;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
nsresult nsFrameSelection::RemoveCellsFromSelection(nsIContent* aTable,
|
|
int32_t aStartRowIndex,
|
|
int32_t aStartColumnIndex,
|
|
int32_t aEndRowIndex,
|
|
int32_t aEndColumnIndex) {
|
|
const RefPtr<Selection> selection = &NormalSelection();
|
|
return mTableSelection.UnselectCells(aTable, aStartRowIndex,
|
|
aStartColumnIndex, aEndRowIndex,
|
|
aEndColumnIndex, false, *selection);
|
|
}
|
|
|
|
nsresult nsFrameSelection::RestrictCellsToSelection(nsIContent* aTable,
|
|
int32_t aStartRowIndex,
|
|
int32_t aStartColumnIndex,
|
|
int32_t aEndRowIndex,
|
|
int32_t aEndColumnIndex) {
|
|
const RefPtr<Selection> selection = &NormalSelection();
|
|
return mTableSelection.UnselectCells(aTable, aStartRowIndex,
|
|
aStartColumnIndex, aEndRowIndex,
|
|
aEndColumnIndex, true, *selection);
|
|
}
|
|
|
|
Result<nsFrameSelection::TableSelection::FirstAndLastCell, nsresult>
|
|
nsFrameSelection::TableSelection::FindFirstAndLastCellOfRowOrColumn(
|
|
const nsIContent& aCellContent) const {
|
|
const nsIContent* table = GetParentTable(&aCellContent);
|
|
if (!table) {
|
|
return Err(NS_ERROR_NULL_POINTER);
|
|
}
|
|
|
|
// Get table and cell layout interfaces to access
|
|
// cell data based on cellmap location
|
|
// Frames are not ref counted, so don't use an nsCOMPtr
|
|
nsTableWrapperFrame* tableFrame = do_QueryFrame(table->GetPrimaryFrame());
|
|
if (!tableFrame) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
nsITableCellLayout* cellLayout = GetCellLayout(&aCellContent);
|
|
if (!cellLayout) {
|
|
return Err(NS_ERROR_FAILURE);
|
|
}
|
|
|
|
// Get location of target cell:
|
|
int32_t rowIndex, colIndex;
|
|
nsresult result = cellLayout->GetCellIndexes(rowIndex, colIndex);
|
|
if (NS_FAILED(result)) {
|
|
return Err(result);
|
|
}
|
|
|
|
// Be sure we start at proper beginning
|
|
// (This allows us to select row or col given ANY cell!)
|
|
if (mMode == TableSelectionMode::Row) {
|
|
colIndex = 0;
|
|
}
|
|
if (mMode == TableSelectionMode::Column) {
|
|
rowIndex = 0;
|
|
}
|
|
|
|
FirstAndLastCell firstAndLastCell;
|
|
while (true) {
|
|
// Loop through all cells in column or row to find first and last
|
|
nsCOMPtr<nsIContent> curCellContent =
|
|
tableFrame->GetCellAt(rowIndex, colIndex);
|
|
if (!curCellContent) {
|
|
break;
|
|
}
|
|
|
|
if (!firstAndLastCell.mFirst) {
|
|
firstAndLastCell.mFirst = curCellContent;
|
|
}
|
|
|
|
firstAndLastCell.mLast = std::move(curCellContent);
|
|
|
|
// Move to next cell in cellmap, skipping spanned locations
|
|
if (mMode == TableSelectionMode::Row) {
|
|
colIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
|
|
} else {
|
|
rowIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
|
|
}
|
|
}
|
|
return firstAndLastCell;
|
|
}
|
|
|
|
nsresult nsFrameSelection::TableSelection::SelectRowOrColumn(
|
|
nsIContent* aCellContent, Selection& aNormalSelection) {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
if (!aCellContent) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
Result<FirstAndLastCell, nsresult> firstAndLastCell =
|
|
FindFirstAndLastCellOfRowOrColumn(*aCellContent);
|
|
if (firstAndLastCell.isErr()) {
|
|
return firstAndLastCell.unwrapErr();
|
|
}
|
|
|
|
// Use SelectBlockOfCells:
|
|
// This will replace existing selection,
|
|
// but allow unselecting by dragging out of selected region
|
|
if (firstAndLastCell.inspect().mFirst && firstAndLastCell.inspect().mLast) {
|
|
nsresult rv{NS_OK};
|
|
|
|
if (!mStartSelectedCell) {
|
|
// We are starting a new block, so select the first cell
|
|
rv = ::SelectCellElement(firstAndLastCell.inspect().mFirst,
|
|
aNormalSelection);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
mStartSelectedCell = firstAndLastCell.inspect().mFirst;
|
|
}
|
|
|
|
const nsCOMPtr<nsIContent> startSelectedCell = mStartSelectedCell;
|
|
rv = SelectBlockOfCells(startSelectedCell,
|
|
MOZ_KnownLive(firstAndLastCell.inspect().mLast),
|
|
aNormalSelection);
|
|
|
|
// This gets set to the cell at end of row/col,
|
|
// but we need it to be the cell under cursor
|
|
mEndSelectedCell = aCellContent;
|
|
return rv;
|
|
}
|
|
|
|
#if 0
|
|
// This is a more efficient strategy that appends row to current selection,
|
|
// but doesn't allow dragging OFF of an existing selection to unselect!
|
|
do {
|
|
// Loop through all cells in column or row
|
|
result = tableLayout->GetCellDataAt(rowIndex, colIndex,
|
|
getter_AddRefs(cellElement),
|
|
curRowIndex, curColIndex,
|
|
rowSpan, colSpan,
|
|
actualRowSpan, actualColSpan,
|
|
isSelected);
|
|
if (NS_FAILED(result)) return result;
|
|
// We're done when cell is not found
|
|
if (!cellElement) break;
|
|
|
|
|
|
// Check spans else we infinitely loop
|
|
NS_ASSERTION(actualColSpan, "actualColSpan is 0!");
|
|
NS_ASSERTION(actualRowSpan, "actualRowSpan is 0!");
|
|
|
|
// Skip cells that are already selected or span from outside our region
|
|
if (!isSelected && rowIndex == curRowIndex && colIndex == curColIndex)
|
|
{
|
|
result = SelectCellElement(cellElement);
|
|
if (NS_FAILED(result)) return result;
|
|
}
|
|
// Move to next row or column in cellmap, skipping spanned locations
|
|
if (mMode == TableSelectionMode::Row)
|
|
colIndex += actualColSpan;
|
|
else
|
|
rowIndex += actualRowSpan;
|
|
}
|
|
while (cellElement);
|
|
#endif
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
// static
|
|
nsIContent* nsFrameSelection::GetFirstCellNodeInRange(const nsRange* aRange) {
|
|
if (!aRange) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsIContent* childContent = aRange->GetChildAtStartOffset();
|
|
if (!childContent) {
|
|
return nullptr;
|
|
}
|
|
// Don't return node if not a cell
|
|
if (!IsCell(childContent)) {
|
|
return nullptr;
|
|
}
|
|
|
|
return childContent;
|
|
}
|
|
|
|
nsRange* nsFrameSelection::TableSelection::GetFirstCellRange(
|
|
const mozilla::dom::Selection& aNormalSelection) {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
nsRange* firstRange = aNormalSelection.GetRangeAt(0);
|
|
if (!GetFirstCellNodeInRange(firstRange)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Setup for next cell
|
|
mSelectedCellIndex = 1;
|
|
|
|
return firstRange;
|
|
}
|
|
|
|
nsRange* nsFrameSelection::TableSelection::GetNextCellRange(
|
|
const mozilla::dom::Selection& aNormalSelection) {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
nsRange* range =
|
|
aNormalSelection.GetRangeAt(AssertedCast<uint32_t>(mSelectedCellIndex));
|
|
|
|
// Get first node in next range of selection - test if it's a cell
|
|
if (!GetFirstCellNodeInRange(range)) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Setup for next cell
|
|
mSelectedCellIndex++;
|
|
|
|
return range;
|
|
}
|
|
|
|
// static
|
|
nsresult nsFrameSelection::GetCellIndexes(const nsIContent* aCell,
|
|
int32_t& aRowIndex,
|
|
int32_t& aColIndex) {
|
|
if (!aCell) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
aColIndex = 0; // initialize out params
|
|
aRowIndex = 0;
|
|
|
|
nsITableCellLayout* cellLayoutObject = GetCellLayout(aCell);
|
|
if (!cellLayoutObject) {
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
return cellLayoutObject->GetCellIndexes(aRowIndex, aColIndex);
|
|
}
|
|
|
|
// static
|
|
nsIContent* nsFrameSelection::IsInSameTable(const nsIContent* aContent1,
|
|
const nsIContent* aContent2) {
|
|
if (!aContent1 || !aContent2) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsIContent* tableNode1 = GetParentTable(aContent1);
|
|
nsIContent* tableNode2 = GetParentTable(aContent2);
|
|
|
|
// Must be in the same table. Note that we want to return false for
|
|
// the test if both tables are null.
|
|
return (tableNode1 == tableNode2) ? tableNode1 : nullptr;
|
|
}
|
|
|
|
// static
|
|
nsIContent* nsFrameSelection::GetParentTable(const nsIContent* aCell) {
|
|
if (!aCell) {
|
|
return nullptr;
|
|
}
|
|
|
|
for (nsIContent* parent = aCell->GetParent(); parent;
|
|
parent = parent->GetParent()) {
|
|
if (parent->IsHTMLElement(nsGkAtoms::table)) {
|
|
return parent;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
nsresult nsFrameSelection::SelectCellElement(nsIContent* aCellElement) {
|
|
const RefPtr<Selection> selection = &NormalSelection();
|
|
return ::SelectCellElement(aCellElement, *selection);
|
|
}
|
|
|
|
nsresult CreateAndAddRange(nsINode* aContainer, int32_t aOffset,
|
|
Selection& aNormalSelection) {
|
|
MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
|
|
|
|
if (!aContainer) {
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
// Set range around child at given offset
|
|
ErrorResult error;
|
|
RefPtr<nsRange> range =
|
|
nsRange::Create(aContainer, aOffset, aContainer, aOffset + 1, error);
|
|
if (NS_WARN_IF(error.Failed())) {
|
|
return error.StealNSResult();
|
|
}
|
|
MOZ_ASSERT(range);
|
|
|
|
ErrorResult err;
|
|
aNormalSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, err);
|
|
return err.StealNSResult();
|
|
}
|
|
|
|
// End of Table Selection
|
|
|
|
void nsFrameSelection::SetAncestorLimiter(Element* aLimiter) {
|
|
if (mLimiters.mAncestorLimiter != aLimiter) {
|
|
mLimiters.mAncestorLimiter = aLimiter;
|
|
const Selection& sel = NormalSelection();
|
|
LogSelectionAPI(&sel, __FUNCTION__, "aLimiter", aLimiter);
|
|
|
|
if (!NodeIsInLimiters(sel.GetFocusNode())) {
|
|
ClearNormalSelection();
|
|
if (mLimiters.mAncestorLimiter) {
|
|
SetChangeReasons(nsISelectionListener::NO_REASON);
|
|
nsCOMPtr<nsIContent> limiter(mLimiters.mAncestorLimiter);
|
|
const nsresult rv =
|
|
TakeFocus(*limiter, 0, 0, CaretAssociationHint::Before,
|
|
FocusMode::kCollapseToNewPoint);
|
|
Unused << NS_WARN_IF(NS_FAILED(rv));
|
|
// TODO: in case of failure, propagate it to the callers.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void nsFrameSelection::SetDelayedCaretData(WidgetMouseEvent* aMouseEvent) {
|
|
if (aMouseEvent) {
|
|
mDelayedMouseEvent.mIsValid = true;
|
|
mDelayedMouseEvent.mIsShift = aMouseEvent->IsShift();
|
|
mDelayedMouseEvent.mClickCount = aMouseEvent->mClickCount;
|
|
} else {
|
|
mDelayedMouseEvent.mIsValid = false;
|
|
}
|
|
}
|
|
|
|
void nsFrameSelection::DisconnectFromPresShell() {
|
|
if (mAccessibleCaretEnabled) {
|
|
Selection& sel = NormalSelection();
|
|
sel.StopNotifyingAccessibleCaretEventHub();
|
|
}
|
|
|
|
StopAutoScrollTimer();
|
|
for (size_t i = 0; i < std::size(mDomSelections); i++) {
|
|
MOZ_ASSERT(mDomSelections[i]);
|
|
mDomSelections[i]->Clear(nullptr);
|
|
}
|
|
|
|
if (auto* presshell = mPresShell) {
|
|
if (const nsFrameSelection* sel = presshell->GetLastSelectionForToString();
|
|
sel == this) {
|
|
presshell->UpdateLastSelectionForToString(nullptr);
|
|
}
|
|
mPresShell = nullptr;
|
|
}
|
|
}
|
|
|
|
#ifdef XP_MACOSX
|
|
/**
|
|
* See Bug 1288453.
|
|
*
|
|
* Update the selection cache on repaint to handle when a pre-existing
|
|
* selection becomes active aka the current selection.
|
|
*
|
|
* 1. Change the current selection by click n dragging another selection.
|
|
* - Make a selection on content page. Make a selection in a text editor.
|
|
* - You can click n drag the content selection to make it active again.
|
|
* 2. Change the current selection when switching to a tab with a selection.
|
|
* - Make selection in tab.
|
|
* - Switching tabs will make its respective selection active.
|
|
*
|
|
* Therefore, we only update the selection cache on a repaint
|
|
* if the current selection being repainted is not an empty selection.
|
|
*
|
|
* If the current selection is empty. The current selection cache
|
|
* would be cleared by AutoCopyListener::OnSelectionChange().
|
|
*/
|
|
static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel) {
|
|
PresShell* presShell = aSel->GetPresShell();
|
|
if (!presShell) {
|
|
return NS_OK;
|
|
}
|
|
nsCOMPtr<Document> aDoc = presShell->GetDocument();
|
|
|
|
if (aDoc && aSel && !aSel->IsCollapsed()) {
|
|
return nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
|
|
aSel, aDoc, nsIClipboard::kSelectionCache, false);
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
#endif // XP_MACOSX
|
|
|
|
// mozilla::AutoCopyListener
|
|
|
|
/*
|
|
* What we do now:
|
|
* On every selection change, we copy to the clipboard anew, creating a
|
|
* HTML buffer, a transferable, an nsISupportsString and
|
|
* a huge mess every time. This is basically what
|
|
* nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() does to move the
|
|
* selection into the clipboard for Edit->Copy.
|
|
*
|
|
* What we should do, to make our end of the deal faster:
|
|
* Create a singleton transferable with our own magic converter. When selection
|
|
* changes (use a quick cache to detect ``real'' changes), we put the new
|
|
* Selection in the transferable. Our magic converter will take care of
|
|
* transferable->whatever-other-format when the time comes to actually
|
|
* hand over the clipboard contents.
|
|
*
|
|
* Other issues:
|
|
* - which X clipboard should we populate?
|
|
* - should we use a different one than Edit->Copy, so that inadvertant
|
|
* selections (or simple clicks, which currently cause a selection
|
|
* notification, regardless of if they're in the document which currently has
|
|
* selection!) don't lose the contents of the ``application''? Or should we
|
|
* just put some intelligence in the ``is this a real selection?'' code to
|
|
* protect our selection against clicks in other documents that don't create
|
|
* selections?
|
|
* - maybe we should just never clear the X clipboard? That would make this
|
|
* problem just go away, which is very tempting.
|
|
*
|
|
* On macOS,
|
|
* nsIClipboard::kSelectionCache is the flag for current selection cache.
|
|
* Set the current selection cache on the parent process in
|
|
* widget cocoa nsClipboard whenever selection changes.
|
|
*/
|
|
|
|
// static
|
|
void AutoCopyListener::OnSelectionChange(Document* aDocument,
|
|
Selection& aSelection,
|
|
int16_t aReason) {
|
|
MOZ_ASSERT(IsEnabled());
|
|
|
|
// For now, we should prevent any updates caused by a call of Selection API.
|
|
// We should allow this in some cases later, though. See the valid usage in
|
|
// bug 1567160.
|
|
if (aReason & nsISelectionListener::JS_REASON) {
|
|
return;
|
|
}
|
|
|
|
if (sClipboardID == nsIClipboard::kSelectionCache) {
|
|
// Do nothing if this isn't in the active window and,
|
|
// in the case of Web content, in the frontmost tab.
|
|
if (!aDocument || !IsInActiveTab(aDocument)) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
static const int16_t kResasonsToHandle =
|
|
nsISelectionListener::MOUSEUP_REASON |
|
|
nsISelectionListener::SELECTALL_REASON |
|
|
nsISelectionListener::KEYPRESS_REASON;
|
|
if (!(aReason & kResasonsToHandle)) {
|
|
return; // Don't care if we are still dragging.
|
|
}
|
|
|
|
if (!aDocument ||
|
|
aSelection.AreNormalAndCrossShadowBoundaryRangesCollapsed()) {
|
|
#ifdef DEBUG_CLIPBOARD
|
|
fprintf(stderr, "CLIPBOARD: no selection/collapsed selection\n");
|
|
#endif
|
|
if (sClipboardID != nsIClipboard::kSelectionCache) {
|
|
// XXX Should we clear X clipboard?
|
|
return;
|
|
}
|
|
|
|
// If on macOS, clear the current selection transferable cached
|
|
// on the parent process (nsClipboard) when the selection is empty.
|
|
DebugOnly<nsresult> rv = nsCopySupport::ClearSelectionCache();
|
|
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
|
|
"nsCopySupport::ClearSelectionCache() failed");
|
|
return;
|
|
}
|
|
|
|
DebugOnly<nsresult> rv =
|
|
nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
|
|
&aSelection, aDocument, sClipboardID, false);
|
|
NS_WARNING_ASSERTION(
|
|
NS_SUCCEEDED(rv),
|
|
"nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() failed");
|
|
}
|