Skip to content

Commit 97eef6a

Browse files
Make _SelectableRegionSelectionContainerDelegate public (#147080)
This change makes `_SelectableRegionContainerDelegate` public so it can be reused and extended by users of `SelectionContainer`. Extending `MultiSelectableRegionContainerDelegate` does not by default provide selection managing across multiple selectables, so often users will copy the implementation found in `_SelectableRegionContainerDelegate`. `_SelectableRegionContainerDelegate` -> `StaticSelectionContainerDelegate`.
1 parent 8461cee commit 97eef6a

File tree

3 files changed

+232
-212
lines changed

3 files changed

+232
-212
lines changed

packages/flutter/lib/src/widgets/selectable_region.dart

Lines changed: 155 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ class SelectableRegionState extends State<SelectableRegion> with TextSelectionDe
351351
final LayerLink _startHandleLayerLink = LayerLink();
352352
final LayerLink _endHandleLayerLink = LayerLink();
353353
final LayerLink _toolbarLayerLink = LayerLink();
354-
final _SelectableRegionContainerDelegate _selectionDelegate = _SelectableRegionContainerDelegate();
354+
final StaticSelectionContainerDelegate _selectionDelegate = StaticSelectionContainerDelegate();
355355
// there should only ever be one selectable, which is the SelectionContainer.
356356
Selectable? _selectable;
357357

@@ -1823,129 +1823,233 @@ class _DirectionallyExtendCaretSelectionAction<T extends DirectionalCaretMovemen
18231823
}
18241824
}
18251825

