1 | //************************************ bs::framework - Copyright 2018 Marko Pintera **************************************// |
2 | //*********** Licensed under the MIT license. See LICENSE.md for full terms. This notice is not to be removed. ***********// |
3 | #include "GUI/BsGUIElementBase.h" |
4 | #include "GUI/BsGUILayout.h" |
5 | #include "GUI/BsGUIPanel.h" |
6 | #include "GUI/BsGUISpace.h" |
7 | #include "GUI/BsGUIElement.h" |
8 | #include "Error/BsException.h" |
9 | #include "GUI/BsGUIWidget.h" |
10 | #include "BsGUIManager.h" |
11 | |
12 | namespace bs |
13 | { |
14 | GUIElementBase::GUIElementBase(const GUIDimensions& dimensions) |
15 | : mDimensions(dimensions) |
16 | { } |
17 | |
18 | GUIElementBase::~GUIElementBase() |
19 | { |
20 | destroyChildElements(); |
21 | } |
22 | |
23 | void GUIElementBase::setPosition(INT32 x, INT32 y) |
24 | { |
25 | mDimensions.x = x; |
26 | mDimensions.y = y; |
27 | |
28 | // Note: I could call _markMeshAsDirty with a little more work. If parent is layout then this call can be ignored |
29 | // and if it's a panel, we can immediately change the position without a full layout rebuild. |
30 | _markLayoutAsDirty(); |
31 | } |
32 | |
33 | void GUIElementBase::setSize(UINT32 width, UINT32 height) |
34 | { |
35 | bool isFixedBefore = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
36 | |
37 | mDimensions.flags |= GUIDF_FixedWidth | GUIDF_OverWidth | GUIDF_FixedHeight | GUIDF_OverHeight; |
38 | mDimensions.minWidth = mDimensions.maxWidth = width; |
39 | mDimensions.minHeight = mDimensions.maxHeight = height; |
40 | |
41 | bool isFixedAfter = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
42 | |
43 | if (isFixedBefore != isFixedAfter) |
44 | refreshChildUpdateParents(); |
45 | |
46 | _markLayoutAsDirty(); |
47 | } |
48 | |
49 | void GUIElementBase::setWidth(UINT32 width) |
50 | { |
51 | bool isFixedBefore = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
52 | |
53 | mDimensions.flags |= GUIDF_FixedWidth | GUIDF_OverWidth; |
54 | mDimensions.minWidth = mDimensions.maxWidth = width; |
55 | |
56 | bool isFixedAfter = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
57 | |
58 | if (isFixedBefore != isFixedAfter) |
59 | refreshChildUpdateParents(); |
60 | |
61 | _markLayoutAsDirty(); |
62 | } |
63 | |
64 | void GUIElementBase::setFlexibleWidth(UINT32 minWidth, UINT32 maxWidth) |
65 | { |
66 | if (maxWidth < minWidth) |
67 | std::swap(minWidth, maxWidth); |
68 | |
69 | bool isFixedBefore = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
70 | |
71 | mDimensions.flags |= GUIDF_OverWidth; |
72 | mDimensions.flags &= ~GUIDF_FixedWidth; |
73 | mDimensions.minWidth = minWidth; |
74 | mDimensions.maxWidth = maxWidth; |
75 | |
76 | bool isFixedAfter = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
77 | |
78 | if (isFixedBefore != isFixedAfter) |
79 | refreshChildUpdateParents(); |
80 | |
81 | _markLayoutAsDirty(); |
82 | } |
83 | |
84 | void GUIElementBase::setHeight(UINT32 height) |
85 | { |
86 | bool isFixedBefore = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
87 | |
88 | mDimensions.flags |= GUIDF_FixedHeight | GUIDF_OverHeight; |
89 | mDimensions.minHeight = mDimensions.maxHeight = height; |
90 | |
91 | bool isFixedAfter = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
92 | |
93 | if (isFixedBefore != isFixedAfter) |
94 | refreshChildUpdateParents(); |
95 | |
96 | _markLayoutAsDirty(); |
97 | } |
98 | |
99 | void GUIElementBase::setFlexibleHeight(UINT32 minHeight, UINT32 maxHeight) |
100 | { |
101 | if (maxHeight < minHeight) |
102 | std::swap(minHeight, maxHeight); |
103 | |
104 | bool isFixedBefore = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
105 | |
106 | mDimensions.flags |= GUIDF_OverHeight; |
107 | mDimensions.flags &= ~GUIDF_FixedHeight; |
108 | mDimensions.minHeight = minHeight; |
109 | mDimensions.maxHeight = maxHeight; |
110 | |
111 | bool isFixedAfter = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
112 | |
113 | if (isFixedBefore != isFixedAfter) |
114 | refreshChildUpdateParents(); |
115 | |
116 | _markLayoutAsDirty(); |
117 | } |
118 | |
119 | void GUIElementBase::resetDimensions() |
120 | { |
121 | bool isFixedBefore = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
122 | |
123 | mDimensions = GUIDimensions::create(); |
124 | |
125 | bool isFixedAfter = (mDimensions.flags & GUIDF_FixedWidth) != 0 && (mDimensions.flags & GUIDF_FixedHeight) != 0; |
126 | |
127 | if (isFixedBefore != isFixedAfter) |
128 | refreshChildUpdateParents(); |
129 | |
130 | _markLayoutAsDirty(); |
131 | } |
132 | |
133 | Rect2I GUIElementBase::getBounds(GUIPanel* relativeTo) |
134 | { |
135 | if (relativeTo == nullptr) |
136 | relativeTo = mAnchorParent; |
137 | |
138 | Rect2I anchorBounds; |
139 | if (relativeTo != nullptr) |
140 | anchorBounds = relativeTo->getGlobalBounds(); |
141 | |
142 | if (mUpdateParent != nullptr && mUpdateParent->_isDirty() && mParentWidget != nullptr) |
143 | mParentWidget->_updateLayout(mUpdateParent); |
144 | |
145 | Rect2I bounds = mLayoutData.area; |
146 | bounds.x -= anchorBounds.x; |
147 | bounds.y -= anchorBounds.y; |
148 | |
149 | return bounds; |
150 | } |
151 | |
152 | void GUIElementBase::setBounds(const Rect2I& bounds) |
153 | { |
154 | setPosition(bounds.x, bounds.y); |
155 | setWidth(bounds.width); |
156 | setHeight(bounds.height); |
157 | } |
158 | |
159 | Rect2I GUIElementBase::getGlobalBounds() |
160 | { |
161 | if (mUpdateParent != nullptr && mUpdateParent->_isDirty() && mParentWidget != nullptr) |
162 | mParentWidget->_updateLayout(mUpdateParent); |
163 | |
164 | return mLayoutData.area; |
165 | } |
166 | |
167 | Rect2I GUIElementBase::getScreenBounds() const |
168 | { |
169 | if (mUpdateParent != nullptr && mUpdateParent->_isDirty() && mParentWidget != nullptr) |
170 | mParentWidget->_updateLayout(mUpdateParent); |
171 | |
172 | Rect2I area = mLayoutData.area; |
173 | if(mParentWidget) |
174 | { |
175 | const Matrix4& widgetTfrm = mParentWidget->getWorldTfrm(); |
176 | Vector2I localPos(area.x, area.y); |
177 | |
178 | const Vector4 widgetPosFlt = widgetTfrm.multiplyAffine(Vector4((float)localPos.x, (float)localPos.y, 0.0f, 1.0f)); |
179 | const Vector2I widgetPos(Math::roundToInt(widgetPosFlt.x), Math::roundToInt(widgetPosFlt.y)); |
180 | |
181 | const RenderWindow* parentWindow = GUIManager::instance().getWidgetWindow(*mParentWidget); |
182 | if(parentWindow) |
183 | { |
184 | const Vector2I windowPos = parentWindow->windowToScreenPos(widgetPos); |
185 | area.x = windowPos.x; |
186 | area.y = windowPos.y; |
187 | } |
188 | else |
189 | { |
190 | area.x = widgetPos.x; |
191 | area.y = widgetPos.y; |
192 | } |
193 | } |
194 | |
195 | return area; |
196 | } |
197 | |
198 | Rect2I GUIElementBase::getVisibleBounds() |
199 | { |
200 | return getBounds(); |
201 | } |
202 | |
203 | void GUIElementBase::_markAsClean() |
204 | { |
205 | mFlags &= ~GUIElem_Dirty; |
206 | } |
207 | |
208 | void GUIElementBase::_markLayoutAsDirty() |
209 | { |
210 | if(!_isVisible()) |
211 | return; |
212 | |
213 | if (mUpdateParent != nullptr) |
214 | mUpdateParent->mFlags |= GUIElem_Dirty; |
215 | else |
216 | mFlags |= GUIElem_Dirty; |
217 | } |
218 | |
219 | void GUIElementBase::_markContentAsDirty() |
220 | { |
221 | if (!_isVisible()) |
222 | return; |
223 | |
224 | if (mParentWidget != nullptr) |
225 | mParentWidget->_markContentDirty(this); |
226 | } |
227 | |
228 | void GUIElementBase::_markMeshAsDirty() |
229 | { |
230 | if(!_isVisible()) |
231 | return; |
232 | |
233 | if (mParentWidget != nullptr) |
234 | mParentWidget->_markMeshDirty(this); |
235 | } |
236 | |
237 | void GUIElementBase::setVisible(bool visible) |
238 | { |
239 | // No visibility states matter if object is not active |
240 | if (!_isActive()) |
241 | return; |
242 | |
243 | bool visibleSelf = (mFlags & GUIElem_HiddenSelf) == 0; |
244 | if (visibleSelf != visible) |
245 | { |
246 | // If making an element visible make sure to mark layout as dirty, as we didn't track any dirty flags while the element was inactive |
247 | if (!visible) |
248 | { |
249 | mFlags |= GUIElem_HiddenSelf; |
250 | _setVisible(false); |
251 | } |
252 | else |
253 | { |
254 | mFlags &= ~GUIElem_HiddenSelf; |
255 | |
256 | if (mParentElement == nullptr || mParentElement->_isVisible()) |
257 | _setVisible(true); |
258 | } |
259 | } |
260 | } |
261 | |
262 | void GUIElementBase::_setVisible(bool visible) |
263 | { |
264 | bool isVisible = (mFlags & GUIElem_Hidden) == 0; |
265 | if (isVisible == visible) |
266 | return; |
267 | |
268 | if (!visible) |
269 | { |
270 | _markMeshAsDirty(); |
271 | |
272 | mFlags |= GUIElem_Hidden; |
273 | |
274 | for (auto& child : mChildren) |
275 | child->_setVisible(false); |
276 | } |
277 | else |
278 | { |
279 | bool childVisibleSelf = (mFlags & GUIElem_HiddenSelf) == 0; |
280 | if (childVisibleSelf) |
281 | { |
282 | mFlags &= ~GUIElem_Hidden; |
283 | _markLayoutAsDirty(); |
284 | |
285 | for (auto& child : mChildren) |
286 | child->_setVisible(true); |
287 | } |
288 | } |
289 | } |
290 | |
291 | void GUIElementBase::setActive(bool active) |
292 | { |
293 | static const UINT8 ACTIVE_FLAGS = GUIElem_InactiveSelf | GUIElem_HiddenSelf; |
294 | |
295 | bool activeSelf = (mFlags & GUIElem_InactiveSelf) == 0; |
296 | if (activeSelf != active) |
297 | { |
298 | if (!active) |
299 | { |
300 | mFlags |= ACTIVE_FLAGS; |
301 | |
302 | _setActive(false); |
303 | _setVisible(false); |
304 | } |
305 | else |
306 | { |
307 | mFlags &= ~ACTIVE_FLAGS; |
308 | |
309 | if (mParentElement != nullptr) |
310 | { |
311 | if (mParentElement->_isActive()) |
312 | { |
313 | _setActive(true); |
314 | |
315 | if (mParentElement->_isVisible()) |
316 | _setVisible(true); |
317 | } |
318 | } |
319 | else |
320 | { |
321 | _setActive(true); |
322 | _setVisible(true); |
323 | } |
324 | } |
325 | } |
326 | } |
327 | |
328 | void GUIElementBase::_setActive(bool active) |
329 | { |
330 | bool isActive = (mFlags & GUIElem_Inactive) == 0; |
331 | if (isActive == active) |
332 | return; |
333 | |
334 | if (!active) |
335 | { |
336 | _markLayoutAsDirty(); |
337 | |
338 | mFlags |= GUIElem_Inactive; |
339 | |
340 | for (auto& child : mChildren) |
341 | child->_setActive(false); |
342 | } |
343 | else |
344 | { |
345 | bool childActiveSelf = (mFlags & GUIElem_InactiveSelf) == 0; |
346 | if (childActiveSelf) |
347 | { |
348 | mFlags &= ~GUIElem_Inactive; |
349 | _markLayoutAsDirty(); |
350 | |
351 | for (auto& child : mChildren) |
352 | child->_setActive(true); |
353 | } |
354 | } |
355 | } |
356 | |
357 | void GUIElementBase::setDisabled(bool disabled) |
358 | { |
359 | bool disabledSelf = (mFlags & GUIElem_DisabledSelf) != 0; |
360 | if (disabledSelf != disabled) |
361 | { |
362 | if (!disabled) |
363 | mFlags &= ~GUIElem_DisabledSelf; |
364 | else |
365 | mFlags |= GUIElem_DisabledSelf; |
366 | |
367 | _setDisabled(disabled); |
368 | } |
369 | } |
370 | |
371 | void GUIElementBase::_setDisabled(bool disabled) |
372 | { |
373 | bool isDisabled = (mFlags & GUIElem_Disabled) != 0; |
374 | if (isDisabled == disabled) |
375 | return; |
376 | |
377 | if (!disabled) |
378 | { |
379 | bool disabledSelf = (mFlags & GUIElem_DisabledSelf) != 0; |
380 | if (!disabledSelf) |
381 | { |
382 | mFlags &= ~GUIElem_Disabled; |
383 | |
384 | for (auto& child : mChildren) |
385 | child->_setDisabled(false); |
386 | } |
387 | } |
388 | else |
389 | { |
390 | mFlags |= GUIElem_Disabled; |
391 | |
392 | for (auto& child : mChildren) |
393 | child->_setDisabled(true); |
394 | } |
395 | |
396 | if (_isVisible()) |
397 | _markContentAsDirty(); |
398 | } |
399 | |
400 | void GUIElementBase::_updateLayout(const GUILayoutData& data) |
401 | { |
402 | _updateOptimalLayoutSizes(); // We calculate optimal sizes of all layouts as a pre-processing step, as they are requested often during update |
403 | _updateLayoutInternal(data); |
404 | } |
405 | |
406 | void GUIElementBase::_updateOptimalLayoutSizes() |
407 | { |
408 | for(auto& child : mChildren) |
409 | { |
410 | child->_updateOptimalLayoutSizes(); |
411 | } |
412 | } |
413 | |
414 | void GUIElementBase::_updateLayoutInternal(const GUILayoutData& data) |
415 | { |
416 | for(auto& child : mChildren) |
417 | { |
418 | child->_updateLayoutInternal(data); |
419 | } |
420 | } |
421 | |
422 | LayoutSizeRange GUIElementBase::_calculateLayoutSizeRange() const |
423 | { |
424 | const GUIDimensions& dimensions = _getDimensions(); |
425 | return dimensions.calculateSizeRange(_getOptimalSize()); |
426 | } |
427 | |
428 | LayoutSizeRange GUIElementBase::_getLayoutSizeRange() const |
429 | { |
430 | return _calculateLayoutSizeRange(); |
431 | } |
432 | |
433 | void GUIElementBase::_getElementAreas(const Rect2I& layoutArea, Rect2I* elementAreas, UINT32 numElements, |
434 | const Vector<LayoutSizeRange>& sizeRanges, const LayoutSizeRange& mySizeRange) const |
435 | { |
436 | assert(mChildren.size() == 0); |
437 | } |
438 | |
439 | void GUIElementBase::_setParent(GUIElementBase* parent) |
440 | { |
441 | if(mParentElement != parent) |
442 | { |
443 | mParentElement = parent; |
444 | _updateAUParents(); |
445 | |
446 | if (parent != nullptr) |
447 | { |
448 | if (_getParentWidget() != parent->_getParentWidget()) |
449 | _changeParentWidget(parent->_getParentWidget()); |
450 | } |
451 | else |
452 | _changeParentWidget(nullptr); |
453 | } |
454 | } |
455 | |
456 | void GUIElementBase::_registerChildElement(GUIElementBase* element) |
457 | { |
458 | assert(!element->_isDestroyed()); |
459 | |
460 | GUIElementBase* parentElement = element->_getParent(); |
461 | if(parentElement != nullptr) |
462 | { |
463 | parentElement->_unregisterChildElement(element); |
464 | } |
465 | |
466 | element->_setParent(this); |
467 | mChildren.push_back(element); |
468 | |
469 | element->_setActive(_isActive()); |
470 | element->_setVisible(_isVisible()); |
471 | element->_setDisabled(_isDisabled()); |
472 | |
473 | // No need to mark ourselves as dirty. If we're part of the element's update chain, this will do it for us. |
474 | element->_markLayoutAsDirty(); |
475 | } |
476 | |
477 | void GUIElementBase::_unregisterChildElement(GUIElementBase* element) |
478 | { |
479 | bool foundElem = false; |
480 | for(auto iter = mChildren.begin(); iter != mChildren.end(); ++iter) |
481 | { |
482 | GUIElementBase* child = *iter; |
483 | |
484 | if (child == element) |
485 | { |
486 | element->_markLayoutAsDirty(); |
487 | |
488 | mChildren.erase(iter); |
489 | element->_setParent(nullptr); |
490 | foundElem = true; |
491 | |
492 | break; |
493 | } |
494 | } |
495 | |
496 | if(!foundElem) |
497 | BS_EXCEPT(InvalidParametersException, "Provided element is not a part of this element." ); |
498 | } |
499 | |
500 | void GUIElementBase::destroyChildElements() |
501 | { |
502 | Vector<GUIElementBase*> childCopy = mChildren; |
503 | for (auto& child : childCopy) |
504 | { |
505 | if (child->_getType() == Type::Element) |
506 | { |
507 | const auto element = static_cast<GUIElement*>(child); |
508 | GUIElement::destroy(element); |
509 | } |
510 | else if (child->_getType() == Type::Layout || child->_getType() == GUIElementBase::Type::Panel) |
511 | { |
512 | const auto layout = static_cast<GUILayout*>(child); |
513 | GUILayout::destroy(layout); |
514 | } |
515 | else if (child->_getType() == Type::FixedSpace) |
516 | { |
517 | const auto space = static_cast<GUIFixedSpace*>(child); |
518 | GUIFixedSpace::destroy(space); |
519 | } |
520 | else if (child->_getType() == Type::FlexibleSpace) |
521 | { |
522 | const auto space = static_cast<GUIFlexibleSpace*>(child); |
523 | GUIFlexibleSpace::destroy(space); |
524 | } |
525 | } |
526 | |
527 | assert(mChildren.empty()); |
528 | } |
529 | |
530 | void GUIElementBase::_changeParentWidget(GUIWidget* widget) |
531 | { |
532 | assert(!_isDestroyed()); |
533 | |
534 | if (mParentWidget != widget) |
535 | { |
536 | if (mParentWidget != nullptr) |
537 | mParentWidget->_unregisterElement(this); |
538 | |
539 | if (widget != nullptr) |
540 | widget->_registerElement(this); |
541 | } |
542 | |
543 | mParentWidget = widget; |
544 | |
545 | for(auto& child : mChildren) |
546 | { |
547 | child->_changeParentWidget(widget); |
548 | } |
549 | |
550 | _markLayoutAsDirty(); |
551 | } |
552 | |
553 | void GUIElementBase::_updateAUParents() |
554 | { |
555 | GUIElementBase* updateParent = nullptr; |
556 | if (mParentElement != nullptr) |
557 | { |
558 | updateParent = mParentElement->findUpdateParent(); |
559 | |
560 | // If parent is a panel then we can do an optimization and only update |
561 | // one child instead of all of them, so change parent to that child. |
562 | if (updateParent != nullptr && updateParent->_getType() == GUIElementBase::Type::Panel) |
563 | { |
564 | GUIElementBase* optimizedUpdateParent = this; |
565 | while (optimizedUpdateParent->_getParent() != updateParent) |
566 | optimizedUpdateParent = optimizedUpdateParent->_getParent(); |
567 | |
568 | updateParent = optimizedUpdateParent; |
569 | } |
570 | } |
571 | |
572 | GUIPanel* anchorParent = nullptr; |
573 | GUIElementBase* currentParent = mParentElement; |
574 | while (currentParent != nullptr) |
575 | { |
576 | if (currentParent->_getType() == Type::Panel) |
577 | { |
578 | anchorParent = static_cast<GUIPanel*>(currentParent); |
579 | break; |
580 | } |
581 | |
582 | currentParent = currentParent->mParentElement; |
583 | } |
584 | |
585 | setAnchorParent(anchorParent); |
586 | setUpdateParent(updateParent); |
587 | } |
588 | |
589 | GUIElementBase* GUIElementBase::findUpdateParent() |
590 | { |
591 | GUIElementBase* currentElement = this; |
592 | while (currentElement != nullptr) |
593 | { |
594 | const GUIDimensions& parentDimensions = currentElement->_getDimensions(); |
595 | bool boundsDependOnChildren = !parentDimensions.fixedHeight() || !parentDimensions.fixedWidth(); |
596 | |
597 | if (!boundsDependOnChildren) |
598 | return currentElement; |
599 | |
600 | currentElement = currentElement->mParentElement; |
601 | } |
602 | |
603 | return nullptr; |
604 | } |
605 | |
606 | void GUIElementBase::refreshChildUpdateParents() |
607 | { |
608 | GUIElementBase* updateParent = findUpdateParent(); |
609 | |
610 | for (auto& child : mChildren) |
611 | { |
612 | GUIElementBase* childUpdateParent = updateParent; |
613 | |
614 | // If parent is a panel then we can do an optimization and only update |
615 | // one child instead of all of them, so change parent to that child. |
616 | if (childUpdateParent != nullptr && childUpdateParent->_getType() == GUIElementBase::Type::Panel) |
617 | { |
618 | GUIElementBase* optimizedUpdateParent = child; |
619 | while (optimizedUpdateParent->_getParent() != childUpdateParent) |
620 | optimizedUpdateParent = optimizedUpdateParent->_getParent(); |
621 | |
622 | childUpdateParent = optimizedUpdateParent; |
623 | } |
624 | |
625 | child->setUpdateParent(childUpdateParent); |
626 | } |
627 | } |
628 | |
629 | void GUIElementBase::setAnchorParent(GUIPanel* anchorParent) |
630 | { |
631 | mAnchorParent = anchorParent; |
632 | |
633 | if (_getType() == Type::Panel) |
634 | return; |
635 | |
636 | for (auto& child : mChildren) |
637 | child->setAnchorParent(anchorParent); |
638 | } |
639 | |
640 | void GUIElementBase::setUpdateParent(GUIElementBase* updateParent) |
641 | { |
642 | mUpdateParent = updateParent; |
643 | |
644 | const GUIDimensions& dimensions = _getDimensions(); |
645 | bool boundsDependOnChildren = !dimensions.fixedHeight() || !dimensions.fixedWidth(); |
646 | |
647 | if (!boundsDependOnChildren) |
648 | return; |
649 | |
650 | for (auto& child : mChildren) |
651 | child->setUpdateParent(updateParent); |
652 | } |
653 | } |
654 | |