/src/serenity/Userland/Libraries/LibWeb/Selection/Selection.cpp
Line | Count | Source |
1 | | /* |
2 | | * Copyright (c) 2021-2022, Andreas Kling <kling@serenityos.org> |
3 | | * |
4 | | * SPDX-License-Identifier: BSD-2-Clause |
5 | | */ |
6 | | |
7 | | #include <LibWeb/Bindings/Intrinsics.h> |
8 | | #include <LibWeb/Bindings/SelectionPrototype.h> |
9 | | #include <LibWeb/DOM/Document.h> |
10 | | #include <LibWeb/DOM/Range.h> |
11 | | #include <LibWeb/Selection/Selection.h> |
12 | | |
13 | | namespace Web::Selection { |
14 | | |
15 | | JS_DEFINE_ALLOCATOR(Selection); |
16 | | |
17 | | JS::NonnullGCPtr<Selection> Selection::create(JS::NonnullGCPtr<JS::Realm> realm, JS::NonnullGCPtr<DOM::Document> document) |
18 | 0 | { |
19 | 0 | return realm->heap().allocate<Selection>(realm, realm, document); |
20 | 0 | } |
21 | | |
22 | | Selection::Selection(JS::NonnullGCPtr<JS::Realm> realm, JS::NonnullGCPtr<DOM::Document> document) |
23 | 0 | : PlatformObject(realm) |
24 | 0 | , m_document(document) |
25 | 0 | { |
26 | 0 | } |
27 | | |
28 | 0 | Selection::~Selection() = default; |
29 | | |
30 | | void Selection::initialize(JS::Realm& realm) |
31 | 0 | { |
32 | 0 | Base::initialize(realm); |
33 | 0 | WEB_SET_PROTOTYPE_FOR_INTERFACE(Selection); |
34 | 0 | } |
35 | | |
36 | | // https://w3c.github.io/selection-api/#dfn-empty |
37 | | bool Selection::is_empty() const |
38 | 0 | { |
39 | | // Each selection can be associated with a single range. |
40 | | // When there is no range associated with the selection, the selection is empty. |
41 | | // The selection must be initially empty. |
42 | | |
43 | | // NOTE: This function should not be confused with Selection.empty() which empties the selection. |
44 | 0 | return !m_range; |
45 | 0 | } |
46 | | |
47 | | void Selection::visit_edges(Cell::Visitor& visitor) |
48 | 0 | { |
49 | 0 | Base::visit_edges(visitor); |
50 | 0 | visitor.visit(m_range); |
51 | 0 | visitor.visit(m_document); |
52 | 0 | } |
53 | | |
54 | | // https://w3c.github.io/selection-api/#dfn-anchor |
55 | | JS::GCPtr<DOM::Node> Selection::anchor_node() |
56 | 0 | { |
57 | 0 | if (!m_range) |
58 | 0 | return nullptr; |
59 | 0 | if (m_direction == Direction::Forwards) |
60 | 0 | return m_range->start_container(); |
61 | 0 | return m_range->end_container(); |
62 | 0 | } |
63 | | |
64 | | // https://w3c.github.io/selection-api/#dfn-anchor |
65 | | unsigned Selection::anchor_offset() |
66 | 0 | { |
67 | 0 | if (!m_range) |
68 | 0 | return 0; |
69 | 0 | if (m_direction == Direction::Forwards) |
70 | 0 | return m_range->start_offset(); |
71 | 0 | return m_range->end_offset(); |
72 | 0 | } |
73 | | |
74 | | // https://w3c.github.io/selection-api/#dfn-focus |
75 | | JS::GCPtr<DOM::Node> Selection::focus_node() |
76 | 0 | { |
77 | 0 | if (!m_range) |
78 | 0 | return nullptr; |
79 | 0 | if (m_direction == Direction::Forwards) |
80 | 0 | return m_range->end_container(); |
81 | 0 | return m_range->start_container(); |
82 | 0 | } |
83 | | |
84 | | // https://w3c.github.io/selection-api/#dfn-focus |
85 | | unsigned Selection::focus_offset() const |
86 | 0 | { |
87 | 0 | if (!m_range) |
88 | 0 | return 0; |
89 | 0 | if (m_direction == Direction::Forwards) |
90 | 0 | return m_range->end_offset(); |
91 | 0 | return m_range->start_offset(); |
92 | 0 | } |
93 | | |
94 | | // https://w3c.github.io/selection-api/#dom-selection-iscollapsed |
95 | | bool Selection::is_collapsed() const |
96 | 0 | { |
97 | | // The attribute must return true if and only if the anchor and focus are the same |
98 | | // (including if both are null). Otherwise it must return false. |
99 | 0 | if (!m_range) |
100 | 0 | return true; |
101 | 0 | return const_cast<Selection*>(this)->anchor_node() == const_cast<Selection*>(this)->focus_node() |
102 | 0 | && m_range->start_offset() == m_range->end_offset(); |
103 | 0 | } |
104 | | |
105 | | // https://w3c.github.io/selection-api/#dom-selection-rangecount |
106 | | unsigned Selection::range_count() const |
107 | 0 | { |
108 | 0 | if (m_range) |
109 | 0 | return 1; |
110 | 0 | return 0; |
111 | 0 | } |
112 | | |
113 | | String Selection::type() const |
114 | 0 | { |
115 | 0 | if (!m_range) |
116 | 0 | return "None"_string; |
117 | 0 | if (m_range->collapsed()) |
118 | 0 | return "Caret"_string; |
119 | 0 | return "Range"_string; |
120 | 0 | } |
121 | | |
122 | | String Selection::direction() const |
123 | 0 | { |
124 | 0 | if (!m_range || m_direction == Direction::Directionless) |
125 | 0 | return "none"_string; |
126 | 0 | if (m_direction == Direction::Forwards) |
127 | 0 | return "forward"_string; |
128 | 0 | return "backward"_string; |
129 | 0 | } |
130 | | |
131 | | // https://w3c.github.io/selection-api/#dom-selection-getrangeat |
132 | | WebIDL::ExceptionOr<JS::GCPtr<DOM::Range>> Selection::get_range_at(unsigned index) |
133 | 0 | { |
134 | | // The method must throw an IndexSizeError exception if index is not 0, or if this is empty. |
135 | 0 | if (index != 0 || is_empty()) |
136 | 0 | return WebIDL::IndexSizeError::create(realm(), "Selection.getRangeAt() on empty Selection or with invalid argument"_string); |
137 | | |
138 | | // Otherwise, it must return a reference to (not a copy of) this's range. |
139 | 0 | return m_range; |
140 | 0 | } |
141 | | |
142 | | // https://w3c.github.io/selection-api/#dom-selection-addrange |
143 | | void Selection::add_range(JS::NonnullGCPtr<DOM::Range> range) |
144 | 0 | { |
145 | | // 1. If the root of the range's boundary points are not the document associated with this, abort these steps. |
146 | 0 | if (&range->start_container()->root() != m_document.ptr()) |
147 | 0 | return; |
148 | | |
149 | | // 2. If rangeCount is not 0, abort these steps. |
150 | 0 | if (range_count() != 0) |
151 | 0 | return; |
152 | | |
153 | | // 3. Set this's range to range by a strong reference (not by making a copy). |
154 | 0 | set_range(range); |
155 | | |
156 | | // AD-HOC: WPT selection/removeAllRanges.html and selection/addRange.htm expect this |
157 | 0 | m_direction = Direction::Forwards; |
158 | 0 | } |
159 | | |
160 | | // https://w3c.github.io/selection-api/#dom-selection-removerange |
161 | | WebIDL::ExceptionOr<void> Selection::remove_range(JS::NonnullGCPtr<DOM::Range> range) |
162 | 0 | { |
163 | | // The method must make this empty by disassociating its range if this's range is range. |
164 | 0 | if (m_range == range) { |
165 | 0 | set_range(nullptr); |
166 | 0 | return {}; |
167 | 0 | } |
168 | | |
169 | | // Otherwise, it must throw a NotFoundError. |
170 | 0 | return WebIDL::NotFoundError::create(realm(), "Selection.removeRange() with invalid argument"_string); |
171 | 0 | } |
172 | | |
173 | | // https://w3c.github.io/selection-api/#dom-selection-removeallranges |
174 | | void Selection::remove_all_ranges() |
175 | 0 | { |
176 | | // The method must make this empty by disassociating its range if this has an associated range. |
177 | 0 | set_range(nullptr); |
178 | 0 | } |
179 | | |
180 | | // https://w3c.github.io/selection-api/#dom-selection-empty |
181 | | void Selection::empty() |
182 | 0 | { |
183 | | // The method must be an alias, and behave identically, to removeAllRanges(). |
184 | 0 | remove_all_ranges(); |
185 | 0 | } |
186 | | |
187 | | // https://w3c.github.io/selection-api/#dom-selection-collapse |
188 | | WebIDL::ExceptionOr<void> Selection::collapse(JS::GCPtr<DOM::Node> node, unsigned offset) |
189 | 0 | { |
190 | | // 1. If node is null, this method must behave identically as removeAllRanges() and abort these steps. |
191 | 0 | if (!node) { |
192 | 0 | remove_all_ranges(); |
193 | 0 | return {}; |
194 | 0 | } |
195 | | |
196 | | // 2. If node is a DocumentType, throw an InvalidNodeTypeError exception and abort these steps. |
197 | 0 | if (node->is_document_type()) |
198 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Selection.collapse() with DocumentType node"_string); |
199 | | |
200 | | // 3. The method must throw an IndexSizeError exception if offset is longer than node's length and abort these steps. |
201 | 0 | if (offset > node->length()) |
202 | 0 | return WebIDL::IndexSizeError::create(realm(), "Selection.collapse() with offset longer than node's length"_string); |
203 | | |
204 | | // 4. If document associated with this is not a shadow-including inclusive ancestor of node, abort these steps. |
205 | 0 | if (!m_document->is_shadow_including_inclusive_ancestor_of(*node)) |
206 | 0 | return {}; |
207 | | |
208 | | // 5. Otherwise, let newRange be a new range. |
209 | 0 | auto new_range = DOM::Range::create(*m_document); |
210 | | |
211 | | // 6. Set the start the start and the end of newRange to (node, offset). |
212 | 0 | TRY(new_range->set_start(*node, offset)); |
213 | | |
214 | | // 7. Set this's range to newRange. |
215 | 0 | set_range(new_range); |
216 | |
|
217 | 0 | return {}; |
218 | 0 | } |
219 | | |
220 | | // https://w3c.github.io/selection-api/#dom-selection-setposition |
221 | | WebIDL::ExceptionOr<void> Selection::set_position(JS::GCPtr<DOM::Node> node, unsigned offset) |
222 | 0 | { |
223 | | // The method must be an alias, and behave identically, to collapse(). |
224 | 0 | return collapse(node, offset); |
225 | 0 | } |
226 | | |
227 | | // https://w3c.github.io/selection-api/#dom-selection-collapsetostart |
228 | | WebIDL::ExceptionOr<void> Selection::collapse_to_start() |
229 | 0 | { |
230 | | // 1. The method must throw InvalidStateError exception if the this is empty. |
231 | 0 | if (!m_range) { |
232 | 0 | return WebIDL::InvalidStateError::create(realm(), "Selection.collapse_to_start() on empty range"_string); |
233 | 0 | } |
234 | | |
235 | | // 2. Otherwise, it must create a new range |
236 | 0 | auto new_range = DOM::Range::create(*m_document); |
237 | | |
238 | | // 3. Set the start both its start and end to the start of this's range |
239 | 0 | TRY(new_range->set_start(*anchor_node(), m_range->start_offset())); |
240 | 0 | TRY(new_range->set_end(*anchor_node(), m_range->start_offset())); |
241 | | |
242 | | // 4. Then set this's range to the newly-created range. |
243 | 0 | set_range(new_range); |
244 | 0 | return {}; |
245 | 0 | } |
246 | | |
247 | | // https://w3c.github.io/selection-api/#dom-selection-collapsetoend |
248 | | WebIDL::ExceptionOr<void> Selection::collapse_to_end() |
249 | 0 | { |
250 | | // 1. The method must throw InvalidStateError exception if the this is empty. |
251 | 0 | if (!m_range) { |
252 | 0 | return WebIDL::InvalidStateError::create(realm(), "Selection.collapse_to_end() on empty range"_string); |
253 | 0 | } |
254 | | |
255 | | // 2. Otherwise, it must create a new range |
256 | 0 | auto new_range = DOM::Range::create(*m_document); |
257 | | |
258 | | // 3. Set the start both its start and end to the start of this's range |
259 | 0 | TRY(new_range->set_start(*anchor_node(), m_range->end_offset())); |
260 | 0 | TRY(new_range->set_end(*anchor_node(), m_range->end_offset())); |
261 | | |
262 | | // 4. Then set this's range to the newly-created range. |
263 | 0 | set_range(new_range); |
264 | |
|
265 | 0 | return {}; |
266 | 0 | } |
267 | | |
268 | | // https://w3c.github.io/selection-api/#dom-selection-extend |
269 | | WebIDL::ExceptionOr<void> Selection::extend(JS::NonnullGCPtr<DOM::Node> node, unsigned offset) |
270 | 0 | { |
271 | | // 1. If the document associated with this is not a shadow-including inclusive ancestor of node, abort these steps. |
272 | 0 | if (!m_document->is_shadow_including_inclusive_ancestor_of(node)) |
273 | 0 | return {}; |
274 | | |
275 | | // 2. If this is empty, throw an InvalidStateError exception and abort these steps. |
276 | 0 | if (!m_range) { |
277 | 0 | return WebIDL::InvalidStateError::create(realm(), "Selection.extend() on empty range"_string); |
278 | 0 | } |
279 | | |
280 | | // 3. Let oldAnchor and oldFocus be the this's anchor and focus, and let newFocus be the boundary point (node, offset). |
281 | 0 | auto& old_anchor_node = *anchor_node(); |
282 | 0 | auto old_anchor_offset = anchor_offset(); |
283 | |
|
284 | 0 | auto& new_focus_node = node; |
285 | 0 | auto new_focus_offset = offset; |
286 | | |
287 | | // 4. Let newRange be a new range. |
288 | 0 | auto new_range = DOM::Range::create(*m_document); |
289 | | |
290 | | // 5. If node's root is not the same as the this's range's root, set the start newRange's start and end to newFocus. |
291 | 0 | if (&node->root() != &m_range->start_container()->root()) { |
292 | 0 | TRY(new_range->set_start(new_focus_node, new_focus_offset)); |
293 | 0 | TRY(new_range->set_end(new_focus_node, new_focus_offset)); |
294 | 0 | } |
295 | | // 6. Otherwise, if oldAnchor is before or equal to newFocus, set the start newRange's start to oldAnchor, then set its end to newFocus. |
296 | 0 | else if (position_of_boundary_point_relative_to_other_boundary_point(old_anchor_node, old_anchor_offset, new_focus_node, new_focus_offset) != DOM::RelativeBoundaryPointPosition::After) { |
297 | 0 | TRY(new_range->set_start(old_anchor_node, old_anchor_offset)); |
298 | 0 | TRY(new_range->set_end(new_focus_node, new_focus_offset)); |
299 | 0 | } |
300 | | // 7. Otherwise, set the start newRange's start to newFocus, then set its end to oldAnchor. |
301 | 0 | else { |
302 | 0 | TRY(new_range->set_start(new_focus_node, new_focus_offset)); |
303 | 0 | TRY(new_range->set_end(old_anchor_node, old_anchor_offset)); |
304 | 0 | } |
305 | | |
306 | | // 8. Set this's range to newRange. |
307 | 0 | set_range(new_range); |
308 | | |
309 | | // 9. If newFocus is before oldAnchor, set this's direction to backwards. Otherwise, set it to forwards. |
310 | 0 | if (position_of_boundary_point_relative_to_other_boundary_point(new_focus_node, new_focus_offset, old_anchor_node, old_anchor_offset) == DOM::RelativeBoundaryPointPosition::Before) { |
311 | 0 | m_direction = Direction::Backwards; |
312 | 0 | } else { |
313 | 0 | m_direction = Direction::Forwards; |
314 | 0 | } |
315 | |
|
316 | 0 | return {}; |
317 | 0 | } |
318 | | |
319 | | // https://w3c.github.io/selection-api/#dom-selection-setbaseandextent |
320 | | WebIDL::ExceptionOr<void> Selection::set_base_and_extent(JS::NonnullGCPtr<DOM::Node> anchor_node, unsigned anchor_offset, JS::NonnullGCPtr<DOM::Node> focus_node, unsigned focus_offset) |
321 | 0 | { |
322 | | // 1. If anchorOffset is longer than anchorNode's length or if focusOffset is longer than focusNode's length, throw an IndexSizeError exception and abort these steps. |
323 | 0 | if (anchor_offset > anchor_node->length()) |
324 | 0 | return WebIDL::IndexSizeError::create(realm(), "Anchor offset points outside of the anchor node"_string); |
325 | | |
326 | 0 | if (focus_offset > focus_node->length()) |
327 | 0 | return WebIDL::IndexSizeError::create(realm(), "Focus offset points outside of the focus node"_string); |
328 | | |
329 | | // 2. If document associated with this is not a shadow-including inclusive ancestor of anchorNode or focusNode, abort these steps. |
330 | 0 | if (!m_document->is_shadow_including_inclusive_ancestor_of(anchor_node) || !m_document->is_shadow_including_inclusive_ancestor_of(focus_node)) |
331 | 0 | return {}; |
332 | | |
333 | | // 3. Let anchor be the boundary point (anchorNode, anchorOffset) and let focus be the boundary point (focusNode, focusOffset). |
334 | | |
335 | | // 4. Let newRange be a new range. |
336 | 0 | auto new_range = DOM::Range::create(*m_document); |
337 | | |
338 | | // 5. If anchor is before focus, set the start the newRange's start to anchor and its end to focus. Otherwise, set the start them to focus and anchor respectively. |
339 | 0 | auto position_of_anchor_relative_to_focus = DOM::position_of_boundary_point_relative_to_other_boundary_point(anchor_node, anchor_offset, focus_node, focus_offset); |
340 | 0 | if (position_of_anchor_relative_to_focus == DOM::RelativeBoundaryPointPosition::Before) { |
341 | 0 | TRY(new_range->set_start(anchor_node, anchor_offset)); |
342 | 0 | TRY(new_range->set_end(focus_node, focus_offset)); |
343 | 0 | } else { |
344 | 0 | TRY(new_range->set_start(focus_node, focus_offset)); |
345 | 0 | TRY(new_range->set_end(anchor_node, anchor_offset)); |
346 | 0 | } |
347 | | |
348 | | // 6. Set this's range to newRange. |
349 | 0 | set_range(new_range); |
350 | | |
351 | | // 7. If focus is before anchor, set this's direction to backwards. Otherwise, set it to forwards |
352 | | // NOTE: "Otherwise" can be seen as "focus is equal to or after anchor". |
353 | 0 | if (position_of_anchor_relative_to_focus == DOM::RelativeBoundaryPointPosition::After) |
354 | 0 | m_direction = Direction::Backwards; |
355 | 0 | else |
356 | 0 | m_direction = Direction::Forwards; |
357 | |
|
358 | 0 | return {}; |
359 | 0 | } |
360 | | |
361 | | // https://w3c.github.io/selection-api/#dom-selection-selectallchildren |
362 | | WebIDL::ExceptionOr<void> Selection::select_all_children(JS::NonnullGCPtr<DOM::Node> node) |
363 | 0 | { |
364 | | // 1. If node is a DocumentType, throw an InvalidNodeTypeError exception and abort these steps. |
365 | 0 | if (node->is_document_type()) |
366 | 0 | return WebIDL::InvalidNodeTypeError::create(realm(), "Selection.selectAllChildren() with DocumentType node"_string); |
367 | | |
368 | | // 2. If node's root is not the document associated with this, abort these steps. |
369 | 0 | if (&node->root() != m_document.ptr()) |
370 | 0 | return {}; |
371 | | |
372 | | // 3. Let newRange be a new range and childCount be the number of children of node. |
373 | 0 | auto new_range = DOM::Range::create(*m_document); |
374 | 0 | auto child_count = node->child_count(); |
375 | | |
376 | | // 4. Set newRange's start to (node, 0). |
377 | 0 | TRY(new_range->set_start(node, 0)); |
378 | | |
379 | | // 5. Set newRange's end to (node, childCount). |
380 | 0 | TRY(new_range->set_end(node, child_count)); |
381 | | |
382 | | // 6. Set this's range to newRange. |
383 | 0 | set_range(new_range); |
384 | | |
385 | | // 7. Set this's direction to forwards. |
386 | 0 | m_direction = Direction::Forwards; |
387 | |
|
388 | 0 | return {}; |
389 | 0 | } |
390 | | |
391 | | // https://w3c.github.io/selection-api/#dom-selection-deletefromdocument |
392 | | WebIDL::ExceptionOr<void> Selection::delete_from_document() |
393 | 0 | { |
394 | | // The method must invoke deleteContents() on this's range if this is not empty. |
395 | | // Otherwise the method must do nothing. |
396 | 0 | if (!is_empty()) |
397 | 0 | return m_range->delete_contents(); |
398 | 0 | return {}; |
399 | 0 | } |
400 | | |
401 | | // https://w3c.github.io/selection-api/#dom-selection-containsnode |
402 | | bool Selection::contains_node(JS::NonnullGCPtr<DOM::Node> node, bool allow_partial_containment) const |
403 | 0 | { |
404 | | // The method must return false if this is empty or if node's root is not the document associated with this. |
405 | 0 | if (!m_range) |
406 | 0 | return false; |
407 | 0 | if (&node->root() != m_document.ptr()) |
408 | 0 | return false; |
409 | | |
410 | | // Otherwise, if allowPartialContainment is false, the method must return true if and only if |
411 | | // start of its range is before or visually equivalent to the first boundary point in the node |
412 | | // and end of its range is after or visually equivalent to the last boundary point in the node. |
413 | 0 | if (!allow_partial_containment) { |
414 | 0 | auto start_relative_position = DOM::position_of_boundary_point_relative_to_other_boundary_point( |
415 | 0 | *m_range->start_container(), |
416 | 0 | m_range->start_offset(), |
417 | 0 | node, |
418 | 0 | 0); |
419 | 0 | auto end_relative_position = DOM::position_of_boundary_point_relative_to_other_boundary_point( |
420 | 0 | *m_range->end_container(), |
421 | 0 | m_range->end_offset(), |
422 | 0 | node, |
423 | 0 | node->length()); |
424 | |
|
425 | 0 | return (start_relative_position == DOM::RelativeBoundaryPointPosition::Before || start_relative_position == DOM::RelativeBoundaryPointPosition::Equal) |
426 | 0 | && (end_relative_position == DOM::RelativeBoundaryPointPosition::Equal || end_relative_position == DOM::RelativeBoundaryPointPosition::After); |
427 | 0 | } |
428 | | |
429 | | // If allowPartialContainment is true, the method must return true if and only if |
430 | | // start of its range is before or visually equivalent to the last boundary point in the node |
431 | | // and end of its range is after or visually equivalent to the first boundary point in the node. |
432 | | |
433 | 0 | auto start_relative_position = DOM::position_of_boundary_point_relative_to_other_boundary_point( |
434 | 0 | *m_range->start_container(), |
435 | 0 | m_range->start_offset(), |
436 | 0 | node, |
437 | 0 | node->length()); |
438 | 0 | auto end_relative_position = DOM::position_of_boundary_point_relative_to_other_boundary_point( |
439 | 0 | *m_range->end_container(), |
440 | 0 | m_range->end_offset(), |
441 | 0 | node, |
442 | 0 | 0); |
443 | |
|
444 | 0 | return (start_relative_position == DOM::RelativeBoundaryPointPosition::Before || start_relative_position == DOM::RelativeBoundaryPointPosition::Equal) |
445 | 0 | && (end_relative_position == DOM::RelativeBoundaryPointPosition::Equal || end_relative_position == DOM::RelativeBoundaryPointPosition::After); |
446 | 0 | } |
447 | | |
448 | | String Selection::to_string() const |
449 | 0 | { |
450 | | // FIXME: This needs more work to be compatible with other engines. |
451 | | // See https://www.w3.org/Bugs/Public/show_bug.cgi?id=10583 |
452 | 0 | if (!m_range) |
453 | 0 | return String {}; |
454 | 0 | return m_range->to_string(); |
455 | 0 | } |
456 | | |
457 | | JS::NonnullGCPtr<DOM::Document> Selection::document() const |
458 | 0 | { |
459 | 0 | return m_document; |
460 | 0 | } |
461 | | |
462 | | JS::GCPtr<DOM::Range> Selection::range() const |
463 | 0 | { |
464 | 0 | return m_range; |
465 | 0 | } |
466 | | |
467 | | void Selection::set_range(JS::GCPtr<DOM::Range> range) |
468 | 0 | { |
469 | 0 | if (m_range == range) |
470 | 0 | return; |
471 | | |
472 | 0 | if (m_range) |
473 | 0 | m_range->set_associated_selection({}, nullptr); |
474 | |
|
475 | 0 | m_range = range; |
476 | |
|
477 | 0 | if (m_range) |
478 | 0 | m_range->set_associated_selection({}, this); |
479 | 0 | } |
480 | | |
481 | | } |