1826-
class _SelectableRegionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
1826+
/// A delegate that manages updating multiple [Selectable] children where the
1827+
/// [Selectable]s do not change or move around frequently.
1828+
///
1829+
/// This delegate keeps track of the [Selectable]s that received start or end
1830+
/// [SelectionEvent]s and the global locations of those events to accurately
1831+
/// synthesize [SelectionEvent]s for children [Selectable]s when needed.
1832+
///
1833+
/// When a new [SelectionEdgeUpdateEvent] is dispatched to a [Selectable], this
1834+
/// delegate checks whether the [Selectable] has already received a selection
1835+
/// update for each edge that currently exists, and synthesizes an event for the
1836+
/// edges that have not yet received an update. This synthesized event is dispatched
1837+
/// before dispatching the new event.
1838+
///
1839+
/// For example, if we have an existing start edge for this delegate and a [Selectable]
1840+
/// child receives an end [SelectionEdgeUpdateEvent] and the child hasn't received a start
1841+
/// [SelectionEdgeUpdateEvent], we synthesize a start [SelectionEdgeUpdateEvent] for the
1842+
/// child [Selectable] and dispatch it before dispatching the original end [SelectionEdgeUpdateEvent].
1843+
///
1844+
/// See also:
1845+
///
1846+
/// * [MultiSelectableSelectionContainerDelegate], for the class that provides
1847+
/// the main implementation details of this [SelectionContainerDelegate].
1848+
class StaticSelectionContainerDelegate extends MultiSelectableSelectionContainerDelegate {
1849+
/// The set of [Selectable]s that have received start events.
18271850
final Set<Selectable> _hasReceivedStartEvent = <Selectable>{};
1851+
1852+
/// The set of [Selectable]s that have received end events.
18281853
final Set<Selectable> _hasReceivedEndEvent = <Selectable>{};
18291854

1855+
/// The global position of the last selection start edge update.
18301856
Offset? _lastStartEdgeUpdateGlobalPosition;
1857+
1858+
/// The global position of the last selection end edge update.
18311859
Offset? _lastEndEdgeUpdateGlobalPosition;
18321860

1833-
@override
1834-
void remove(Selectable selectable) {
1835-
_hasReceivedStartEvent.remove(selectable);
1836-
_hasReceivedEndEvent.remove(selectable);
1837-
super.remove(selectable);
1861+
/// Tracks whether a selection edge update event for a given [Selectable] was received.
1862+
///
1863+
/// When `forEnd` is true, the [Selectable] will be registered as having received
1864+
/// an end event. When false, the [Selectable] is registered as having received
1865+
/// a start event.
1866+
///
1867+
/// When `forEnd` is null, the [Selectable] will be registered as having received both
1868+
/// start and end events.
1869+
///
1870+
/// Call this method when a [SelectionEvent] is dispatched to a child selectable managed
1871+
/// by this delegate.
1872+
///
1873+
/// Subclasses should call [clearInternalSelectionStateForSelectable] to clean up any state
1874+
/// added by this method, for example when removing a [Selectable] from this delegate.
1875+
@protected
1876+
void didReceiveSelectionEventFor({required Selectable selectable, bool? forEnd}) {
1877+
switch (forEnd) {
1878+
case true:
1879+
_hasReceivedEndEvent.add(selectable);
1880+
case false:
1881+
_hasReceivedStartEvent.add(selectable);
1882+
case null:
1883+
_hasReceivedStartEvent.add(selectable);
1884+
_hasReceivedEndEvent.add(selectable);
1885+
}
18381886
}
18391887

1840-
void _updateLastEdgeEventsFromGeometries() {
1888+
/// Updates the internal selection state after a [SelectionEvent] that
1889+
/// selects a boundary such as: [SelectWordSelectionEvent],
1890+
/// [SelectParagraphSelectionEvent], and [SelectAllSelectionEvent].
1891+
///
1892+
/// Call this method after determining the new selection as a result of
1893+
/// a [SelectionEvent] that selects a boundary. The [currentSelectionStartIndex]
1894+
/// and [currentSelectionEndIndex] should be set to valid values at the time
1895+
/// this method is called.
1896+
///
1897+
/// Subclasses should call [clearInternalSelectionStateForSelectable] to clean up any state
1898+
/// added by this method, for example when removing a [Selectable] from this delegate.
1899+
@protected
1900+
void didReceiveSelectionBoundaryEvents() {
1901+
if (currentSelectionStartIndex == -1 || currentSelectionEndIndex == -1) {
1902+
return;
1903+
}
1904+
final int start = min(currentSelectionStartIndex, currentSelectionEndIndex);
1905+
final int end = max(currentSelectionStartIndex, currentSelectionEndIndex);
1906+
for (int index = start; index <= end; index += 1) {
1907+
didReceiveSelectionEventFor(selectable: selectables[index]);
1908+
}
1909+
_updateLastSelectionEdgeLocationsFromGeometries();
1910+
}
1911+
1912+
/// Updates the last selection edge location of the edge specified by `forEnd`
1913+
/// to the provided `globalSelectionEdgeLocation`.
1914+
@protected
1915+
void updateLastSelectionEdgeLocation({required Offset globalSelectionEdgeLocation, required bool forEnd}) {
1916+
if (forEnd) {
1917+
_lastEndEdgeUpdateGlobalPosition = globalSelectionEdgeLocation;
1918+
} else {
1919+
_lastStartEdgeUpdateGlobalPosition = globalSelectionEdgeLocation;
1920+
}
1921+
}
1922+
1923+
/// Updates the last selection edge locations of both start and end selection
1924+
/// edges based on their [SelectionGeometry].
1925+
void _updateLastSelectionEdgeLocationsFromGeometries() {
18411926
if (currentSelectionStartIndex != -1 && selectables[currentSelectionStartIndex].value.hasSelection) {
18421927
final Selectable start = selectables[currentSelectionStartIndex];
18431928
final Offset localStartEdge = start.value.startSelectionPoint!.localPosition +
18441929
Offset(0, - start.value.startSelectionPoint!.lineHeight / 2);
1845-
_lastStartEdgeUpdateGlobalPosition = MatrixUtils.transformPoint(start.getTransformTo(null), localStartEdge);
1930+
updateLastSelectionEdgeLocation(
1931+
globalSelectionEdgeLocation: MatrixUtils.transformPoint(start.getTransformTo(null), localStartEdge),
1932+
forEnd: false,
1933+
);
18461934
}
18471935
if (currentSelectionEndIndex != -1 && selectables[currentSelectionEndIndex].value.hasSelection) {
18481936
final Selectable end = selectables[currentSelectionEndIndex];
18491937
final Offset localEndEdge = end.value.endSelectionPoint!.localPosition +
18501938
Offset(0, -end.value.endSelectionPoint!.lineHeight / 2);
1851-
_lastEndEdgeUpdateGlobalPosition = MatrixUtils.transformPoint(end.getTransformTo(null), localEndEdge);
1939+
updateLastSelectionEdgeLocation(
1940+
globalSelectionEdgeLocation: MatrixUtils.transformPoint(end.getTransformTo(null), localEndEdge),
1941+
forEnd: true,
1942+
);
18521943
}
18531944
}
18541945

1946+
/// Clears the internal selection state.
1947+
///
1948+
/// This indicates that no [Selectable] child under this delegate
1949+
/// has received start or end events, and resets any tracked global
1950+
/// locations for start and end [SelectionEdgeUpdateEvent]s.
1951+
@protected
1952+
void clearInternalSelectionState() {
1953+
selectables.forEach(clearInternalSelectionStateForSelectable);
1954+
_lastStartEdgeUpdateGlobalPosition = null;
1955+
_lastEndEdgeUpdateGlobalPosition = null;
1956+
}
1957+
1958+
/// Clears the internal selection state for a given [Selectable].
1959+
///
1960+
/// This indicates that the given `selectable` has neither received a
1961+
/// start or end [SelectionEdgeUpdateEvent]s.
1962+
///
1963+
/// Subclasses should call this method to clean up state added in
1964+
/// [didReceiveSelectionEventFor] and [didReceiveSelectionBoundaryEvents].
1965+
@protected
1966+
void clearInternalSelectionStateForSelectable(Selectable selectable) {
1967+
_hasReceivedStartEvent.remove(selectable);
1968+
_hasReceivedEndEvent.remove(selectable);
1969+
}
1970+
1971+
@override
1972+
void remove(Selectable selectable) {
1973+
clearInternalSelectionStateForSelectable(selectable);
1974+
super.remove(selectable);
1975+
}
1976+
18551977
@override
18561978
SelectionResult handleSelectAll(SelectAllSelectionEvent event) {
18571979
final SelectionResult result = super.handleSelectAll(event);
1858-
for (final Selectable selectable in selectables) {
1859-
_hasReceivedStartEvent.add(selectable);
1860-
_hasReceivedEndEvent.add(selectable);
1861-
}
1862-
// Synthesize last update event so the edge updates continue to work.
1863-
_updateLastEdgeEventsFromGeometries();
1980+
didReceiveSelectionBoundaryEvents();
18641981
return result;
18651982
}
18661983

1867-
/// Selects a word in a [Selectable] at the location
1868-
/// [SelectWordSelectionEvent.globalPosition].
18691984
@override
18701985
SelectionResult handleSelectWord(SelectWordSelectionEvent event) {
18711986
final SelectionResult result = super.handleSelectWord(event);
1872-
if (currentSelectionStartIndex != -1) {
1873-
_hasReceivedStartEvent.add(selectables[currentSelectionStartIndex]);
1874-
}
1875-
if (currentSelectionEndIndex != -1) {
1876-
_hasReceivedEndEvent.add(selectables[currentSelectionEndIndex]);
1877-
}
1878-
_updateLastEdgeEventsFromGeometries();
1987+
didReceiveSelectionBoundaryEvents();
18791988
return result;
18801989
}
18811990

1882-
/// Selects a paragraph in a [Selectable] at the location
1883-
/// [SelectParagraphSelectionEvent.globalPosition].
18841991
@override
18851992
SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
18861993
final SelectionResult result = super.handleSelectParagraph(event);
1887-
if (currentSelectionStartIndex != -1) {
1888-
_hasReceivedStartEvent.add(selectables[currentSelectionStartIndex]);
1889-
}
1890-
if (currentSelectionEndIndex != -1) {
1891-
_hasReceivedEndEvent.add(selectables[currentSelectionEndIndex]);
1892-
}
1893-
_updateLastEdgeEventsFromGeometries();
1994+
didReceiveSelectionBoundaryEvents();
18941995
return result;
18951996
}
18961997

18971998
@override
18981999
SelectionResult handleClearSelection(ClearSelectionEvent event) {
18992000
final SelectionResult result = super.handleClearSelection(event);
1900-
_hasReceivedStartEvent.clear();
1901-
_hasReceivedEndEvent.clear();
1902-
_lastStartEdgeUpdateGlobalPosition = null;
1903-
_lastEndEdgeUpdateGlobalPosition = null;
2001+
clearInternalSelectionState();
19042002
return result;
19052003
}
19062004

19072005
@override
19082006
SelectionResult handleSelectionEdgeUpdate(SelectionEdgeUpdateEvent event) {
1909-
if (event.type == SelectionEventType.endEdgeUpdate) {
1910-
_lastEndEdgeUpdateGlobalPosition = event.globalPosition;
1911-
} else {
1912-
_lastStartEdgeUpdateGlobalPosition = event.globalPosition;
1913-
}
2007+
updateLastSelectionEdgeLocation(
2008+
globalSelectionEdgeLocation: event.globalPosition,
2009+
forEnd: event.type == SelectionEventType.endEdgeUpdate,
2010+
);
19142011
return super.handleSelectionEdgeUpdate(event);
19152012
}
19162013

19172014
@override
19182015
void dispose() {
1919-
_hasReceivedStartEvent.clear();
1920-
_hasReceivedEndEvent.clear();
2016+
clearInternalSelectionState();
19212017
super.dispose();
19222018
}
19232019

19242020
@override
19252021
SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
19262022
switch (event.type) {
19272023
case SelectionEventType.startEdgeUpdate:
1928-
_hasReceivedStartEvent.add(selectable);
2024+
didReceiveSelectionEventFor(selectable: selectable, forEnd: false);
19292025
ensureChildUpdated(selectable);
19302026
case SelectionEventType.endEdgeUpdate:
1931-
_hasReceivedEndEvent.add(selectable);
2027+
didReceiveSelectionEventFor(selectable: selectable, forEnd: true);
19322028
ensureChildUpdated(selectable);
19332029
case SelectionEventType.clear:
1934-
_hasReceivedStartEvent.remove(selectable);
1935-
_hasReceivedEndEvent.remove(selectable);
2030+
clearInternalSelectionStateForSelectable(selectable);
19362031
case SelectionEventType.selectAll:
19372032
case SelectionEventType.selectWord:
19382033
case SelectionEventType.selectParagraph:
19392034
break;
19402035
case SelectionEventType.granularlyExtendSelection:
19412036
case SelectionEventType.directionallyExtendSelection:
1942-
_hasReceivedStartEvent.add(selectable);
1943-
_hasReceivedEndEvent.add(selectable);
2037+
didReceiveSelectionEventFor(selectable: selectable);
19442038
ensureChildUpdated(selectable);
19452039
}
19462040
return super.dispatchSelectionEventToChild(selectable, event);
19472041
}
19482042

2043+
/// Ensures the `selectable` child has received the most up to date selection events.
2044+
///
2045+
/// This method is called when:
2046+
/// 1. A new [Selectable] is added to the delegate, and its screen location
2047+
/// falls into the previous selection.
2048+
/// 2. Before a [SelectionEvent] of type
2049+
/// [SelectionEventType.startEdgeUpdate], [SelectionEventType.endEdgeUpdate],
2050+
/// [SelectionEventType.granularlyExtendSelection], or
2051+
/// [SelectionEventType.directionallyExtendSelection] is dispatched
2052+
/// to a [Selectable] child.
19492053
@override
19502054
void ensureChildUpdated(Selectable selectable) {
19512055
if (_lastEndEdgeUpdateGlobalPosition != null && _hasReceivedEndEvent.add(selectable)) {

0 commit comments

Comments
 (0)