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 }