File indexing completed on 2024-05-12 03:47:26
0001 /* 0002 File : AspectTreeModel.h 0003 Project : LabPlot 0004 Description : Represents a tree of AbstractAspect objects as a Qt item model. 0005 -------------------------------------------------------------------- 0006 SPDX-FileCopyrightText: 2007-2009 Knut Franke <knut.franke@gmx.de> 0007 SPDX-FileCopyrightText: 2007-2009 Tilman Benkert <thzs@gmx.net> 0008 SPDX-FileCopyrightText: 2011-2021 Alexander Semke <alexander.semke@web.de> 0009 SPDX-License-Identifier: GPL-2.0-or-later 0010 */ 0011 0012 #include "backend/core/AspectTreeModel.h" 0013 #include "backend/core/AbstractAspect.h" 0014 #include "backend/core/column/Column.h" 0015 #include "backend/worksheet/WorksheetElement.h" 0016 0017 #include <QApplication> 0018 #include <QDateTime> 0019 #include <QFontMetrics> 0020 #include <QIcon> 0021 #include <QMenu> 0022 0023 #include <KLocalizedString> 0024 0025 /** 0026 * \class AspectTreeModel 0027 * \brief Represents a tree of AbstractAspect objects as a Qt item model. 0028 * 0029 * This class is an adapter between an AbstractAspect hierarchy and Qt's view classes. 0030 * 0031 * It represents children of an Aspect as rows in the model, with the fixed columns 0032 * Name (AbstractAspect::name()), Type (the class name), Created (AbstractAspect::creationTime()) 0033 * and Comment (AbstractAspect::comment()). Name is decorated using AbstractAspect::icon(). 0034 * The tooltip for all columns is generated from AbstractAspect::caption(). 0035 * 0036 * Name and Comment are editable. 0037 * 0038 * For views which support this (currently ProjectExplorer), the menu created by 0039 * AbstractAspect::createContextMenu() is made available via the custom role ContextMenuRole. 0040 */ 0041 0042 /** 0043 * \enum AspectTreeModel::CustomDataRole 0044 * \brief Custom data roles used in addition to Qt::ItemDataRole 0045 */ 0046 /** 0047 * \var AspectTreeModel::ContextMenuRole 0048 * \brief pointer to a new context menu for an Aspect 0049 */ 0050 0051 /** 0052 * \fn QModelIndex AspectTreeModel::modelIndexOfAspect(const AbstractAspect *aspect, int column=0) const 0053 * \brief Convenience wrapper around QAbstractItemModel::createIndex(). 0054 */ 0055 0056 AspectTreeModel::AspectTreeModel(AbstractAspect* root, QObject* parent) 0057 : QAbstractItemModel(parent) 0058 , m_root(root) { 0059 connect(m_root, &AbstractAspect::renameRequested, this, &AspectTreeModel::renameRequestedSlot); 0060 connect(m_root, &AbstractAspect::aspectDescriptionChanged, this, &AspectTreeModel::aspectDescriptionChanged); 0061 connect(m_root, 0062 QOverload<const AbstractAspect*, const AbstractAspect*, const AbstractAspect*>::of(&AbstractAspect::childAspectAboutToBeAdded), 0063 this, 0064 QOverload<const AbstractAspect*, const AbstractAspect*, const AbstractAspect*>::of(&AspectTreeModel::aspectAboutToBeAdded)); 0065 connect(m_root, &AbstractAspect::childAspectAboutToBeRemoved, this, &AspectTreeModel::aspectAboutToBeRemoved); 0066 connect(m_root, &AbstractAspect::childAspectAdded, this, &AspectTreeModel::aspectAdded); 0067 connect(m_root, &AbstractAspect::childAspectRemoved, this, &AspectTreeModel::aspectRemoved); 0068 connect(m_root, &AbstractAspect::aspectHiddenAboutToChange, this, &AspectTreeModel::aspectHiddenAboutToChange); 0069 connect(m_root, &AbstractAspect::aspectHiddenChanged, this, &AspectTreeModel::aspectHiddenChanged); 0070 } 0071 0072 /*! 0073 \c list contains the class names of the aspects, that can be selected in the corresponding model view. 0074 */ 0075 void AspectTreeModel::setSelectableAspects(const QList<AspectType>& list) { 0076 m_selectableAspects = list; 0077 } 0078 0079 const QList<AspectType>& AspectTreeModel::selectableAspects() const { 0080 return m_selectableAspects; 0081 } 0082 0083 void AspectTreeModel::setReadOnly(bool readOnly) { 0084 m_readOnly = readOnly; 0085 } 0086 0087 void AspectTreeModel::enablePlottableColumnsOnly(bool value) { 0088 m_plottableColumnsOnly = value; 0089 } 0090 0091 void AspectTreeModel::enableNumericColumnsOnly(bool value) { 0092 m_numericColumnsOnly = value; 0093 } 0094 0095 void AspectTreeModel::enableNonEmptyNumericColumnsOnly(bool value) { 0096 m_nonEmptyNumericColumnsOnly = value; 0097 } 0098 0099 void AspectTreeModel::enableShowPlotDesignation(bool value) { 0100 m_showPlotDesignation = value; 0101 } 0102 0103 QModelIndex AspectTreeModel::index(int row, int column, const QModelIndex& parent) const { 0104 if (!hasIndex(row, column, parent)) 0105 return QModelIndex{}; 0106 0107 if (!parent.isValid()) { 0108 if (row != 0) 0109 return QModelIndex{}; 0110 return createIndex(row, column, m_root); 0111 } 0112 0113 auto* parent_aspect = static_cast<AbstractAspect*>(parent.internalPointer()); 0114 auto* child_aspect = parent_aspect->child<AbstractAspect>(row); 0115 if (!child_aspect) 0116 return QModelIndex{}; 0117 return createIndex(row, column, child_aspect); 0118 } 0119 0120 QModelIndex AspectTreeModel::parent(const QModelIndex& index) const { 0121 if (!index.isValid()) 0122 return QModelIndex{}; 0123 0124 auto* aspect = static_cast<AbstractAspect*>(index.internalPointer()); 0125 if (!aspect) 0126 return QModelIndex{}; 0127 0128 auto* parent = aspect->parentAspect(); 0129 if (!parent) 0130 return QModelIndex{}; 0131 0132 return modelIndexOfAspect(parent); 0133 } 0134 0135 int AspectTreeModel::rowCount(const QModelIndex& parent) const { 0136 if (!parent.isValid()) 0137 return 1; 0138 0139 auto* parent_aspect = static_cast<AbstractAspect*>(parent.internalPointer()); 0140 return parent_aspect->childCount<AbstractAspect>(); 0141 } 0142 0143 int AspectTreeModel::columnCount(const QModelIndex& /*parent*/) const { 0144 return 4; 0145 } 0146 0147 QVariant AspectTreeModel::headerData(int section, Qt::Orientation orientation, int role) const { 0148 if (orientation != Qt::Horizontal) 0149 return {}; 0150 0151 switch (role) { 0152 case Qt::DisplayRole: 0153 switch (section) { 0154 case 0: 0155 return i18n("Name"); 0156 case 1: 0157 return i18n("Type"); 0158 case 2: 0159 return i18n("Created"); 0160 case 3: 0161 return i18n("Comment"); 0162 default: 0163 return {}; 0164 } 0165 default: 0166 return {}; 0167 } 0168 } 0169 0170 QVariant AspectTreeModel::data(const QModelIndex& index, int role) const { 0171 if (!index.isValid()) 0172 return {}; 0173 0174 auto* aspect = static_cast<AbstractAspect*>(index.internalPointer()); 0175 switch (role) { 0176 case Qt::DisplayRole: 0177 case Qt::EditRole: 0178 switch (index.column()) { 0179 case 0: { 0180 const auto* column = dynamic_cast<const Column*>(aspect); 0181 if (column) { 0182 QString name = aspect->name(); 0183 if (m_plottableColumnsOnly && !column->isPlottable()) 0184 name = i18n("%1 (non-plottable data)", name); 0185 else if (m_numericColumnsOnly && !column->isNumeric()) 0186 name = i18n("%1 (non-numeric data)", name); 0187 else if (m_nonEmptyNumericColumnsOnly && !column->hasValues()) 0188 name = i18n("%1 (no values)", name); 0189 0190 if (m_showPlotDesignation) 0191 name += QLatin1Char('\t') + column->plotDesignationString(); 0192 0193 return name; 0194 } else if (aspect) 0195 return aspect->name(); 0196 else 0197 return {}; 0198 } 0199 case 1: 0200 if (QLatin1String(aspect->metaObject()->className()) == QLatin1String("CantorWorksheet")) 0201 return QLatin1String("Notebook"); 0202 else if (QLatin1String(aspect->metaObject()->className()) == QLatin1String("Datapicker")) 0203 return QLatin1String("DataExtractor"); 0204 else if (QLatin1String(aspect->metaObject()->className()) == QLatin1String("CartesianPlot")) 0205 return QLatin1String("Plot Area"); 0206 else 0207 return QLatin1String(aspect->metaObject()->className()); 0208 case 2: 0209 return QLocale::system().toString(aspect->creationTime(), QLocale::ShortFormat); 0210 case 3: 0211 return aspect->comment().replace(QLatin1Char('\n'), QLatin1Char(' ')).simplified(); 0212 default: 0213 return {}; 0214 } 0215 case Qt::ToolTipRole: { 0216 QString toolTip; 0217 if (aspect->comment().isEmpty()) 0218 toolTip = QLatin1String("<b>") + aspect->name() + QLatin1String("</b>"); 0219 else 0220 toolTip = 0221 QLatin1String("<b>") + aspect->name() + QLatin1String("</b><br><br>") + aspect->comment().replace(QLatin1Char('\n'), QLatin1String("<br>")); 0222 0223 const auto* col = dynamic_cast<const Column*>(aspect); 0224 if (col) { 0225 toolTip += QLatin1String("<br>"); 0226 toolTip += QLatin1String("<br>") + i18n("Size: %1", col->rowCount()); 0227 // TODO: active this once we have a more efficient implementation of this function 0228 // toolTip += QLatin1String("<br>") + i18n("Values: %1", col->availableRowCount()); 0229 toolTip += QLatin1String("<br>") + i18n("Type: %1", col->columnModeString()); 0230 toolTip += QLatin1String("<br>") + i18n("Plot Designation: %1", col->plotDesignationString()); 0231 0232 // in case it's a calculated column, add additional information 0233 // about the formula and parameters 0234 if (!col->formula().isEmpty()) { 0235 toolTip += QLatin1String("<br><br>") + i18n("Formula:"); 0236 QString f(QStringLiteral("f(")); 0237 QString parameters; 0238 for (int i = 0; i < col->formulaData().size(); ++i) { 0239 auto& data = col->formulaData().at(i); 0240 0241 // string for the function definition like f(x,y), etc. 0242 f += data.variableName(); 0243 if (i != col->formulaData().size() - 1) 0244 f += QStringLiteral(", "); 0245 0246 // string for the parameters and the references to the used columns for them 0247 if (!parameters.isEmpty()) 0248 parameters += QLatin1String("<br>"); 0249 parameters += data.variableName(); 0250 if (data.column()) 0251 parameters += QStringLiteral(" = ") + data.column()->path(); 0252 } 0253 0254 toolTip += QStringLiteral("<br>") + f + QStringLiteral(") = ") + col->formula(); 0255 toolTip += QStringLiteral("<br>") + parameters; 0256 if (col->formulaAutoUpdate()) 0257 toolTip += QStringLiteral("<br>") + i18n("auto update: true"); 0258 else 0259 toolTip += QStringLiteral("<br>") + i18n("auto update: false"); 0260 } 0261 } 0262 0263 return toolTip; 0264 } 0265 case Qt::DecorationRole: 0266 return index.column() == 0 ? aspect->icon() : QIcon(); 0267 case Qt::ForegroundRole: { 0268 const WorksheetElement* we = dynamic_cast<WorksheetElement*>(aspect); 0269 if (we) { 0270 if (!we->isVisible()) 0271 return QVariant(QApplication::palette().color(QPalette::Disabled, QPalette::Text)); 0272 } 0273 return QVariant(QApplication::palette().color(QPalette::Active, QPalette::Text)); 0274 } 0275 default: 0276 return {}; 0277 } 0278 } 0279 0280 Qt::ItemFlags AspectTreeModel::flags(const QModelIndex& index) const { 0281 if (!index.isValid()) 0282 return Qt::NoItemFlags; 0283 0284 Qt::ItemFlags result; 0285 auto* aspect = static_cast<AbstractAspect*>(index.internalPointer()); 0286 0287 if (!m_selectableAspects.isEmpty()) { 0288 for (AspectType type : m_selectableAspects) { 0289 if (aspect->inherits(type)) { 0290 result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; 0291 if (index != this->index(0, 0, QModelIndex()) && !m_filterString.isEmpty()) { 0292 if (this->containsFilterString(aspect)) 0293 result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; 0294 else 0295 result &= ~Qt::ItemIsEnabled; 0296 } 0297 break; 0298 } else 0299 result &= ~Qt::ItemIsEnabled; 0300 } 0301 } else { 0302 // default case: the list for the selectable aspects is empty and all aspects are selectable. 0303 // Apply filter, if available. Indices, that don't match the filter are not selectable. 0304 // Don't apply any filter to the very first index in the model - this top index corresponds to the project item. 0305 if (index != this->index(0, 0, QModelIndex()) && !m_filterString.isEmpty()) { 0306 if (this->containsFilterString(aspect)) 0307 result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; 0308 else 0309 result = Qt::ItemIsSelectable; 0310 } else 0311 result = Qt::ItemIsEnabled | Qt::ItemIsSelectable; 0312 } 0313 0314 // the columns "name" and "description" are editable 0315 if (!m_readOnly) { 0316 if (index.column() == 0 || index.column() == 3) 0317 result |= Qt::ItemIsEditable; 0318 } 0319 0320 const auto* column = dynamic_cast<const Column*>(aspect); 0321 if (column) { 0322 // allow to drag and drop columns for the faster creation of curves in the plots. 0323 // TODO: allow drag&drop later for other objects too, once we implement copy and paste in the project explorer 0324 result = result | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled; 0325 0326 if (m_plottableColumnsOnly && !column->isPlottable()) 0327 result &= ~Qt::ItemIsEnabled; 0328 0329 if (m_numericColumnsOnly && !column->isNumeric()) 0330 result &= ~Qt::ItemIsEnabled; 0331 0332 if (m_nonEmptyNumericColumnsOnly && !(column->isNumeric() && column->hasValues())) 0333 result &= ~Qt::ItemIsEnabled; 0334 } 0335 0336 return result; 0337 } 0338 0339 void AspectTreeModel::aspectDescriptionChanged(const AbstractAspect* aspect) { 0340 Q_EMIT dataChanged(modelIndexOfAspect(aspect), modelIndexOfAspect(aspect, 3)); 0341 } 0342 0343 void AspectTreeModel::aspectAboutToBeAdded(const AbstractAspect* parent, const AbstractAspect* before, const AbstractAspect* /*child*/) { 0344 int index = parent->indexOfChild<AbstractAspect>(before); 0345 if (index == -1) 0346 index = parent->childCount<AbstractAspect>(); 0347 0348 beginInsertRows(modelIndexOfAspect(parent), index, index); 0349 } 0350 0351 void AspectTreeModel::aspectAdded(const AbstractAspect* aspect) { 0352 endInsertRows(); 0353 AbstractAspect* parent = aspect->parentAspect(); 0354 Q_EMIT dataChanged(modelIndexOfAspect(parent), modelIndexOfAspect(parent, 3)); 0355 0356 connect(aspect, &AbstractAspect::renameRequested, this, &AspectTreeModel::renameRequestedSlot); 0357 connect(aspect, &AbstractAspect::childAspectSelectedInView, this, &AspectTreeModel::aspectSelectedInView); 0358 connect(aspect, &AbstractAspect::childAspectDeselectedInView, this, &AspectTreeModel::aspectDeselectedInView); 0359 0360 // add signal-slot connects for all children, too 0361 const auto& children = aspect->children<AbstractAspect>(AbstractAspect::ChildIndexFlag::Recursive); 0362 for (const auto* child : children) { 0363 connect(child, &AbstractAspect::renameRequested, this, &AspectTreeModel::renameRequestedSlot); 0364 connect(child, &AbstractAspect::childAspectSelectedInView, this, &AspectTreeModel::aspectSelectedInView); 0365 connect(child, &AbstractAspect::childAspectDeselectedInView, this, &AspectTreeModel::aspectDeselectedInView); 0366 } 0367 } 0368 0369 void AspectTreeModel::aspectAboutToBeRemoved(const AbstractAspect* aspect) { 0370 AbstractAspect* parent = aspect->parentAspect(); 0371 int index = parent->indexOfChild<AbstractAspect>(aspect); 0372 m_aspectAboutToBeRemovedCalled = true; 0373 beginRemoveRows(modelIndexOfAspect(parent), index, index); 0374 } 0375 0376 void AspectTreeModel::aspectRemoved() { 0377 // make sure aspectToBeRemoved(), and with this beginRemoveRows() in the model, was called 0378 // prior to calling endRemoveRows() further below. 0379 // see https://invent.kde.org/education/labplot/-/merge_requests/278 for more information. 0380 if (!m_aspectAboutToBeRemovedCalled) 0381 return; 0382 0383 m_aspectAboutToBeRemovedCalled = false; 0384 endRemoveRows(); 0385 } 0386 0387 void AspectTreeModel::aspectHiddenAboutToChange(const AbstractAspect* aspect) { 0388 for (AbstractAspect* i = aspect->parentAspect(); i; i = i->parentAspect()) 0389 if (i->hidden()) 0390 return; 0391 if (aspect->hidden()) 0392 aspectAboutToBeAdded(aspect->parentAspect(), aspect, aspect); 0393 else 0394 aspectAboutToBeRemoved(aspect); 0395 } 0396 0397 void AspectTreeModel::aspectHiddenChanged(const AbstractAspect* aspect) { 0398 for (AbstractAspect* i = aspect->parentAspect(); i; i = i->parentAspect()) 0399 if (i->hidden()) 0400 return; 0401 if (aspect->hidden()) 0402 aspectRemoved(); 0403 else 0404 aspectAdded(aspect); 0405 } 0406 0407 bool AspectTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) { 0408 if (!index.isValid() || role != Qt::EditRole) 0409 return false; 0410 auto* aspect = static_cast<AbstractAspect*>(index.internalPointer()); 0411 switch (index.column()) { 0412 case 0: { 0413 if (!aspect->setName(value.toString(), AbstractAspect::NameHandling::UniqueRequired)) { 0414 Q_EMIT statusInfo(i18n("The name \"%1\" is already in use. Choose another name.", value.toString())); 0415 return false; 0416 } 0417 break; 0418 } 0419 case 3: 0420 aspect->setComment(value.toString()); 0421 break; 0422 default: 0423 return false; 0424 } 0425 Q_EMIT dataChanged(index, index); 0426 return true; 0427 } 0428 0429 QModelIndex AspectTreeModel::modelIndexOfAspect(const AbstractAspect* aspect, int column) const { 0430 if (!aspect) 0431 return QModelIndex(); 0432 AbstractAspect* parent = aspect->parentAspect(); 0433 return createIndex(parent ? parent->indexOfChild<AbstractAspect>(aspect) : 0, column, const_cast<AbstractAspect*>(aspect)); 0434 } 0435 0436 /*! 0437 returns the model index of an aspect defined via its path. 0438 */ 0439 QModelIndex AspectTreeModel::modelIndexOfAspect(const QString& path, int column) const { 0440 // determine the aspect out of aspect path 0441 AbstractAspect* aspect = nullptr; 0442 if (m_root->path() != path) { 0443 const auto& children = m_root->children<AbstractAspect>(AbstractAspect::ChildIndexFlag::Recursive); 0444 for (auto* child : children) { 0445 if (child->path() == path) { 0446 aspect = child; 0447 break; 0448 } 0449 } 0450 } else 0451 aspect = m_root; 0452 0453 // return the model index of the aspect 0454 if (aspect) 0455 return modelIndexOfAspect(aspect, column); 0456 0457 return QModelIndex{}; 0458 } 0459 0460 void AspectTreeModel::setFilterString(const QString& s) { 0461 m_filterString = s; 0462 QModelIndex topLeft = this->index(0, 0, QModelIndex()); 0463 QModelIndex bottomRight = this->index(this->rowCount() - 1, 3, QModelIndex()); 0464 Q_EMIT dataChanged(topLeft, bottomRight); 0465 } 0466 0467 void AspectTreeModel::setFilterCaseSensitivity(Qt::CaseSensitivity cs) { 0468 m_filterCaseSensitivity = cs; 0469 } 0470 0471 void AspectTreeModel::setFilterMatchCompleteWord(bool b) { 0472 m_matchCompleteWord = b; 0473 } 0474 0475 bool AspectTreeModel::containsFilterString(const AbstractAspect* aspect) const { 0476 if (m_matchCompleteWord) { 0477 if (aspect->name().compare(m_filterString, m_filterCaseSensitivity) == 0) 0478 return true; 0479 } else { 0480 if (aspect->name().contains(m_filterString, m_filterCaseSensitivity)) 0481 return true; 0482 } 0483 0484 // check for the occurrence of the filter string in the names of the parents 0485 if (aspect->parentAspect()) 0486 return this->containsFilterString(aspect->parentAspect()); 0487 else 0488 return false; 0489 0490 // TODO make this optional 0491 // //check for the occurrence of the filter string in the names of the children 0492 // foreach(const AbstractAspect * child, aspect->children<AbstractAspect>()) { 0493 // if ( this->containsFilterString(child) ) 0494 // return true; 0495 // } 0496 } 0497 0498 // ############################################################################## 0499 // ################################# SLOTS #################################### 0500 // ############################################################################## 0501 void AspectTreeModel::renameRequestedSlot() { 0502 auto* aspect = dynamic_cast<AbstractAspect*>(QObject::sender()); 0503 if (aspect) 0504 Q_EMIT renameRequested(modelIndexOfAspect(aspect)); 0505 } 0506 0507 void AspectTreeModel::aspectSelectedInView(const AbstractAspect* aspect) { 0508 if (aspect->hidden()) { 0509 // a hidden aspect was selected in the view (e.g. plot title in WorksheetView) 0510 // select the parent aspect first, if available 0511 AbstractAspect* parent = aspect->parentAspect(); 0512 if (parent) 0513 Q_EMIT indexSelected(modelIndexOfAspect(parent)); 0514 0515 // Q_EMIT also this signal, so the GUI can handle this selection. 0516 Q_EMIT hiddenAspectSelected(aspect); 0517 } else 0518 Q_EMIT indexSelected(modelIndexOfAspect(aspect)); 0519 0520 // deselect the root item when one of the children was selected in the view 0521 // in order to avoid multiple selection with the project item (if selected) in the project explorer 0522 Q_EMIT indexDeselected(modelIndexOfAspect(m_root)); 0523 } 0524 0525 void AspectTreeModel::aspectDeselectedInView(const AbstractAspect* aspect) { 0526 if (aspect->hidden()) { 0527 AbstractAspect* parent = aspect->parentAspect(); 0528 if (parent) 0529 Q_EMIT indexDeselected(modelIndexOfAspect(parent)); 0530 } else 0531 Q_EMIT indexDeselected(modelIndexOfAspect(aspect)); 0532 }