File indexing completed on 2024-05-12 03:47:25
0001 /* 0002 File : AbstractAspect.cpp 0003 Project : LabPlot 0004 Description : Base class for all objects in a Project. 0005 -------------------------------------------------------------------- 0006 SPDX-FileCopyrightText: 2007-2009 Tilman Benkert <thzs@gmx.net> 0007 SPDX-FileCopyrightText: 2007-2010 Knut Franke <knut.franke@gmx.de> 0008 SPDX-FileCopyrightText: 2011-2022 Alexander Semke <alexander.semke@web.de> 0009 SPDX-FileCopyrightText: 2023 Stefan Gerlach <stefan.gerlach@uni.kn> 0010 0011 SPDX-License-Identifier: GPL-2.0-or-later 0012 */ 0013 0014 #include "backend/core/AbstractAspect.h" 0015 #include "backend/core/AspectFactory.h" 0016 #include "backend/core/AspectPrivate.h" 0017 #include "backend/core/Project.h" 0018 #include "backend/core/aspectcommands.h" 0019 #include "backend/lib/PropertyChangeCommand.h" 0020 #include "backend/lib/SignallingUndoCommand.h" 0021 #include "backend/lib/XmlStreamReader.h" 0022 #include "backend/lib/trace.h" 0023 0024 #include <KStandardAction> 0025 #include <QClipboard> 0026 #include <QMenu> 0027 #include <QMimeData> 0028 0029 /** 0030 * \class AbstractAspect 0031 * \brief Base class of all persistent objects in a Project. 0032 * 0033 * Before going into the details, it's useful to understand the ideas behind the 0034 * \ref aspect "Aspect Framework". 0035 * 0036 * Aspects organize themselves into trees, where a parent takes ownership of its children. Usually, 0037 * though not necessarily, a Project instance will sit at the root of the tree (without a Project 0038 * ancestor, project() will return 0 and undo does not work). Children are organized using 0039 * addChild(), removeChild(), child(), indexOfChild() and childCount() on the parent's side as well 0040 * as the equivalent convenience methods index() and remove() on the child's side. 0041 * In contrast to the similar feature of QObject, Aspect trees are fully undo/redo aware and provide 0042 * signals around object adding/removal. 0043 * 0044 * AbstractAspect manages for every Aspect the properties #name, #comment, #captionSpec and 0045 * #creationTime. All of these translate into the caption() as described in the documentation 0046 * of setCaptionSpec(). 0047 * 0048 * If an undoStack() can be found (usually it is managed by Project), changes to the properties 0049 * as well as adding/removing children support multi-level undo/redo. In order to support undo/redo 0050 * for problem-specific data in derived classes, make sure that all changes to your data are done 0051 * by handing appropriate commands to exec(). 0052 */ 0053 0054 /** 0055 * \enum AbstractAspect::ChildIndexFlag 0056 * \brief Flags which control numbering scheme of children. 0057 */ 0058 /** 0059 * \var AbstractAspect::IncludeHidden 0060 * \brief Include aspects marked as "hidden" in numbering or listing children. 0061 */ 0062 /** 0063 * \var AbstractAspect::Recursive 0064 * \brief Recursively handle all descendents, not just immediate children. 0065 */ 0066 /** 0067 * \var AbstractAspect::Compress 0068 * \brief Remove all null pointers from the result list. 0069 */ 0070 0071 //////////////////////////////////////////////////////////////////////////////////////////////////// 0072 // documentation of template and inline methods 0073 //////////////////////////////////////////////////////////////////////////////////////////////////// 0074 0075 /** 0076 * \fn template < class T > T *AbstractAspect::ancestor() const 0077 * \brief Return the closest ancestor of class T (or NULL if none found). 0078 */ 0079 0080 /** 0081 * \fn template < class T > QVector<T*> AbstractAspect::children(const ChildIndexFlags &flags=0) const 0082 * \brief Return list of children inheriting from class T. 0083 * 0084 * Use AbstractAspect for T in order to get all children. 0085 */ 0086 0087 /** 0088 * \fn template < class T > T *AbstractAspect::child(int index, const ChildIndexFlags &flags=0) const 0089 * \brief Return child identified by (0 based) index and class. 0090 * 0091 * Identifying objects by an index is inherently error-prone and confusing, 0092 * given that the index can be based on different criteria (viz, counting 0093 * only instances of specific classes and including/excluding hidden 0094 * aspects). Therefore, it is recommended to avoid indices wherever possible 0095 * and instead refer to aspects using AbstractAspect pointers. 0096 */ 0097 0098 /** 0099 * \fn template < class T > T *AbstractAspect::child(const QString &name) const 0100 * \brief Get child by name and class. 0101 */ 0102 0103 /** 0104 * \fn template < class T > int AbstractAspect::childCount(const ChildIndexFlags &flags=0) const 0105 * \brief Return the number of child Aspects inheriting from given class. 0106 */ 0107 0108 /** 0109 * \fn template < class T > int AbstractAspect::indexOfChild(const AbstractAspect * child, const ChildIndexFlags &flags=0) const 0110 * \brief Return (0 based) index of child in the list of children inheriting from class T. 0111 */ 0112 0113 /** 0114 * \fn void AbstractAspect::aspectDescriptionAboutToChange(const AbstractAspect *aspect) 0115 * \brief Emitted before the name, comment or caption spec is changed 0116 */ 0117 0118 /** 0119 * \fn void AbstractAspect::aspectDescriptionChanged(const AbstractAspect *aspect) 0120 * \brief Emitted after the name, comment or caption spec have changed 0121 */ 0122 0123 /** 0124 * \fn void AbstractAspect::aspectAboutToBeAdded(const AbstractAspect *parent, const AbstractAspect *before, const AbstractAspect * child) 0125 * \brief Emitted before a new child is inserted 0126 */ 0127 0128 /** 0129 * \fn void AbstractAspect::aspectAdded(const AbstractAspect *aspect) 0130 * \brief Emitted after a new Aspect has been added to the tree 0131 */ 0132 0133 /** 0134 * \fn void AbstractAspect::aspectAboutToBeRemoved(const AbstractAspect *aspect) 0135 * \brief Emitted before an aspect is removed from its parent 0136 */ 0137 0138 /** 0139 * \fn void AbstractAspect::aspectRemoved(const AbstractAspect *parent, const AbstractAspect * before, const AbstractAspect * child) 0140 * \brief Emitted from the parent after removing a child 0141 */ 0142 0143 /** 0144 * \fn void AbstractAspect::aspectHiddenAboutToChange(const AbstractAspect *aspect) 0145 * \brief Emitted before the hidden attribute is changed 0146 */ 0147 0148 /** 0149 * \fn void AbstractAspect::aspectHiddenChanged(const AbstractAspect *aspect) 0150 * \brief Emitted after the hidden attribute has changed 0151 */ 0152 0153 /** 0154 * \fn void AbstractAspect::statusInfo(const QString &text) 0155 * \brief Emitted whenever some aspect in the tree wants to give status information to the user 0156 * \sa info(const QString&) 0157 */ 0158 0159 /** 0160 * \fn protected void AbstractAspect::info(const QString &text) 0161 * \brief Implementations should call this whenever status information should be given to the user. 0162 * 0163 * This will cause statusInfo() to be emitted. Typically, this will cause the specified string 0164 * to be displayed in a status bar, a log window or some similar non-blocking way so as not to 0165 * disturb the workflow. 0166 */ 0167 0168 /** 0169 * \fn protected virtual void childSelected(const AbstractAspect*) {} 0170 * \brief called when a child's child aspect was selected in the model 0171 */ 0172 0173 /** 0174 * \fn protected virtual void childDeselected() 0175 * \brief called when a child aspect was deselected in the model 0176 */ 0177 0178 /** 0179 * \fn protected virtual void childDeselected(const AbstractAspect*) 0180 * \brief called when a child's child aspect was deselected in the model 0181 */ 0182 0183 //////////////////////////////////////////////////////////////////////////////////////////////////// 0184 // start of AbstractAspect implementation 0185 //////////////////////////////////////////////////////////////////////////////////////////////////// 0186 0187 AbstractAspect::AbstractAspect(const QString& name, AspectType type) 0188 : m_type(type) 0189 , d(new AbstractAspectPrivate(this, name)) { 0190 } 0191 0192 AbstractAspect::~AbstractAspect() { 0193 delete d; 0194 } 0195 0196 QString AbstractAspect::name() const { 0197 return d->m_name; 0198 } 0199 0200 QUuid AbstractAspect::uuid() const { 0201 return d->m_uuid; 0202 } 0203 0204 void AbstractAspect::setSuppressWriteUuid(bool suppress) { 0205 d->m_suppressWriteUuid = suppress; 0206 } 0207 0208 /*! 0209 * \brief AbstractAspect::setName 0210 * sets the name of the abstract aspect 0211 * \param value - the new value for the name that needs to be checked and made unique if it's not the case yet 0212 * \param autoUnique - if set to \true the new name is automatically made unique, the name is not changed and \c false is returned otherwise. default is \true. 0213 * \param skipAutoUnique - if set to \true, don't check for uniqueness, the caller has to guarantee the uniqueness. default is \false. 0214 * \return returns, if the new name is valid or not 0215 */ 0216 bool AbstractAspect::setName(const QString& value, NameHandling handling, QUndoCommand* /*parent*/) { 0217 if (value.isEmpty()) 0218 return setName(QLatin1String("1"), handling); 0219 0220 if (value == d->m_name) 0221 return true; // name not changed, but the name is valid 0222 0223 QString new_name; 0224 if ((handling == NameHandling::UniqueRequired || handling == NameHandling::AutoUnique) && d->m_parent) { 0225 new_name = d->m_parent->uniqueNameFor(value); 0226 0227 if (handling == NameHandling::UniqueRequired && new_name.compare(value) != 0) // value is not unique, so don't change name 0228 return false; // this value is used in the dock to check if the name is valid 0229 0230 // NameHandling::Autounique 0231 if (new_name != value) 0232 info(i18n(R"(Intended name "%1" was changed to "%2" in order to avoid name collision.)", value, new_name)); 0233 } else 0234 new_name = value; 0235 exec(new AspectNameChangeCmd(this->d, new_name)); 0236 return true; 0237 } 0238 0239 QString AbstractAspect::comment() const { 0240 return d->m_comment; 0241 } 0242 0243 void AbstractAspect::setComment(const QString& value) { 0244 if (value == d->m_comment) 0245 return; 0246 exec(new PropertyChangeCommand<QString>(i18n("%1: change comment", d->m_name), &d->m_comment, value), 0247 "aspectDescriptionAboutToChange", 0248 "aspectDescriptionChanged", 0249 QArgument<const AbstractAspect*>("const AbstractAspect*", this)); 0250 } 0251 0252 void AbstractAspect::setCreationTime(const QDateTime& time) { 0253 d->m_creation_time = time; 0254 } 0255 0256 QDateTime AbstractAspect::creationTime() const { 0257 return d->m_creation_time; 0258 } 0259 0260 bool AbstractAspect::hidden() const { 0261 return d->m_hidden; 0262 } 0263 0264 /** 0265 * \brief Set "hidden" property, i.e. whether to exclude this aspect from being shown in the explorer. 0266 */ 0267 void AbstractAspect::setHidden(bool value) { 0268 if (value == d->m_hidden) 0269 return; 0270 d->m_hidden = value; 0271 } 0272 0273 /** 0274 * \brief Set "fixed" property which defines whether the object can be renamed, deleted, etc. 0275 */ 0276 void AbstractAspect::setFixed(bool value) { 0277 if (value == d->m_fixed) 0278 return; 0279 d->m_fixed = value; 0280 } 0281 0282 bool AbstractAspect::isFixed() const { 0283 return d->m_fixed; 0284 } 0285 0286 void AbstractAspect::setMoved(bool value) { 0287 d->m_moved = value; 0288 } 0289 0290 bool AbstractAspect::isMoved() const { 0291 return d->m_moved; 0292 } 0293 0294 void AbstractAspect::setIsLoading(bool load) { 0295 d->m_isLoading = load; 0296 } 0297 0298 bool AbstractAspect::isLoading() const { 0299 return d->m_isLoading; 0300 } 0301 0302 /** 0303 * \brief Return an icon to be used for decorating my views. 0304 */ 0305 QIcon AbstractAspect::icon() const { 0306 return {}; 0307 } 0308 0309 /** 0310 * \brief Return a new context menu. 0311 * 0312 * The caller takes ownership of the menu. 0313 */ 0314 QMenu* AbstractAspect::createContextMenu() { 0315 QMenu* menu = new QMenu(); 0316 menu->addSection(this->name()); 0317 0318 // TODO: activate this again when the functionality is implemented 0319 // menu->addAction( KStandardAction::cut(this) ); 0320 0321 QAction* actionDuplicate = nullptr; 0322 if (!isFixed() && m_type != AspectType::Project && m_type != AspectType::CantorWorksheet) { 0323 // copy action: 0324 // don't allow to copy fixed aspects 0325 auto* action = KStandardAction::copy(this); 0326 connect(action, &QAction::triggered, this, &AbstractAspect::copy); 0327 menu->addAction(action); 0328 0329 // duplicate action: 0330 // don't allow to duplicate legends in the plots 0331 if (m_type != AspectType::CartesianPlotLegend) { 0332 actionDuplicate = new QAction(QIcon::fromTheme(QLatin1String("edit-copy")), i18n("Duplicate Here"), this); 0333 actionDuplicate->setShortcut(Qt::CTRL | Qt::Key_D); 0334 connect(actionDuplicate, &QAction::triggered, this, &AbstractAspect::duplicate); 0335 menu->addAction(actionDuplicate); 0336 } 0337 } 0338 0339 // paste action: 0340 // determine the aspect type of the content available in the clipboard 0341 // and enable the paste entry if the content is labplot specific 0342 // and if it can be pasted into the current aspect 0343 QString name; 0344 auto t = clipboardAspectType(name); 0345 if (t != AspectType::AbstractAspect && pasteTypes().indexOf(t) != -1) { 0346 auto* action = KStandardAction::paste(this); 0347 action->setText(i18n("Paste '%1'", name)); 0348 if (actionDuplicate) 0349 menu->insertAction(actionDuplicate, action); 0350 else 0351 menu->addAction(action); 0352 connect(action, &QAction::triggered, this, &AbstractAspect::paste); 0353 } 0354 menu->addSeparator(); 0355 0356 // action to create data spreadsheet based on the results of the calculations for types that support it 0357 QAction* actionDataSpreadsheet = new QAction(QIcon::fromTheme(QLatin1String("labplot-spreadsheet")), i18n("Create Data Spreadsheet"), this); 0358 0359 // handle types that support it 0360 bool dataAvailable = false; 0361 if (const auto* analysisCurve = dynamic_cast<XYAnalysisCurve*>(this)) { 0362 if (analysisCurve->resultAvailable()) { 0363 connect(actionDataSpreadsheet, &QAction::triggered, static_cast<XYAnalysisCurve*>(this), &XYAnalysisCurve::createDataSpreadsheet); 0364 dataAvailable = true; 0365 } 0366 } else if (const auto* equationCurve = dynamic_cast<XYEquationCurve*>(this)) { 0367 if (equationCurve->dataAvailable()) { 0368 connect(actionDataSpreadsheet, &QAction::triggered, static_cast<XYEquationCurve*>(this), &XYEquationCurve::createDataSpreadsheet); 0369 dataAvailable = true; 0370 } 0371 } else if (const auto* histogram = dynamic_cast<Histogram*>(this)) { 0372 if (histogram->bins()) { 0373 connect(actionDataSpreadsheet, &QAction::triggered, static_cast<Histogram*>(this), &Histogram::createDataSpreadsheet); 0374 dataAvailable = true; 0375 } 0376 } else if (const auto* boxPlot = dynamic_cast<BoxPlot*>(this)) { 0377 if (!boxPlot->dataColumns().isEmpty()) { 0378 connect(actionDataSpreadsheet, &QAction::triggered, static_cast<BoxPlot*>(this), &BoxPlot::createDataSpreadsheet); 0379 dataAvailable = true; 0380 } 0381 } 0382 0383 if (dataAvailable) { 0384 menu->addAction(actionDataSpreadsheet); 0385 menu->addSeparator(); 0386 } 0387 0388 // don't allow to rename and delete fixed objects and 0389 // - columns in live-data source 0390 // - Mqtt subscriptions 0391 // - Mqtt topics 0392 // - Columns in Mqtt topics 0393 // TODO: make also these objects fixed and remove this additional handling for them here 0394 bool disabled = isFixed() || (m_type == AspectType::Column && parentAspect()->type() == AspectType::LiveDataSource) 0395 #ifdef HAVE_MQTT 0396 || (m_type == AspectType::MQTTSubscription) 0397 || (m_type == AspectType::MQTTTopic) | (m_type == AspectType::Column && parentAspect()->type() == AspectType::MQTTTopic) 0398 #endif 0399 ; 0400 0401 if (disabled) 0402 return menu; 0403 0404 // rename and delete actions: 0405 menu->addAction(QIcon::fromTheme(QLatin1String("edit-rename")), i18n("Rename"), this, &AbstractAspect::renameRequested); 0406 if (m_type != AspectType::Project) 0407 menu->addAction(QIcon::fromTheme(QLatin1String("edit-delete")), i18n("Delete"), this, QOverload<>::of(&AbstractAspect::remove)); 0408 0409 // move up and down actions: 0410 // don't shown them for worksheet elements since they implement their own "Drawing order" menu 0411 if (!dynamic_cast<WorksheetElement*>(this) && this != project()) { 0412 const auto* parent = parentAspect(); 0413 int count = parent->childCount<AbstractAspect>(); 0414 if (count > 1) { 0415 auto* moveMenu = new QMenu(i18n("Move"), menu); 0416 moveMenu->setIcon(QIcon::fromTheme(QStringLiteral("layer-bottom"))); 0417 if (parent->indexOfChild<AbstractAspect>(this) != 0) 0418 moveMenu->addAction(QIcon::fromTheme(QStringLiteral("draw-arrow-up")), i18n("Up"), this, &AbstractAspect::moveUp); 0419 0420 if (parent->indexOfChild<AbstractAspect>(this) != count - 1) 0421 moveMenu->addAction(QIcon::fromTheme(QStringLiteral("draw-arrow-down")), i18n("Down"), this, &AbstractAspect::moveDown); 0422 menu->addSeparator(); 0423 menu->addMenu(moveMenu); 0424 } 0425 } 0426 0427 return menu; 0428 } 0429 0430 AspectType AbstractAspect::type() const { 0431 return m_type; 0432 } 0433 0434 bool AbstractAspect::inherits(AspectType type) const { 0435 return (static_cast<quint64>(m_type) & static_cast<quint64>(type)) == static_cast<quint64>(type); 0436 } 0437 0438 /** 0439 * \brief In the parent-child hierarchy, return the first parent of type \param type or null pointer if there is none. 0440 */ 0441 AbstractAspect* AbstractAspect::parent(AspectType type) const { 0442 AbstractAspect* parent = parentAspect(); 0443 if (!parent) 0444 return nullptr; 0445 0446 if (parent->inherits(type)) 0447 return parent; 0448 0449 return parent->parent(type); 0450 } 0451 0452 /** 0453 * \brief Return my parent Aspect or 0 if I currently don't have one. 0454 */ 0455 AbstractAspect* AbstractAspect::parentAspect() const { 0456 return d->m_parent; 0457 } 0458 0459 void AbstractAspect::setParentAspect(AbstractAspect* parent) { 0460 d->m_parent = parent; 0461 } 0462 0463 /** 0464 * \brief Return the folder the Aspect is contained in or 0 if there is none. 0465 * 0466 * The returned folder may be the aspect itself if it inherits Folder. 0467 */ 0468 Folder* AbstractAspect::folder() { 0469 if (inherits(AspectType::Folder)) 0470 return static_cast<class Folder*>(this); 0471 AbstractAspect* parent_aspect = parentAspect(); 0472 while (parent_aspect && !parent_aspect->inherits(AspectType::Folder)) 0473 parent_aspect = parent_aspect->parentAspect(); 0474 return static_cast<class Folder*>(parent_aspect); 0475 } 0476 0477 /** 0478 * \brief Return whether the there is a path upwards to the given aspect 0479 * 0480 * This also returns true if other==this. 0481 */ 0482 bool AbstractAspect::isDescendantOf(AbstractAspect* other) { 0483 if (other == this) 0484 return true; 0485 AbstractAspect* parent_aspect = parentAspect(); 0486 while (parent_aspect) { 0487 if (parent_aspect == other) 0488 return true; 0489 parent_aspect = parent_aspect->parentAspect(); 0490 } 0491 return false; 0492 } 0493 0494 /** 0495 * \brief Return the Project this Aspect belongs to, or 0 if it is currently not part of one. 0496 */ 0497 Project* AbstractAspect::project() { 0498 return parentAspect() ? parentAspect()->project() : nullptr; 0499 } 0500 0501 /** 0502 * \brief Return the path that leads from the top-most Aspect (usually a Project) to me. 0503 */ 0504 QString AbstractAspect::path() const { 0505 return parentAspect() ? parentAspect()->path() + QLatin1Char('/') + name() : QString(); 0506 } 0507 0508 /** 0509 * \brief Add the given Aspect to my list of children. 0510 */ 0511 void AbstractAspect::addChild(AbstractAspect* child, QUndoCommand* parent) { 0512 Q_CHECK_PTR(child); 0513 0514 const QString new_name = uniqueNameFor(child->name()); 0515 bool execute = false; 0516 if (!parent) { 0517 execute = true; 0518 parent = new QUndoCommand(i18n("%1: add %2", name(), new_name)); 0519 } 0520 if (new_name != child->name()) { 0521 info(i18n(R"(Renaming "%1" to "%2" in order to avoid name collision.)", child->name(), new_name)); 0522 child->setName(new_name, NameHandling::AutoUnique, parent); 0523 } 0524 0525 new AspectChildAddCmd(d, child, d->m_children.count(), parent); 0526 0527 if (execute) 0528 exec(parent); 0529 } 0530 0531 /** 0532 * \brief Add the given Aspect to my list of children without any checks and without putting this step onto the undo-stack 0533 */ 0534 void AbstractAspect::addChildFast(AbstractAspect* child) { 0535 Q_EMIT childAspectAboutToBeAdded(this, nullptr, child); // TODO: before-pointer is 0 here, also in the commands classes. why? 0536 d->insertChild(d->m_children.count(), child); 0537 child->finalizeAdd(); 0538 // PERFTRACE(Q_FUNC_INFO); 0539 Q_EMIT childAspectAdded(child); 0540 // print_callstack(); 0541 } 0542 0543 /** 0544 * \brief Insert the given Aspect at a specific position in my list of children. 0545 */ 0546 void AbstractAspect::insertChildBefore(AbstractAspect* child, AbstractAspect* before, QUndoCommand* parent) { 0547 insertChild(child, d->indexOfChild(before), parent); 0548 } 0549 0550 void AbstractAspect::insertChild(AbstractAspect* child, int index, QUndoCommand* parent) { 0551 Q_CHECK_PTR(child); 0552 0553 if (index == -1) 0554 index = d->m_children.count(); 0555 0556 QString new_name = uniqueNameFor(child->name()); 0557 bool execute = false; 0558 if (!parent) { 0559 execute = true; 0560 const auto* before = this->child<AbstractAspect>(index); 0561 parent = 0562 new QUndoCommand(before ? i18n("%1: insert %2 before %3", name(), new_name, before->name()) : i18n("%1: insert %2 before end", name(), new_name)); 0563 } 0564 0565 if (new_name != child->name()) { 0566 info(i18n(R"(Renaming "%1" to "%2" in order to avoid name collision.)", child->name(), new_name)); 0567 child->setName(new_name, NameHandling::AutoUnique, parent); 0568 } 0569 0570 new AspectChildAddCmd(d, child, index, parent); 0571 0572 if (execute) 0573 exec(parent); 0574 } 0575 0576 /** 0577 * \brief Insert the given Aspect at a specific position in my list of children.without any checks and without putting this step onto the undo-stack 0578 */ 0579 void AbstractAspect::insertChildBeforeFast(AbstractAspect* child, AbstractAspect* before) { 0580 connect(child, &AbstractAspect::selected, this, &AbstractAspect::childSelected); 0581 connect(child, &AbstractAspect::deselected, this, &AbstractAspect::childDeselected); 0582 0583 int index = d->indexOfChild(before); 0584 if (index == -1) 0585 index = d->m_children.count(); 0586 0587 Q_EMIT childAspectAboutToBeAdded(this, nullptr, child); 0588 d->insertChild(index, child); 0589 child->finalizeAdd(); 0590 Q_EMIT childAspectAdded(child); 0591 } 0592 0593 /** 0594 * \brief Remove the given Aspect from my list of children. 0595 * 0596 * The ownership of the child is transferred to the undo command, 0597 * i.e., the aspect is deleted by the undo command. 0598 * \sa reparent() 0599 */ 0600 void AbstractAspect::removeChild(AbstractAspect* child, QUndoCommand* parent) { 0601 // QDEBUG(Q_FUNC_INFO << ", CHILD =" << child << ", PARENT =" << child->parentAspect()) 0602 0603 bool execute = false; 0604 if (!parent) { 0605 execute = true; 0606 parent = new QUndoCommand(i18n("%1: remove %2", name(), child->name())); 0607 } 0608 0609 new AspectChildRemoveCmd(d, child, parent); 0610 0611 if (execute) 0612 exec(parent); 0613 } 0614 0615 /** 0616 * \brief Remove all child Aspects. 0617 */ 0618 void AbstractAspect::removeAllChildren() { 0619 beginMacro(i18n("%1: remove all children", name())); 0620 0621 QVector<AbstractAspect*> children_list = children(); 0622 QVector<AbstractAspect*>::const_iterator i = children_list.constBegin(); 0623 AbstractAspect *current = nullptr, *nextSibling = nullptr; 0624 if (i != children_list.constEnd()) { 0625 current = *i; 0626 if (++i != children_list.constEnd()) 0627 nextSibling = *i; 0628 } 0629 0630 while (current) { 0631 Q_EMIT childAspectAboutToBeRemoved(current); 0632 exec(new AspectChildRemoveCmd(d, current)); 0633 Q_EMIT childAspectRemoved(this, nextSibling, current); 0634 0635 current = nextSibling; 0636 if (i != children_list.constEnd() && ++i != children_list.constEnd()) 0637 nextSibling = *i; 0638 else 0639 nextSibling = nullptr; 0640 } 0641 0642 endMacro(); 0643 } 0644 0645 /** 0646 * \brief Move a child to another parent aspect and transfer ownership. 0647 */ 0648 void AbstractAspect::reparent(AbstractAspect* newParent, int newIndex) { 0649 Q_ASSERT(parentAspect()); 0650 Q_ASSERT(newParent); 0651 int max_index = newParent->childCount<AbstractAspect>(ChildIndexFlag::IncludeHidden); 0652 if (newIndex == -1) 0653 newIndex = max_index; 0654 Q_ASSERT(newIndex >= 0 && newIndex <= max_index); 0655 0656 // AbstractAspect* old_parent = parentAspect(); 0657 // int old_index = old_parent->indexOfChild<AbstractAspect>(this, IncludeHidden); 0658 // auto* old_sibling = old_parent->child<AbstractAspect>(old_index+1, IncludeHidden); 0659 // auto* new_sibling = newParent->child<AbstractAspect>(newIndex, IncludeHidden); 0660 0661 // Q_EMIT newParent->aspectAboutToBeAdded(newParent, new_sibling, this); 0662 exec(new AspectChildReparentCmd(parentAspect()->d, newParent->d, this, newIndex)); 0663 // Q_EMIT old_parent->aspectRemoved(old_parent, old_sibling, this); 0664 } 0665 0666 QVector<AbstractAspect*> AbstractAspect::children(AspectType type, ChildIndexFlags flags) const { 0667 QVector<AbstractAspect*> result; 0668 for (auto* child : children()) { 0669 if (flags & ChildIndexFlag::IncludeHidden || !child->hidden()) { 0670 if (child->inherits(type)) 0671 result << child; 0672 0673 if (flags & ChildIndexFlag::Recursive) 0674 result << child->children(type, flags); 0675 } 0676 } 0677 0678 return result; 0679 } 0680 0681 const QVector<AbstractAspect*>& AbstractAspect::children() const { 0682 Q_ASSERT(d); 0683 return d->m_children; 0684 } 0685 0686 /** 0687 * \brief Remove me from my parent's list of children. 0688 */ 0689 void AbstractAspect::remove(QUndoCommand* parent) { 0690 if (parentAspect()) 0691 parentAspect()->removeChild(this, parent); 0692 } 0693 0694 void AbstractAspect::remove() { 0695 remove(nullptr); 0696 } 0697 0698 void AbstractAspect::moveUp() { 0699 auto* parent = parentAspect(); 0700 int index = parent->indexOfChild<AbstractAspect>(this); 0701 auto* sibling = parent->child<AbstractAspect>(index - 1); 0702 beginMacro(i18n("%1: move up", name())); 0703 setMoved(true); 0704 remove(); 0705 parent->insertChildBefore(this, sibling); 0706 setMoved(false); 0707 endMacro(); 0708 } 0709 0710 void AbstractAspect::moveDown() { 0711 auto* parent = parentAspect(); 0712 int index = parent->indexOfChild<AbstractAspect>(this); 0713 auto* sibling = parent->child<AbstractAspect>(index + 2); 0714 beginMacro(i18n("%1: move down", name())); 0715 setMoved(true); 0716 remove(); 0717 parent->insertChildBefore(this, sibling); 0718 setMoved(false); 0719 endMacro(); 0720 } 0721 0722 /*! 0723 * returns the list of all parent aspects (folders and sub-folders) 0724 */ 0725 QVector<AbstractAspect*> AbstractAspect::dependsOn() const { 0726 QVector<AbstractAspect*> aspects; 0727 if (parentAspect()) 0728 aspects << parentAspect() << parentAspect()->dependsOn(); 0729 0730 return aspects; 0731 } 0732 0733 /*! 0734 * return the list of all aspect types that can be copy&pasted into the current aspect. 0735 * returns an empty list on default, needs to be re-implemented in all derived classes 0736 * that want to allow other aspects to be pasted into. 0737 */ 0738 QVector<AspectType> AbstractAspect::pasteTypes() const { 0739 return {}; 0740 } 0741 0742 void AbstractAspect::setPasted(bool pasted) { 0743 d->m_pasted = pasted; 0744 } 0745 0746 bool AbstractAspect::pasted() const { 0747 return d->m_pasted; 0748 } 0749 0750 /*! 0751 * copies the aspect to the clipboard. The standard XML-serialization 0752 * via AbstractAspect::load() is used. 0753 */ 0754 void AbstractAspect::copy() { 0755 QString output; 0756 QXmlStreamWriter writer(&output); 0757 writer.writeStartDocument(); 0758 0759 // add LabPlot's copy&paste "identifier" 0760 writer.writeDTD(QLatin1String("<!DOCTYPE LabPlotCopyPasteXML>")); 0761 writer.writeStartElement(QStringLiteral("copy_content")); // root element 0762 0763 // write the type of the copied aspect 0764 writer.writeStartElement(QStringLiteral("type")); 0765 writer.writeAttribute(QStringLiteral("value"), QString::number(static_cast<int>(m_type))); 0766 writer.writeEndElement(); 0767 0768 setSuppressWriteUuid(true); 0769 const auto& children = this->children(AspectType::AbstractAspect, {ChildIndexFlag::IncludeHidden, ChildIndexFlag::Recursive}); 0770 for (const auto& child : children) 0771 child->setSuppressWriteUuid(true); 0772 0773 // write the aspect itself 0774 save(&writer); 0775 0776 for (const auto& child : children) 0777 child->setSuppressWriteUuid(false); 0778 setSuppressWriteUuid(false); 0779 0780 writer.writeEndElement(); // end the root-element 0781 writer.writeEndDocument(); 0782 QApplication::clipboard()->setText(output); 0783 } 0784 0785 void AbstractAspect::duplicate() { 0786 copy(); 0787 parentAspect()->paste(true); 0788 } 0789 0790 /*! 0791 * in case the clipboard containts a LabPlot's specific copy&paste content, 0792 * this function deserializes the XML string and adds the created aspect as 0793 * a child to the current aspect ("paste"). 0794 */ 0795 void AbstractAspect::paste(bool duplicate) { 0796 const QClipboard* clipboard = QApplication::clipboard(); 0797 const QMimeData* mimeData = clipboard->mimeData(); 0798 if (!mimeData->hasText()) 0799 return; 0800 0801 const QString& xml = clipboard->text(); 0802 if (!xml.startsWith(QLatin1String("<?xml version=\"1.0\"?><!DOCTYPE LabPlotCopyPasteXML>"))) 0803 return; 0804 0805 WAIT_CURSOR; 0806 AbstractAspect* aspect = nullptr; 0807 XmlStreamReader reader(xml); 0808 while (!reader.atEnd()) { 0809 reader.readNext(); 0810 0811 if (!reader.isStartElement()) 0812 continue; 0813 0814 if (reader.name() == QLatin1String("type")) { 0815 auto attribs = reader.attributes(); 0816 auto type = static_cast<AspectType>(attribs.value(QLatin1String("value")).toInt()); 0817 if (type != AspectType::AbstractAspect) 0818 aspect = AspectFactory::createAspect(type, this); 0819 } else { 0820 if (aspect) { 0821 aspect->setPasted(true); 0822 aspect->load(&reader, false); 0823 break; 0824 } 0825 } 0826 } 0827 0828 if (aspect) { 0829 if (!duplicate) 0830 beginMacro(i18n("%1: pasted '%2'", name(), aspect->name())); 0831 else { 0832 beginMacro(i18n("%1: duplicated '%2'", name(), aspect->name())); 0833 aspect->setName(i18n("Copy of '%1'", aspect->name())); 0834 } 0835 0836 if (aspect->type() != AspectType::CartesianPlotLegend) 0837 addChild(aspect); 0838 else { 0839 // spectial handling for the legend since only one single 0840 // legend object is allowed per plot 0841 auto* plot = static_cast<CartesianPlot*>(this); 0842 auto* legend = static_cast<CartesianPlotLegend*>(aspect); 0843 plot->addLegend(legend); 0844 } 0845 0846 project()->restorePointers(aspect); 0847 project()->retransformElements(aspect); 0848 aspect->setPasted(false); 0849 endMacro(); 0850 } 0851 RESET_CURSOR; 0852 } 0853 0854 /*! 0855 * helper function determening whether the current content of the clipboard 0856 * contants the labplot specific copy&paste XML content. In case a valid content 0857 * is available, the aspect type of the object to be pasted is returned. 0858 * AspectType::AbstractAspect is returned otherwise. 0859 */ 0860 AspectType AbstractAspect::clipboardAspectType(QString& name) { 0861 AspectType type = AspectType::AbstractAspect; 0862 const QClipboard* clipboard = QApplication::clipboard(); 0863 const QMimeData* mimeData = clipboard->mimeData(); 0864 if (!mimeData->hasText()) 0865 return type; 0866 0867 const QString& xml = clipboard->text(); 0868 if (!xml.startsWith(QLatin1String("<?xml version=\"1.0\"?><!DOCTYPE LabPlotCopyPasteXML>"))) 0869 return type; 0870 0871 XmlStreamReader reader(xml); 0872 bool typeFound = false; 0873 while (!reader.atEnd()) { 0874 reader.readNext(); 0875 if (reader.isStartElement()) { 0876 auto attribs = reader.attributes(); 0877 if (reader.name() == QLatin1String("type")) { 0878 type = static_cast<AspectType>(attribs.value(QLatin1String("value")).toInt()); 0879 typeFound = true; 0880 } else { 0881 name = attribs.value(QLatin1String("name")).toString(); 0882 if (typeFound) 0883 break; 0884 } 0885 } 0886 } 0887 0888 return type; 0889 } 0890 0891 bool AbstractAspect::isDraggable() const { 0892 return false; 0893 } 0894 0895 QVector<AspectType> AbstractAspect::dropableOn() const { 0896 return {}; 0897 } 0898 0899 //////////////////////////////////////////////////////////////////////////////////////////////////// 0900 //! \name serialize/deserialize 0901 //@{ 0902 //////////////////////////////////////////////////////////////////////////////////////////////////// 0903 0904 /** 0905 * \fn virtual void AbstractAspect::save(QXmlStreamWriter *) const 0906 * \brief Save as XML 0907 */ 0908 0909 /** 0910 * \fn virtual bool AbstractAspect::load(XmlStreamReader *) 0911 * \brief Load from XML 0912 * 0913 * XmlStreamReader supports errors as well as warnings. If only 0914 * warnings (non-critical errors) occur, this function must return 0915 * the reader at the end element corresponding to the current 0916 * element at the time the function was called. 0917 * 0918 * This function is normally intended to be called directly 0919 * after the ctor. If you want to call load on an aspect that 0920 * has been altered, you must make sure beforehand that 0921 * it is in the same state as after creation, e.g., remove 0922 * all its child aspects. 0923 * 0924 * \return false on error 0925 */ 0926 0927 /** 0928 * \brief Save the comment to XML 0929 */ 0930 void AbstractAspect::writeCommentElement(QXmlStreamWriter* writer) const { 0931 writer->writeStartElement(QLatin1String("comment")); 0932 writer->writeCharacters(comment()); 0933 writer->writeEndElement(); 0934 } 0935 0936 /** 0937 * \brief Load comment from an XML element 0938 */ 0939 bool AbstractAspect::readCommentElement(XmlStreamReader* reader) { 0940 d->m_comment = reader->readElementText(); 0941 return true; 0942 } 0943 0944 /** 0945 * \brief Save name and creation time to XML 0946 */ 0947 void AbstractAspect::writeBasicAttributes(QXmlStreamWriter* writer) const { 0948 writer->writeAttribute(QLatin1String("creation_time"), creationTime().toString(QLatin1String("yyyy-dd-MM hh:mm:ss:zzz"))); 0949 writer->writeAttribute(QLatin1String("name"), name()); 0950 if (!d->m_suppressWriteUuid) 0951 writer->writeAttribute(QLatin1String("uuid"), uuid().toString()); 0952 } 0953 0954 /** 0955 * \brief Load name and creation time from XML 0956 * 0957 * \return false on error 0958 */ 0959 bool AbstractAspect::readBasicAttributes(XmlStreamReader* reader) { 0960 const QXmlStreamAttributes& attribs = reader->attributes(); 0961 0962 // name 0963 QString str = attribs.value(QLatin1String("name")).toString(); 0964 if (str.isEmpty()) 0965 reader->raiseWarning(i18n("Attribute 'name' is missing or empty.")); 0966 0967 d->m_name = str; 0968 0969 // creation time 0970 str = attribs.value(QLatin1String("creation_time")).toString(); 0971 if (str.isEmpty()) { 0972 reader->raiseWarning(i18n("Invalid creation time for '%1'. Using current time.", name())); 0973 d->m_creation_time = QDateTime::currentDateTime(); 0974 } else { 0975 QDateTime creation_time = QDateTime::fromString(str, QLatin1String("yyyy-dd-MM hh:mm:ss:zzz")); 0976 if (creation_time.isValid()) 0977 d->m_creation_time = creation_time; 0978 else 0979 d->m_creation_time = QDateTime::currentDateTime(); 0980 } 0981 0982 str = attribs.value(QLatin1String("uuid")).toString(); 0983 if (!str.isEmpty()) { 0984 d->m_uuid = QUuid(str); 0985 } 0986 return true; 0987 } 0988 0989 //////////////////////////////////////////////////////////////////////////////////////////////////// 0990 //@} 0991 //////////////////////////////////////////////////////////////////////////////////////////////////// 0992 0993 //////////////////////////////////////////////////////////////////////////////////////////////////// 0994 //! \name undo related 0995 //@{ 0996 //////////////////////////////////////////////////////////////////////////////////////////////////// 0997 void AbstractAspect::setUndoAware(bool b) { 0998 d->m_undoAware = b; 0999 } 1000 1001 /** 1002 * \brief Return the undo stack of the Project, or 0 if this Aspect is not part of a Project. 1003 * 1004 * It's also possible to construct undo-enabled Aspect trees without Project. 1005 * The only requirement is that the root Aspect reimplements undoStack() to get the 1006 * undo stack from somewhere (the default implementation just delegates to parentAspect()). 1007 */ 1008 QUndoStack* AbstractAspect::undoStack() const { 1009 return parentAspect() ? parentAspect()->undoStack() : nullptr; 1010 } 1011 1012 /** 1013 * \brief Execute the given command, pushing it on the undoStack() if available. 1014 */ 1015 void AbstractAspect::exec(QUndoCommand* cmd) { 1016 Q_CHECK_PTR(cmd); 1017 if (d->m_undoAware) { 1018 QUndoStack* stack = undoStack(); 1019 if (stack) 1020 stack->push(cmd); 1021 else { 1022 cmd->redo(); 1023 delete cmd; 1024 } 1025 1026 if (project()) 1027 project()->setChanged(true); 1028 } else { 1029 cmd->redo(); 1030 delete cmd; 1031 } 1032 } 1033 1034 /** 1035 * \brief Execute command and arrange for signals to be sent before/after it is redone or undone. 1036 * 1037 * \arg \c command The command to be executed. 1038 * \arg \c preChangeSignal The name of the signal to be triggered before re-/undoing the command. 1039 * \arg \c postChangeSignal The name of the signal to be triggered after re-/undoing the command. 1040 * \arg <tt>val0,val1,val2,val3</tt> Arguments to the signals; to be given using Q_ARG(). 1041 * 1042 * Signal arguments are given using the macro Q_ARG(typename, const value&). Since 1043 * the variable given as "value" will likely be out of scope when the signals are emitted, a copy 1044 * needs to be created. This uses QMetaType, which means that (non-trivial) argument types need to 1045 * be registered using qRegisterMetaType() before giving them to exec() (in particular, this also 1046 * goes for pointers to custom data types). 1047 * 1048 * \sa SignallingUndoCommand 1049 */ 1050 void AbstractAspect::exec(QUndoCommand* command, 1051 const char* preChangeSignal, 1052 const char* postChangeSignal, 1053 QGenericArgument val0, 1054 QGenericArgument val1, 1055 QGenericArgument val2, 1056 QGenericArgument val3) { 1057 beginMacro(command->text()); 1058 exec(new SignallingUndoCommand(QLatin1String("change signal"), this, preChangeSignal, postChangeSignal, val0, val1, val2, val3)); 1059 exec(command); 1060 exec(new SignallingUndoCommand(QLatin1String("change signal"), this, postChangeSignal, preChangeSignal, val0, val1, val2, val3)); 1061 endMacro(); 1062 } 1063 1064 /** 1065 * \brief Begin an undo stack macro (series of commands) 1066 */ 1067 void AbstractAspect::beginMacro(const QString& text) { 1068 if (!d->m_undoAware) 1069 return; 1070 1071 QUndoStack* stack = undoStack(); 1072 if (stack) 1073 stack->beginMacro(text); 1074 } 1075 1076 /** 1077 * \brief End the current undo stack macro 1078 */ 1079 void AbstractAspect::endMacro() { 1080 if (!d->m_undoAware) 1081 return; 1082 1083 QUndoStack* stack = undoStack(); 1084 if (stack) 1085 stack->endMacro(); 1086 } 1087 1088 //////////////////////////////////////////////////////////////////////////////////////////////////// 1089 //@} 1090 //////////////////////////////////////////////////////////////////////////////////////////////////// 1091 1092 /*! 1093 * this function is called when the selection in ProjectExplorer was changed. 1094 * forwards the selection/deselection to the parent aspect via emitting a signal. 1095 */ 1096 void AbstractAspect::setSelected(bool s) { 1097 if (s) 1098 Q_EMIT selected(this); 1099 else 1100 Q_EMIT deselected(this); 1101 } 1102 1103 void AbstractAspect::childSelected(const AbstractAspect* aspect) { 1104 // forward the signal to the highest possible level in the parent-child hierarchy 1105 // e.g. axis of a plot was selected. Don't include parent aspects here that do not 1106 // need to react on the selection of children: 1107 //* Folder 1108 //* XYFitCurve with the child column for calculated residuals 1109 //* XYSmouthCurve with the child column for calculated rough values 1110 //* CantorWorksheet with the child columns for CAS variables 1111 AbstractAspect* parent = this->parentAspect(); 1112 if (parent && !parent->inherits(AspectType::Folder) && !parent->inherits(AspectType::XYFitCurve) && !parent->inherits(AspectType::XYSmoothCurve) 1113 && !parent->inherits(AspectType::CantorWorksheet)) 1114 Q_EMIT this->selected(aspect); 1115 } 1116 1117 void AbstractAspect::childDeselected(const AbstractAspect* aspect) { 1118 // forward the signal to the highest possible level in the parent-child hierarchy 1119 // e.g. axis of a plot was selected. Don't include parent aspects here that do not 1120 // need to react on the deselection of children: 1121 //* Folder 1122 //* XYFitCurve with the child column for calculated residuals 1123 //* XYSmouthCurve with the child column for calculated rough values 1124 //* CantorWorksheet with the child columns for CAS variables 1125 AbstractAspect* parent = this->parentAspect(); 1126 if (parent && !parent->inherits(AspectType::Folder) && !parent->inherits(AspectType::XYFitCurve) && !parent->inherits(AspectType::XYSmoothCurve) 1127 && !parent->inherits(AspectType::CantorWorksheet)) 1128 Q_EMIT this->deselected(aspect); 1129 } 1130 1131 /** 1132 * \brief Make the specified name unique among my children by incrementing a trailing number. 1133 */ 1134 QString AbstractAspect::uniqueNameFor(const QString& name) const { 1135 QStringList names; 1136 for (auto* child : children()) 1137 names << child->name(); 1138 1139 return uniqueNameFor(name, names); 1140 } 1141 1142 /*! 1143 * static helper function that makes the string \c name unique and avoids duplicates 1144 * in the list of strings \c names. 1145 */ 1146 QString AbstractAspect::uniqueNameFor(const QString& name, const QStringList& names) { 1147 if (!names.contains(name)) 1148 return name; 1149 1150 QString base = name; 1151 int last_non_digit; 1152 for (last_non_digit = base.size() - 1; last_non_digit >= 0; --last_non_digit) { 1153 if (base[last_non_digit].category() == QChar::Number_DecimalDigit) { 1154 base.chop(1); 1155 } else { 1156 if (base[last_non_digit].category() == QChar::Separator_Space) 1157 break; 1158 else { 1159 // non-digit character is found and it's not the separator, 1160 // the string either doesn't have any digits at all or is of 1161 // the form "data_2020.06". In this case we don't use anything 1162 // from the original name to increment the number 1163 last_non_digit = 0; 1164 base = name; 1165 break; 1166 } 1167 } 1168 } 1169 1170 if (last_non_digit >= 0 && base[last_non_digit].category() != QChar::Separator_Space) 1171 base.append(QLatin1Char(' ')); 1172 1173 int new_nr = QStringView(name).right(name.size() - base.size()).toInt(); 1174 QString new_name; 1175 do 1176 new_name = base + QString::number(++new_nr); 1177 while (names.contains(new_name)); 1178 1179 return new_name; 1180 } 1181 1182 void AbstractAspect::connectChild(AbstractAspect* child) { 1183 connect(child, &AbstractAspect::aspectDescriptionAboutToChange, this, &AbstractAspect::aspectDescriptionAboutToChange); 1184 connect(child, &AbstractAspect::aspectDescriptionChanged, this, &AbstractAspect::aspectDescriptionChanged); 1185 connect(child, 1186 QOverload<const AbstractAspect*, const AbstractAspect*, const AbstractAspect*>::of(&AbstractAspect::childAspectAboutToBeAdded), 1187 this, 1188 QOverload<const AbstractAspect*, const AbstractAspect*, const AbstractAspect*>::of(&AbstractAspect::childAspectAboutToBeAdded)); 1189 connect(child, &AbstractAspect::childAspectAdded, this, &AbstractAspect::childAspectAdded); 1190 connect(child, 1191 QOverload<const AbstractAspect*>::of(&AbstractAspect::childAspectAboutToBeRemoved), 1192 this, 1193 QOverload<const AbstractAspect*>::of(&AbstractAspect::childAspectAboutToBeRemoved)); 1194 connect(child, &AbstractAspect::childAspectRemoved, this, &AbstractAspect::childAspectRemoved); 1195 connect(child, &AbstractAspect::aspectHiddenAboutToChange, this, &AbstractAspect::aspectHiddenAboutToChange); 1196 connect(child, &AbstractAspect::aspectHiddenChanged, this, &AbstractAspect::aspectHiddenChanged); 1197 connect(child, &AbstractAspect::statusInfo, this, &AbstractAspect::statusInfo); 1198 1199 connect(child, &AbstractAspect::selected, this, &AbstractAspect::childSelected); 1200 connect(child, &AbstractAspect::deselected, this, &AbstractAspect::childDeselected); 1201 } 1202 1203 // ############################################################################## 1204 // ###################### Private implementation ############################### 1205 // ############################################################################## 1206 AbstractAspectPrivate::AbstractAspectPrivate(AbstractAspect* owner, const QString& name) 1207 : m_name(name.isEmpty() ? QLatin1String("1") : name) 1208 , q(owner) { 1209 m_creation_time = QDateTime::currentDateTime(); 1210 } 1211 1212 AbstractAspectPrivate::~AbstractAspectPrivate() { 1213 for (auto* child : qAsConst(m_children)) 1214 delete child; 1215 } 1216 1217 void AbstractAspectPrivate::insertChild(int index, AbstractAspect* child) { 1218 m_children.insert(index, child); 1219 1220 // Always remove from any previous parent before adding to a new one! 1221 // Can't handle this case here since two undo commands have to be created. 1222 Q_ASSERT(child->parentAspect() == nullptr); 1223 child->setParentAspect(q); 1224 q->connectChild(child); 1225 } 1226 1227 int AbstractAspectPrivate::indexOfChild(const AbstractAspect* child) const { 1228 for (int i = 0; i < m_children.size(); ++i) 1229 if (m_children.at(i) == child) 1230 return i; 1231 1232 return -1; 1233 }