[go: nahoru, domu]

blob: 77c3d77bd01224baf3dda06816842dc42921df3a [file] [log] [blame]
Qingqing Deng6f56a912019-05-13 10:10:37 -07001/*
2 * Copyright 2019 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
Siyamed Sinirfd8bc422019-11-21 18:23:58 -080017package androidx.ui.core.selection
Qingqing Deng6f56a912019-05-13 10:10:37 -070018
Qingqing Deng6f56a912019-05-13 10:10:37 -070019import androidx.ui.core.LayoutCoordinates
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070020import androidx.ui.core.clipboard.ClipboardManager
Qingqing Deng6f56a912019-05-13 10:10:37 -070021import androidx.ui.core.gesture.DragObserver
Qingqing Dengbf370e72019-10-17 11:14:39 -070022import androidx.ui.core.gesture.LongPressDragObserver
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080023import androidx.ui.core.hapticfeedback.HapticFeedback
24import androidx.ui.core.hapticfeedback.HapticFeedbackType
Qingqing Deng648e1bf2019-12-30 11:49:48 -080025import androidx.ui.text.AnnotatedString
26import androidx.ui.text.length
27import androidx.ui.text.subSequence
George Mount842c8c12020-01-08 16:03:42 -080028import androidx.ui.unit.PxPosition
29import androidx.ui.unit.px
Qingqing Deng6f56a912019-05-13 10:10:37 -070030
Qingqing Deng35f97ea2019-09-18 19:24:37 -070031/**
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +010032 * A bridge class between user interaction to the text composables for text selection.
Qingqing Deng35f97ea2019-09-18 19:24:37 -070033 */
Siyamed Sinir700df8452019-10-22 20:23:58 -070034internal class SelectionManager(private val selectionRegistrar: SelectionRegistrarImpl) {
Qingqing Deng6f56a912019-05-13 10:10:37 -070035 /**
36 * The current selection.
37 */
38 var selection: Selection? = null
39
40 /**
41 * The manager will invoke this every time it comes to the conclusion that the selection should
42 * change. The expectation is that this callback will end up causing `setSelection` to get
43 * called. This is what makes this a "controlled component".
44 */
45 var onSelectionChange: (Selection?) -> Unit = {}
46
47 /**
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080048 * [HapticFeedback] handle to perform haptic feedback.
49 */
50 var hapticFeedBack: HapticFeedback? = null
51
52 /**
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -070053 * [ClipboardManager] to perform clipboard features.
54 */
55 var clipboardManager: ClipboardManager? = null
56
57 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070058 * Layout Coordinates of the selection container.
59 */
60 lateinit var containerLayoutCoordinates: LayoutCoordinates
61
62 /**
Qingqing Deng6f56a912019-05-13 10:10:37 -070063 * The beginning position of the drag gesture. Every time a new drag gesture starts, it wil be
64 * recalculated.
65 */
66 private var dragBeginPosition = PxPosition.Origin
67
68 /**
69 * The total distance being dragged of the drag gesture. Every time a new drag gesture starts,
70 * it will be zeroed out.
71 */
72 private var dragTotalDistance = PxPosition.Origin
73
74 /**
Qingqing Deng0cb86fe2019-07-16 15:36:27 -070075 * A flag to check if the selection start or end handle is being dragged.
76 * If this value is true, then onPress will not select any text.
77 * This value will be set to true when either handle is being dragged, and be reset to false
78 * when the dragging is stopped.
79 */
80 private var draggingHandle = false
81
82 /**
Siyamed Sinir472c3162019-10-21 23:41:00 -070083 * Iterates over the handlers, gets the selection for each Composable, and merges all the
84 * returned [Selection]s.
85 *
86 * @param startPosition [PxPosition] for the start of the selection
87 * @param endPosition [PxPosition] for the end of the selection
Siyamed Sinir0100f122019-11-16 00:23:12 -080088 * @param longPress the selection is a result of long press
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080089 * @param previousSelection previous selection
Siyamed Sinir472c3162019-10-21 23:41:00 -070090 *
91 * @return [Selection] object which is constructed by combining all Composables that are
92 * selected.
93 */
Qingqing Deng5819ff62019-11-18 15:26:23 -080094 // This function is internal for testing purposes.
95 internal fun mergeSelections(
Siyamed Sinir472c3162019-10-21 23:41:00 -070096 startPosition: PxPosition,
97 endPosition: PxPosition,
Siyamed Sinir0100f122019-11-16 00:23:12 -080098 longPress: Boolean = false,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -080099 previousSelection: Selection? = null,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800100 isStartHandle: Boolean = true
Siyamed Sinir472c3162019-10-21 23:41:00 -0700101 ): Selection? {
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800102
Qingqing Deng247f2b42019-12-12 19:48:37 -0800103 val newSelection = selectionRegistrar.sort(containerLayoutCoordinates)
104 .fold(null) { mergedSelection: Selection?,
105 handler: Selectable ->
106 merge(
107 mergedSelection,
108 handler.getSelection(
109 startPosition = startPosition,
110 endPosition = endPosition,
111 containerLayoutCoordinates = containerLayoutCoordinates,
112 longPress = longPress,
113 previousSelection = previousSelection,
114 isStartHandle = isStartHandle
115 )
Siyamed Sinir700df8452019-10-22 20:23:58 -0700116 )
Qingqing Deng247f2b42019-12-12 19:48:37 -0800117 }
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800118 if (previousSelection != newSelection) hapticFeedBack?.performHapticFeedback(
119 HapticFeedbackType.TextHandleMove
120 )
121 return newSelection
Siyamed Sinir472c3162019-10-21 23:41:00 -0700122 }
123
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800124 internal fun getSelectedText(): AnnotatedString? {
125 val selectables = selectionRegistrar.sort(containerLayoutCoordinates)
126 var selectedText: AnnotatedString? = null
127
128 selection?.let {
129 for (handler in selectables) {
130 // Continue if the current selectable is before the selection starts.
131 if (handler != it.start.selectable && handler != it.end.selectable &&
132 selectedText == null
133 ) continue
134
135 val currentSelectedText = getCurrentSelectedText(
136 selectable = handler,
137 selection = it
138 )
139 selectedText = selectedText?.plus(currentSelectedText) ?: currentSelectedText
140
141 // Break if the current selectable is the last selected selectable.
142 if (handler == it.end.selectable && !it.handlesCrossed ||
143 handler == it.start.selectable && it.handlesCrossed
144 ) break
145 }
146 }
147 return selectedText
148 }
149
Qingqing Dengf2d0a2d2020-03-12 14:19:50 -0700150 internal fun copy() {
151 val selectedText = getSelectedText()
152 selectedText?.let { clipboardManager?.setText(it) }
153 }
154
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800155 // This is for PressGestureDetector to cancel the selection.
156 fun onRelease() {
157 // Call mergeSelections with an out of boundary input to inform all text widgets to
158 // cancel their individual selection.
Qingqing Denga5d80952019-10-11 16:46:52 -0700159 mergeSelections(
Siyamed Sinir0100f122019-11-16 00:23:12 -0800160 startPosition = PxPosition((-1).px, (-1).px),
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800161 endPosition = PxPosition((-1).px, (-1).px),
162 previousSelection = selection
Siyamed Sinir0100f122019-11-16 00:23:12 -0800163 )
Siyamed Sinire810eab2019-11-22 12:36:38 -0800164 if (selection != null) onSelectionChange(null)
Qingqing Dengb6f5d8a2019-11-11 18:19:22 -0800165 }
166
Siyamed Sinire810eab2019-11-22 12:36:38 -0800167 val longPressDragObserver = object : LongPressDragObserver {
168 override fun onLongPress(pxPosition: PxPosition) {
169 if (draggingHandle) return
170 val newSelection = mergeSelections(
171 startPosition = pxPosition,
172 endPosition = pxPosition,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800173 longPress = true,
174 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800175 )
176 if (newSelection != selection) onSelectionChange(newSelection)
177 dragBeginPosition = pxPosition
Siyamed Sinir700df8452019-10-22 20:23:58 -0700178 }
Siyamed Sinir700df8452019-10-22 20:23:58 -0700179
Siyamed Sinire810eab2019-11-22 12:36:38 -0800180 override fun onDragStart() {
181 super.onDragStart()
182 // selection never started
183 if (selection == null) return
184 // Zero out the total distance that being dragged.
185 dragTotalDistance = PxPosition.Origin
186 }
187
188 override fun onDrag(dragDistance: PxPosition): PxPosition {
189 // selection never started, did not consume any drag
190 if (selection == null) return PxPosition.Origin
191
192 dragTotalDistance += dragDistance
193 val newSelection = mergeSelections(
194 startPosition = dragBeginPosition,
195 endPosition = dragBeginPosition + dragTotalDistance,
Qingqing Deng74baaa22019-12-12 13:28:25 -0800196 longPress = true,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800197 previousSelection = selection
Siyamed Sinire810eab2019-11-22 12:36:38 -0800198 )
199
200 if (newSelection != selection) onSelectionChange(newSelection)
201 return dragDistance
202 }
203 }
204
Qingqing Deng13743f72019-07-15 15:00:45 -0700205 /**
206 * Adjust coordinates for given text offset.
207 *
208 * Currently [android.text.Layout.getLineBottom] returns y coordinates of the next
209 * line's top offset, which is not included in current line's hit area. To be able to
210 * hit current line, move up this y coordinates by 1 pixel.
211 */
Siyamed Sinir472c3162019-10-21 23:41:00 -0700212 private fun getAdjustedCoordinates(position: PxPosition): PxPosition {
213 return PxPosition(position.x, position.y - 1.px)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700214 }
215
Siyamed Sinir472c3162019-10-21 23:41:00 -0700216 fun handleDragObserver(isStartHandle: Boolean): DragObserver {
Qingqing Deng6f56a912019-05-13 10:10:37 -0700217 return object : DragObserver {
Andrey Kulikov0e6c40f2019-09-06 18:33:14 +0100218 override fun onStart(downPosition: PxPosition) {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700219 val selection = selection!!
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100220 // The LayoutCoordinates of the composable where the drag gesture should begin. This
Qingqing Deng6f56a912019-05-13 10:10:37 -0700221 // is used to convert the position of the beginning of the drag gesture from the
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100222 // composable coordinates to selection container coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700223 val beginLayoutCoordinates = if (isStartHandle) {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800224 selection.start.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700225 } else {
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800226 selection.end.selectable.getLayoutCoordinates()!!
Siyamed Sinir472c3162019-10-21 23:41:00 -0700227 }
228
Qingqing Deng6f56a912019-05-13 10:10:37 -0700229 // The position of the character where the drag gesture should begin. This is in
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100230 // the composable coordinates.
Siyamed Sinir472c3162019-10-21 23:41:00 -0700231 val beginCoordinates = getAdjustedCoordinates(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800232 if (isStartHandle)
233 selection.start.selectable.getHandlePosition(
234 selection = selection, isStartHandle = true
235 ) else
236 selection.end.selectable.getHandlePosition(
237 selection = selection, isStartHandle = false
238 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700239 )
240
Louis Pullen-Freilich5da28bd2019-10-15 17:05:07 +0100241 // Convert the position where drag gesture begins from composable coordinates to
Qingqing Deng6f56a912019-05-13 10:10:37 -0700242 // selection container coordinates.
243 dragBeginPosition = containerLayoutCoordinates.childToLocal(
244 beginLayoutCoordinates,
245 beginCoordinates
246 )
247
248 // Zero out the total distance that being dragged.
249 dragTotalDistance = PxPosition.Origin
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700250 draggingHandle = true
Qingqing Deng6f56a912019-05-13 10:10:37 -0700251 }
252
253 override fun onDrag(dragDistance: PxPosition): PxPosition {
Siyamed Sinir472c3162019-10-21 23:41:00 -0700254 val selection = selection!!
Qingqing Deng6f56a912019-05-13 10:10:37 -0700255 dragTotalDistance += dragDistance
256
Siyamed Sinir472c3162019-10-21 23:41:00 -0700257 val currentStart = if (isStartHandle) {
258 dragBeginPosition + dragTotalDistance
259 } else {
260 containerLayoutCoordinates.childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800261 selection.start.selectable.getLayoutCoordinates()!!,
262 getAdjustedCoordinates(
263 selection.start.selectable.getHandlePosition(
264 selection = selection,
265 isStartHandle = true
266 )
267 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700268 )
Qingqing Deng6f56a912019-05-13 10:10:37 -0700269 }
Siyamed Sinir472c3162019-10-21 23:41:00 -0700270
271 val currentEnd = if (isStartHandle) {
272 containerLayoutCoordinates.childToLocal(
Qingqing Deng6d1b7d22019-12-27 17:48:08 -0800273 selection.end.selectable.getLayoutCoordinates()!!,
274 getAdjustedCoordinates(
275 selection.end.selectable.getHandlePosition(
276 selection = selection,
277 isStartHandle = false
278 )
279 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700280 )
281 } else {
282 dragBeginPosition + dragTotalDistance
283 }
284
Qingqing Denga5d80952019-10-11 16:46:52 -0700285 val finalSelection = mergeSelections(
286 startPosition = currentStart,
287 endPosition = currentEnd,
Qingqing Deng25b8f8d2020-01-17 16:36:19 -0800288 previousSelection = selection,
Qingqing Dengff65ed62020-01-06 17:55:48 -0800289 isStartHandle = isStartHandle
Qingqing Denga5d80952019-10-11 16:46:52 -0700290 )
Siyamed Sinir472c3162019-10-21 23:41:00 -0700291 onSelectionChange(finalSelection)
Qingqing Deng6f56a912019-05-13 10:10:37 -0700292 return dragDistance
293 }
Qingqing Deng0cb86fe2019-07-16 15:36:27 -0700294
295 override fun onStop(velocity: PxPosition) {
296 super.onStop(velocity)
297 draggingHandle = false
298 }
Qingqing Deng6f56a912019-05-13 10:10:37 -0700299 }
300 }
301}
302
Siyamed Sinir700df8452019-10-22 20:23:58 -0700303private fun merge(lhs: Selection?, rhs: Selection?): Selection? {
304 return lhs?.merge(rhs) ?: rhs
305}
Qingqing Deng648e1bf2019-12-30 11:49:48 -0800306
307private fun getCurrentSelectedText(
308 selectable: Selectable,
309 selection: Selection
310): AnnotatedString {
311 val currentText = selectable.getText()
312
313 return if (
314 selectable != selection.start.selectable &&
315 selectable != selection.end.selectable
316 ) {
317 // Select the full text content if the current selectable is between the
318 // start and the end selectables.
319 currentText
320 } else if (
321 selectable == selection.start.selectable &&
322 selectable == selection.end.selectable
323 ) {
324 // Select partial text content if the current selectable is the start and
325 // the end selectable.
326 if (selection.handlesCrossed) {
327 currentText.subSequence(selection.end.offset, selection.start.offset)
328 } else {
329 currentText.subSequence(selection.start.offset, selection.end.offset)
330 }
331 } else if (selectable == selection.start.selectable) {
332 // Select partial text content if the current selectable is the start
333 // selectable.
334 if (selection.handlesCrossed) {
335 currentText.subSequence(0, selection.start.offset)
336 } else {
337 currentText.subSequence(selection.start.offset, currentText.length)
338 }
339 } else {
340 // Selectable partial text content if the current selectable is the end
341 // selectable.
342 if (selection.handlesCrossed) {
343 currentText.subSequence(selection.end.offset, currentText.length)
344 } else {
345 currentText.subSequence(0, selection.end.offset)
346 }
347 }
348}