File indexing completed on 2024-04-28 03:47:19

0001 /*
0002     File                 : ProjectExplorer.cpp
0003     Project              : LabPlot
0004     Description          : A tree view for displaying and editing an AspectTreeModel.
0005     --------------------------------------------------------------------
0006     SPDX-FileCopyrightText: 2007-2008 Tilman Benkert <thzs@gmx.net>
0007     SPDX-FileCopyrightText: 2010-2021 Alexander Semke <alexander.semke@web.de>
0008 
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include "ProjectExplorer.h"
0013 #include "backend/core/AbstractPart.h"
0014 #include "backend/core/AspectTreeModel.h"
0015 #include "backend/core/Project.h"
0016 #include "backend/core/Settings.h"
0017 #include "backend/core/column/Column.h"
0018 #include "backend/lib/XmlStreamReader.h"
0019 #include "backend/worksheet/plots/cartesian/CartesianPlot.h"
0020 #include "commonfrontend/core/ContentDockWidget.h"
0021 
0022 #include <KConfig>
0023 #include <KConfigGroup>
0024 #include <KLocalizedString>
0025 #include <KMessageBox>
0026 #include <KMessageWidget>
0027 
0028 #include <kcoreaddons_version.h>
0029 #if KCOREADDONS_VERSION >= QT_VERSION_CHECK(5, 79, 0)
0030 #define HAS_FUZZY_MATCHER true
0031 #include <KFuzzyMatcher>
0032 #else
0033 #define HAS_FUZZY_MATCHER false
0034 #endif
0035 
0036 #include <QClipboard>
0037 #include <QContextMenuEvent>
0038 #include <QDrag>
0039 #include <QHeaderView>
0040 #include <QLabel>
0041 #include <QLineEdit>
0042 #include <QMenu>
0043 #include <QMimeData>
0044 #include <QPushButton>
0045 #include <QTreeView>
0046 #include <QVBoxLayout>
0047 
0048 /*!
0049   \class ProjectExplorer
0050   \brief A tree view for displaying and editing an AspectTreeModel.
0051 
0052   In addition to the functionality of QTreeView, ProjectExplorer allows
0053   the usage of the context menus provided by AspectTreeModel
0054   and propagates the item selection in the view to the model.
0055   Furthermore, features for searching and filtering in the model are provided.
0056 
0057   \ingroup commonfrontend
0058 */
0059 
0060 ProjectExplorer::ProjectExplorer(QWidget* parent)
0061     : m_treeView(new QTreeView(parent))
0062     , m_frameFilter(new QFrame(this)) {
0063     auto* layout = new QVBoxLayout(this);
0064     layout->setSpacing(0);
0065     layout->setContentsMargins(0, 0, 0, 0);
0066 
0067     auto* layoutFilter = new QHBoxLayout(m_frameFilter);
0068     layoutFilter->setSpacing(0);
0069     layoutFilter->setContentsMargins(0, 0, 0, 0);
0070 
0071     m_leFilter = new QLineEdit(m_frameFilter);
0072     m_leFilter->setClearButtonEnabled(true);
0073     m_leFilter->setPlaceholderText(i18n("Search/Filter"));
0074     layoutFilter->addWidget(m_leFilter);
0075 
0076     bFilterOptions = new QPushButton(m_frameFilter);
0077     bFilterOptions->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0078     bFilterOptions->setCheckable(true);
0079     bFilterOptions->setFlat(true);
0080     layoutFilter->addWidget(bFilterOptions);
0081 
0082     layout->addWidget(m_frameFilter);
0083 
0084     m_treeView->setAnimated(true);
0085     m_treeView->setAlternatingRowColors(true);
0086     m_treeView->setSelectionBehavior(QAbstractItemView::SelectRows);
0087     m_treeView->setSelectionMode(QAbstractItemView::ExtendedSelection);
0088     m_treeView->setUniformRowHeights(true);
0089     m_treeView->viewport()->installEventFilter(this);
0090     m_treeView->header()->setStretchLastSection(true);
0091     m_treeView->header()->installEventFilter(this);
0092     m_treeView->setDragEnabled(true);
0093     m_treeView->setAcceptDrops(true);
0094     m_treeView->setDropIndicatorShown(true);
0095     m_treeView->setDragDropMode(QAbstractItemView::InternalMove);
0096 
0097     layout->addWidget(m_treeView);
0098 
0099     connect(m_leFilter, &QLineEdit::textChanged, this, &ProjectExplorer::filterTextChanged);
0100     connect(bFilterOptions, &QPushButton::toggled, this, &ProjectExplorer::toggleFilterOptionsMenu);
0101 }
0102 
0103 ProjectExplorer::~ProjectExplorer() {
0104     // save the visible columns
0105     QString status;
0106     for (int i = 0; i < list_showColumnActions.size(); ++i) {
0107         if (list_showColumnActions.at(i)->isChecked()) {
0108             if (!status.isEmpty())
0109                 status += QLatin1Char(' ');
0110             status += QString::number(i);
0111         }
0112     }
0113     KConfigGroup group = Settings::group(QStringLiteral("ProjectExplorer"));
0114     group.writeEntry("VisibleColumns", status);
0115 }
0116 
0117 void ProjectExplorer::createActions() {
0118     expandTreeAction = new QAction(QIcon::fromTheme(QLatin1String("expand-all")), i18n("Expand All"), this);
0119     connect(expandTreeAction, &QAction::triggered, m_treeView, &QTreeView::expandAll);
0120 
0121     expandSelectedTreeAction = new QAction(QIcon::fromTheme(QLatin1String("expand-all")), i18n("Expand Selected"), this);
0122     connect(expandSelectedTreeAction, &QAction::triggered, this, &ProjectExplorer::expandSelected);
0123 
0124     collapseTreeAction = new QAction(QIcon::fromTheme(QLatin1String("collapse-all")), i18n("Collapse All"), this);
0125     connect(collapseTreeAction, &QAction::triggered, m_treeView, &QTreeView::collapseAll);
0126 
0127     collapseSelectedTreeAction = new QAction(QIcon::fromTheme(QLatin1String("collapse-all")), i18n("Collapse Selected"), this);
0128     connect(collapseSelectedTreeAction, &QAction::triggered, this, &ProjectExplorer::collapseSelected);
0129 
0130     deleteSelectedTreeAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete Selected"), this);
0131     connect(deleteSelectedTreeAction, &QAction::triggered, this, &ProjectExplorer::deleteSelected);
0132 
0133     toggleFilterAction = new QAction(QIcon::fromTheme(QLatin1String("view-filter")), i18n("Search/Filter Options"), this);
0134     toggleFilterAction->setCheckable(true);
0135     toggleFilterAction->setChecked(true);
0136     connect(toggleFilterAction, &QAction::triggered, this, [=]() {
0137         m_frameFilter->setVisible(!m_frameFilter->isVisible());
0138     });
0139 }
0140 
0141 /*!
0142   shows the context menu in the tree. In addition to the context menu of the currently selected aspect,
0143   treeview specific options are added.
0144 */
0145 void ProjectExplorer::contextMenuEvent(QContextMenuEvent* event) {
0146     if (!m_treeView->model())
0147         return;
0148 
0149     if (!expandTreeAction)
0150         createActions();
0151 
0152     const auto& index = m_treeView->indexAt(m_treeView->viewport()->mapFrom(this, event->pos()));
0153     if (!index.isValid())
0154         m_treeView->clearSelection();
0155 
0156     const auto& items = m_treeView->selectionModel()->selectedIndexes();
0157     QMenu* menu = nullptr;
0158     if (items.size() / 4 == 1) {
0159         auto* aspect = static_cast<AbstractAspect*>(index.internalPointer());
0160         menu = aspect->createContextMenu();
0161 
0162         if (aspect == m_project) {
0163             auto* firstAction = menu->actions().at(2);
0164             menu->insertSeparator(firstAction);
0165             menu->insertAction(firstAction, expandTreeAction);
0166             menu->insertAction(firstAction, collapseTreeAction);
0167         }
0168     } else {
0169         menu = new QMenu(this);
0170 
0171         if (items.size() / 4 > 1) {
0172             // add "expand/collapse" entries if the selected indices have children
0173             bool hasChildren = false;
0174             for (int i = 0; i < items.size() / 4; ++i) {
0175                 const auto& index = items.at(i * 4);
0176                 const auto* aspect = static_cast<AbstractAspect*>(index.internalPointer());
0177                 if (aspect->childCount<AbstractAspect>()) {
0178                     hasChildren = true;
0179                     break;
0180                 }
0181             }
0182             if (hasChildren) {
0183                 menu->addAction(expandSelectedTreeAction);
0184                 menu->addAction(collapseSelectedTreeAction);
0185                 menu->addSeparator();
0186             }
0187 
0188             menu->addAction(deleteSelectedTreeAction);
0189         } else {
0190             QMenu* projectMenu = m_project->createContextMenu();
0191             projectMenu->setTitle(m_project->name());
0192             menu->addMenu(projectMenu);
0193 
0194             menu->addSeparator();
0195             menu->addAction(expandTreeAction);
0196             menu->addAction(collapseTreeAction);
0197             menu->addSeparator();
0198             menu->addAction(toggleFilterAction);
0199 
0200             // Menu for showing/hiding the columns in the tree view
0201             QMenu* columnsMenu = menu->addMenu(i18n("Columns"));
0202             columnsMenu->addAction(showAllColumnsAction);
0203             columnsMenu->addSeparator();
0204             for (auto* action : qAsConst(list_showColumnActions))
0205                 columnsMenu->addAction(action);
0206 
0207             // TODO
0208             // Menu for showing/hiding the top-level aspects (Worksheet, Spreadhsheet, etc) in the tree view
0209             //  QMenu* objectsMenu = menu->addMenu(i18n("Show/Hide objects"));
0210         }
0211     }
0212 
0213     if (menu)
0214         menu->exec(event->globalPos());
0215 
0216     delete menu;
0217 }
0218 
0219 void ProjectExplorer::setCurrentAspect(const AbstractAspect* aspect) {
0220     // HACK: when doing redo/undo in MainWin and an object is being deleted,
0221     // we don't want to jump to another object in the project explorer.
0222     // we reuse the aspectAddedSignalSuppressed also for the deletion of aspects.
0223     if (m_project->aspectAddedSignalSuppressed())
0224         return;
0225 
0226     const auto* tree_model = dynamic_cast<AspectTreeModel*>(m_treeView->model());
0227     if (tree_model) {
0228         const auto& index = tree_model->modelIndexOfAspect(aspect);
0229 // TODO: This crashes on Windows in Debug mode
0230 #if !defined(HAVE_WINDOWS) || defined(NDEBUG)
0231         m_treeView->setCurrentIndex(index);
0232 #endif
0233     }
0234 }
0235 
0236 /*!
0237   Sets the \c model for the tree view to present.
0238 */
0239 void ProjectExplorer::setModel(AspectTreeModel* treeModel) {
0240     m_treeView->setModel(treeModel);
0241 
0242     connect(treeModel, &AspectTreeModel::renameRequested, m_treeView, static_cast<void (QAbstractItemView::*)(const QModelIndex&)>(&QAbstractItemView::edit));
0243     connect(treeModel, &AspectTreeModel::indexSelected, this, &ProjectExplorer::selectIndex);
0244     connect(treeModel, &AspectTreeModel::indexDeselected, this, &ProjectExplorer::deselectIndex);
0245     connect(treeModel, &AspectTreeModel::hiddenAspectSelected, this, &ProjectExplorer::hiddenAspectSelected);
0246 
0247     connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ProjectExplorer::selectionChanged);
0248 
0249     // create action for showing/hiding the columns in the tree.
0250     // this is done here since the number of columns is  not available in createActions() yet.
0251     if (list_showColumnActions.isEmpty()) {
0252         // read the status of the column actions if available
0253         KConfigGroup group = Settings::group(QStringLiteral("ProjectExplorer"));
0254         const QString& status = group.readEntry(QLatin1String("VisibleColumns"), QString());
0255         QVector<int> checkedActions;
0256         if (!status.isEmpty()) {
0257             QStringList strList = status.split(QLatin1Char(' '));
0258             for (int i = 0; i < strList.size(); ++i)
0259                 checkedActions << strList.at(i).toInt();
0260         }
0261 
0262         if (!showAllColumnsAction) {
0263             showAllColumnsAction = new QAction(i18n("Show All"), this);
0264             showAllColumnsAction->setCheckable(true);
0265             showAllColumnsAction->setChecked(true);
0266             showAllColumnsAction->setEnabled(false);
0267             connect(showAllColumnsAction, &QAction::triggered, this, &ProjectExplorer::showAllColumns);
0268         }
0269 
0270         if (checkedActions.size() != m_treeView->model()->columnCount()) {
0271             showAllColumnsAction->setEnabled(true);
0272             showAllColumnsAction->setChecked(false);
0273         } else {
0274             showAllColumnsAction->setEnabled(false);
0275             showAllColumnsAction->setChecked(true);
0276         }
0277 
0278         // create an action for every available column in the model
0279         for (int i = 0; i < m_treeView->model()->columnCount(); i++) {
0280             QAction* showColumnAction = new QAction(treeModel->headerData(i, Qt::Horizontal).toString(), this);
0281             showColumnAction->setCheckable(true);
0282 
0283             // restore the status, if available
0284             if (!checkedActions.isEmpty()) {
0285                 if (checkedActions.indexOf(i) != -1)
0286                     showColumnAction->setChecked(true);
0287                 else
0288                     m_treeView->hideColumn(i);
0289             } else
0290                 showColumnAction->setChecked(true);
0291 
0292             list_showColumnActions.append(showColumnAction);
0293 
0294             connect(showColumnAction, &QAction::triggered, this, [=] {
0295                 ProjectExplorer::toggleColumn(i);
0296             });
0297         }
0298     } else {
0299         for (int i = 0; i < list_showColumnActions.size(); ++i) {
0300             if (!list_showColumnActions.at(i)->isChecked())
0301                 m_treeView->hideColumn(i);
0302         }
0303     }
0304 }
0305 
0306 void ProjectExplorer::setProject(Project* project) {
0307     connect(project, &Project::childAspectAdded, this, &ProjectExplorer::aspectAdded);
0308     connect(project, &Project::requestSaveState, this, &ProjectExplorer::save);
0309     connect(project, &Project::requestLoadState, this, &ProjectExplorer::load);
0310     connect(project, &Project::requestNavigateTo, this, &ProjectExplorer::navigateTo);
0311     m_project = project;
0312 
0313     // for newly created projects, resize the header to fit the size of the header section names.
0314     // for projects loaded from a file, this function will be called laterto fit the sizes
0315     // of the content once the project is loaded
0316     resizeHeader();
0317 
0318     // remove all error messages from the previous project, if any are visible
0319     if (m_messageWidget && m_messageWidget->isVisible())
0320         m_messageWidget->close();
0321 }
0322 
0323 QModelIndex ProjectExplorer::currentIndex() const {
0324     return m_treeView->currentIndex();
0325 }
0326 
0327 AbstractAspect* ProjectExplorer::currentAspect() const {
0328     if (!currentIndex().isValid())
0329         return nullptr;
0330     return static_cast<AbstractAspect*>(currentIndex().internalPointer());
0331 }
0332 
0333 void ProjectExplorer::search() {
0334     m_leFilter->setFocus();
0335 }
0336 
0337 /*!
0338     handles the contextmenu-event of the horizontal header in the tree view.
0339     Provides a menu for selective showing and hiding of columns.
0340 */
0341 bool ProjectExplorer::eventFilter(QObject* obj, QEvent* event) {
0342     if (obj == m_treeView->header() && event->type() == QEvent::ContextMenu) {
0343         // Menu for showing/hiding the columns in the tree view
0344         QMenu* columnsMenu = new QMenu(m_treeView->header());
0345         columnsMenu->addSection(i18n("Columns"));
0346         columnsMenu->addAction(showAllColumnsAction);
0347         columnsMenu->addSeparator();
0348         for (auto* action : qAsConst(list_showColumnActions))
0349             columnsMenu->addAction(action);
0350 
0351         auto* e = static_cast<QContextMenuEvent*>(event);
0352         columnsMenu->exec(e->globalPos());
0353         delete columnsMenu;
0354 
0355         return true;
0356     } else if (obj == m_treeView->viewport()) {
0357         if (event->type() == QEvent::MouseButtonPress) {
0358             auto* e = static_cast<QMouseEvent*>(event);
0359 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0360             const auto position = e->globalPosition().toPoint();
0361 #else
0362             const auto position = e->globalPos();
0363 #endif
0364             if (e->button() == Qt::LeftButton) {
0365                 QModelIndex index = m_treeView->indexAt(e->pos());
0366                 if (!index.isValid())
0367                     return false;
0368 
0369                 auto* aspect = static_cast<AbstractAspect*>(index.internalPointer());
0370                 if (aspect->isDraggable()) {
0371                     m_dragStartPos = position;
0372                     m_dragStarted = false;
0373                 }
0374             }
0375         } else if (event->type() == QEvent::MouseMove) {
0376             auto* e = static_cast<QMouseEvent*>(event);
0377 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0378             const auto position = e->globalPosition().toPoint();
0379 #else
0380             const auto position = e->globalPos();
0381 #endif
0382             if (!m_dragStarted && m_treeView->selectionModel()->selectedIndexes().size() > 0
0383                 && (position - m_dragStartPos).manhattanLength() >= QApplication::startDragDistance()) {
0384                 m_dragStarted = true;
0385                 auto* drag = new QDrag(this);
0386                 auto* mimeData = new QMimeData;
0387 
0388                 // determine the selected objects and serialize the pointers to QMimeData
0389                 QVector<quintptr> vec;
0390                 QModelIndexList items = m_treeView->selectionModel()->selectedIndexes();
0391                 // there are four model indices in each row -> divide by 4 to obtain the number of selected rows (=aspects)
0392                 const int columnCount = m_treeView->model()->columnCount();
0393                 for (int i = 0; i < items.size() / columnCount; ++i) {
0394                     const QModelIndex& index = items.at(i * columnCount);
0395                     auto* aspect = static_cast<AbstractAspect*>(index.internalPointer());
0396                     vec << (quintptr)aspect;
0397                 }
0398 
0399                 QByteArray data;
0400                 QDataStream stream(&data, QIODevice::WriteOnly);
0401                 stream << (quintptr)m_project; // serialize the project pointer first, will be used as the unique identifier
0402                 stream << vec;
0403 
0404                 mimeData->setData(QStringLiteral("labplot-dnd"), data);
0405                 drag->setMimeData(mimeData);
0406                 drag->exec();
0407             }
0408         } else if (event->type() == QEvent::DragEnter) {
0409             // ignore events not related to internal drags of columns etc., e.g. dropping of external files onto LabPlot
0410             auto* dragEnterEvent = static_cast<QDragEnterEvent*>(event);
0411             const QMimeData* mimeData = dragEnterEvent->mimeData();
0412             if (!mimeData) {
0413                 event->ignore();
0414                 return false;
0415             }
0416 
0417             if (mimeData->formats().at(0) != QLatin1String("labplot-dnd")) {
0418                 event->ignore();
0419                 return false;
0420             } else {
0421                 // drad&drop between the different project windows is not supported yet.
0422                 // check whether we're dragging inside of the same project.
0423                 QByteArray data = mimeData->data(QLatin1String("labplot-dnd"));
0424                 QDataStream stream(&data, QIODevice::ReadOnly);
0425                 quintptr ptr = 0;
0426                 stream >> ptr;
0427                 auto* project = reinterpret_cast<Project*>(ptr);
0428                 if (project != m_project) {
0429                     event->ignore();
0430                     return false;
0431                 }
0432             }
0433 
0434             event->setAccepted(true);
0435         } else if (event->type() == QEvent::DragMove) {
0436             auto* dragMoveEvent = static_cast<QDragEnterEvent*>(event);
0437             const QMimeData* mimeData = dragMoveEvent->mimeData();
0438             QVector<quintptr> vec = m_project->droppedAspects(mimeData);
0439 
0440             AbstractAspect* sourceAspect{nullptr};
0441             if (!vec.isEmpty())
0442                 sourceAspect = reinterpret_cast<AbstractAspect*>(vec.at(0));
0443 
0444             if (!sourceAspect)
0445                 return false;
0446 
0447                 // determine the aspect under the cursor
0448 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0449             QModelIndex index = m_treeView->indexAt(dragMoveEvent->position().toPoint());
0450 #else
0451             QModelIndex index = m_treeView->indexAt(dragMoveEvent->pos());
0452 #endif
0453             if (!index.isValid())
0454                 return false;
0455 
0456             // accept only the events when the aspect being dragged is dropable onto the aspect under the cursor
0457             // and the aspect under the cursor is not already the parent of the dragged aspect
0458             auto* destinationAspect = static_cast<AbstractAspect*>(index.internalPointer());
0459             bool accept = sourceAspect->dropableOn().indexOf(destinationAspect->type()) != -1 && sourceAspect->parentAspect() != destinationAspect;
0460             event->setAccepted(accept);
0461         } else if (event->type() == QEvent::Drop) {
0462             auto* dropEvent = static_cast<QDropEvent*>(event);
0463             const QMimeData* mimeData = dropEvent->mimeData();
0464             if (!mimeData)
0465                 return false;
0466 
0467             QVector<quintptr> vec = m_project->droppedAspects(mimeData);
0468             if (vec.isEmpty())
0469                 return false;
0470 
0471 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
0472             QModelIndex index = m_treeView->indexAt(dropEvent->position().toPoint());
0473 #else
0474             QModelIndex index = m_treeView->indexAt(dropEvent->pos());
0475 #endif
0476             if (!index.isValid())
0477                 return false;
0478 
0479             // process the dropped objects
0480             auto* aspect = static_cast<AbstractAspect*>(index.internalPointer());
0481             aspect->processDropEvent(vec);
0482 
0483             // expand the current aspect to see the dropped objects
0484             const auto* model = static_cast<const AspectTreeModel*>(m_treeView->model());
0485             m_treeView->setExpanded(model->modelIndexOfAspect(aspect), true);
0486         }
0487     }
0488 
0489     return QObject::eventFilter(obj, event);
0490 }
0491 
0492 void ProjectExplorer::keyPressEvent(QKeyEvent* event) {
0493     // current selected aspect
0494     auto* aspect = static_cast<AbstractAspect*>(m_treeView->currentIndex().internalPointer());
0495 
0496     if (event->matches(QKeySequence::Delete))
0497         deleteSelected();
0498     else if (event->matches(QKeySequence::Copy)) {
0499         // copy
0500         if (aspect != m_project) {
0501             aspect->copy();
0502             showErrorMessage(QString());
0503         }
0504     } else if (event->matches(QKeySequence::Paste)) {
0505         // paste
0506         QString name;
0507         auto t = AbstractAspect::clipboardAspectType(name);
0508         if (!name.isEmpty()) {
0509             if (t != AspectType::AbstractAspect && aspect->pasteTypes().indexOf(t) != -1) {
0510                 aspect->paste();
0511                 showErrorMessage(QString());
0512             } else {
0513                 QString msg = i18n("'%1' cannot be pasted into '%2'.", name, aspect->name());
0514                 showErrorMessage(msg);
0515             }
0516         } else {
0517             // no name is available, we are copy&pasting the content of a columm ("the data") and not the column itself
0518             const auto* mimeData = QApplication::clipboard()->mimeData();
0519             if (!mimeData->hasFormat(QStringLiteral("text/plain")))
0520                 return;
0521 
0522             // pasting is allowed into spreadsheet columns only
0523             if (aspect->type() == AspectType::Column && aspect->parentAspect()->type() == AspectType::Spreadsheet) {
0524                 auto* column = static_cast<Column*>(aspect);
0525                 column->pasteData();
0526             } else {
0527                 QString msg = i18n("Data cannot be pasted into '%1' directly. Select a spreadsheet column for this.", aspect->name());
0528                 showErrorMessage(msg);
0529             }
0530         }
0531     } else if ((event->modifiers() & Qt::ControlModifier) && (event->key() == Qt::Key_D)) {
0532         // duplicate
0533         if (aspect != m_project) {
0534             aspect->copy();
0535             aspect->parentAspect()->paste(true);
0536             showErrorMessage(QString());
0537         }
0538     } else if (event->key() == 32) {
0539         // space key - hide/show the current object
0540         changeSelectedVisible();
0541     }
0542 }
0543 
0544 void ProjectExplorer::showErrorMessage(const QString& message) {
0545     if (message.isEmpty()) {
0546         if (m_messageWidget && m_messageWidget->isVisible())
0547             m_messageWidget->close();
0548     } else {
0549         if (!m_messageWidget) {
0550             m_messageWidget = new KMessageWidget(this);
0551             m_messageWidget->setMessageType(KMessageWidget::Error);
0552             layout()->addWidget(m_messageWidget);
0553         }
0554         m_messageWidget->setText(message);
0555         m_messageWidget->animatedShow();
0556     }
0557 }
0558 
0559 // ##############################################################################
0560 // #################################  SLOTS  ####################################
0561 // ##############################################################################
0562 /*!
0563   expand the aspect \c aspect (the tree index corresponding to it) in the tree view
0564   and makes it visible and selected. Called when a new aspect is added to the project.
0565  */
0566 void ProjectExplorer::aspectAdded(const AbstractAspect* aspect) {
0567     if (m_project->isLoading() || m_project->aspectAddedSignalSuppressed())
0568         return;
0569 
0570     // don't do anything if hidden aspects were added
0571     if (aspect->hidden())
0572         return;
0573 
0574     // don't do anything for newly added data spreadsheets of data picker curves
0575     if (aspect->type() == AspectType::Spreadsheet && aspect->parentAspect()->type() == AspectType::DatapickerCurve)
0576         return;
0577 
0578     const auto* tree_model = qobject_cast<AspectTreeModel*>(m_treeView->model());
0579     const auto& index = tree_model->modelIndexOfAspect(aspect);
0580 
0581     // expand and make the aspect visible
0582     // no need to expand and to show child columns of a statistics spreadsheet
0583     if (aspect->type() != AspectType::StatisticsSpreadsheet)
0584         m_treeView->setExpanded(index, true);
0585 
0586     // newly added columns are only expanded but not selected, return here
0587     if (aspect->type() == AspectType::Column) {
0588         m_treeView->setExpanded(tree_model->modelIndexOfAspect(aspect->parentAspect()), true);
0589         return;
0590     }
0591 
0592     m_treeView->scrollTo(index);
0593     m_treeView->setCurrentIndex(index);
0594     m_treeView->header()->resizeSections(QHeaderView::ResizeToContents);
0595     m_treeView->header()->resizeSection(0, m_treeView->header()->sectionSize(0) * 1.2);
0596 }
0597 
0598 void ProjectExplorer::navigateTo(const QString& path) {
0599     const auto* tree_model = dynamic_cast<AspectTreeModel*>(m_treeView->model());
0600     if (tree_model) {
0601         const auto& index = tree_model->modelIndexOfAspect(path);
0602         if (!index.isValid())
0603             return;
0604         m_treeView->scrollTo(index);
0605         m_treeView->setCurrentIndex(index);
0606         auto* aspect = static_cast<AbstractAspect*>(index.internalPointer());
0607         aspect->setSelected(true);
0608     }
0609 }
0610 
0611 void ProjectExplorer::toggleColumn(int index) {
0612     // determine the total number of checked column actions
0613     int checked = 0;
0614     for (const auto* action : qAsConst(list_showColumnActions)) {
0615         if (action->isChecked())
0616             checked++;
0617     }
0618 
0619     if (list_showColumnActions.at(index)->isChecked()) {
0620         m_treeView->showColumn(index);
0621         m_treeView->header()->resizeSection(0, 0);
0622         m_treeView->header()->resizeSections(QHeaderView::ResizeToContents);
0623 
0624         for (auto* action : qAsConst(list_showColumnActions))
0625             action->setEnabled(true);
0626 
0627         // deactivate the "show all column"-action, if all actions are checked
0628         if (checked == list_showColumnActions.size()) {
0629             showAllColumnsAction->setEnabled(false);
0630             showAllColumnsAction->setChecked(true);
0631         }
0632     } else {
0633         m_treeView->hideColumn(index);
0634         showAllColumnsAction->setEnabled(true);
0635         showAllColumnsAction->setChecked(false);
0636 
0637         // if there is only one checked column-action, deactivated it.
0638         // It should't be possible to hide all columns
0639         if (checked == 1) {
0640             int i = 0;
0641             while (!list_showColumnActions.at(i)->isChecked())
0642                 i++;
0643 
0644             list_showColumnActions.at(i)->setEnabled(false);
0645         }
0646     }
0647 }
0648 
0649 void ProjectExplorer::showAllColumns() {
0650     for (int i = 0; i < m_treeView->model()->columnCount(); i++) {
0651         m_treeView->showColumn(i);
0652         m_treeView->header()->resizeSection(0, 0);
0653         m_treeView->header()->resizeSections(QHeaderView::ResizeToContents);
0654     }
0655     showAllColumnsAction->setEnabled(false);
0656 
0657     for (auto* action : qAsConst(list_showColumnActions)) {
0658         action->setEnabled(true);
0659         action->setChecked(true);
0660     }
0661 }
0662 
0663 /*!
0664   toggles the menu for the filter/search options
0665 */
0666 void ProjectExplorer::toggleFilterOptionsMenu(bool checked) {
0667     if (!checked)
0668         return;
0669 
0670     if (!caseSensitiveAction) {
0671         caseSensitiveAction = new QAction(i18n("Case Sensitive"), this);
0672         caseSensitiveAction->setCheckable(true);
0673         caseSensitiveAction->setChecked(false);
0674         connect(caseSensitiveAction, &QAction::triggered, this, [=]() {
0675             if (!m_leFilter->text().isEmpty())
0676                 filterTextChanged(m_leFilter->text());
0677         });
0678 
0679         matchCompleteWordAction = new QAction(i18n("Match Complete Word"), this);
0680         matchCompleteWordAction->setCheckable(true);
0681         matchCompleteWordAction->setChecked(false);
0682         connect(matchCompleteWordAction, &QAction::triggered, this, [=]() {
0683             if (!m_leFilter->text().isEmpty())
0684                 filterTextChanged(m_leFilter->text());
0685         });
0686 
0687 #if HAS_FUZZY_MATCHER
0688         fuzzyMatchingAction = new QAction(i18n("Fuzzy Matching"), this);
0689         fuzzyMatchingAction->setCheckable(true);
0690         fuzzyMatchingAction->setChecked(true);
0691         connect(fuzzyMatchingAction, &QAction::triggered, this, [=]() {
0692             bool enabled = !fuzzyMatchingAction->isChecked();
0693             caseSensitiveAction->setEnabled(enabled);
0694             matchCompleteWordAction->setEnabled(enabled);
0695             if (!m_leFilter->text().isEmpty())
0696                 filterTextChanged(m_leFilter->text());
0697         });
0698         caseSensitiveAction->setEnabled(false);
0699         matchCompleteWordAction->setEnabled(false);
0700 #endif
0701     }
0702 
0703     QMenu menu;
0704 #if HAS_FUZZY_MATCHER
0705     menu.addAction(fuzzyMatchingAction);
0706     menu.addSeparator();
0707 #endif
0708     menu.addAction(caseSensitiveAction);
0709     menu.addAction(matchCompleteWordAction);
0710     connect(&menu, &QMenu::aboutToHide, bFilterOptions, &QPushButton::toggle);
0711     menu.exec(bFilterOptions->mapToGlobal(QPoint(0, bFilterOptions->height())));
0712 }
0713 
0714 void ProjectExplorer::resizeHeader() {
0715     m_treeView->header()->resizeSections(QHeaderView::ResizeToContents);
0716     m_treeView->header()->resizeSection(0, m_treeView->header()->sectionSize(0) * 1.2); // make the column "Name" somewhat bigger
0717 }
0718 
0719 /*!
0720   called when the filter/search text was changend.
0721 */
0722 void ProjectExplorer::filterTextChanged(const QString& text) {
0723     QModelIndex root = m_treeView->model()->index(0, 0);
0724     filter(root, text);
0725 }
0726 
0727 bool ProjectExplorer::filter(const QModelIndex& index, const QString& text) {
0728     const auto* model = index.model();
0729     const int rows = model->rowCount(index);
0730 
0731     // if the filter string is empty, just traverse the whole model and make every index visible
0732     if (text.isEmpty()) {
0733         for (int i = 0; i < rows; ++i) {
0734             m_treeView->setRowHidden(i, index, false);
0735             const auto& child = model->index(i, 0, index);
0736             if (model->hasChildren(child))
0737                 filter(child, text);
0738         }
0739         return true;
0740     }
0741 
0742 #if HAS_FUZZY_MATCHER
0743     bool fuzzyFiltering = true;
0744     if (fuzzyMatchingAction && !fuzzyMatchingAction->isChecked())
0745         fuzzyFiltering = false;
0746 #endif
0747 
0748     bool childVisible = false;
0749     for (int i = 0; i < rows; i++) {
0750         const auto& child = model->index(i, 0, index);
0751         auto* aspect = static_cast<AbstractAspect*>(child.internalPointer());
0752         bool visible;
0753 #if HAS_FUZZY_MATCHER
0754         if (fuzzyFiltering)
0755             visible = KFuzzyMatcher::matchSimple(text, aspect->name());
0756         else
0757 #endif
0758         {
0759             bool matchCompleteWord = false;
0760             if (matchCompleteWordAction && matchCompleteWordAction->isChecked())
0761                 matchCompleteWord = true;
0762 
0763             Qt::CaseSensitivity sensitivity = Qt::CaseInsensitive;
0764             if (caseSensitiveAction && caseSensitiveAction->isChecked())
0765                 sensitivity = Qt::CaseSensitive;
0766 
0767             if (matchCompleteWord)
0768                 visible = aspect->name().startsWith(text, sensitivity);
0769             else
0770                 visible = aspect->name().contains(text, sensitivity);
0771         }
0772 
0773         if (visible) {
0774             // current item is visible -> make all its children visible without applying the filter
0775             for (int j = 0; j < model->rowCount(child); ++j)
0776                 m_treeView->setRowHidden(j, child, false);
0777 
0778             childVisible = true;
0779         } else {
0780             // check children items. if one of the children is visible, make the parent (current) item visible too.
0781             visible = filter(child, text);
0782             if (visible)
0783                 childVisible = true;
0784         }
0785 
0786         m_treeView->setRowHidden(i, index, !visible);
0787     }
0788 
0789     return childVisible;
0790 }
0791 
0792 void ProjectExplorer::selectIndex(const QModelIndex& index) {
0793     if (m_project->isLoading())
0794         return;
0795 
0796     DEBUG(Q_FUNC_INFO)
0797 
0798     if (!m_treeView->selectionModel()->isSelected(index)) {
0799         m_changeSelectionFromView = true;
0800         m_treeView->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
0801         m_treeView->setExpanded(index, true);
0802         m_treeView->scrollTo(index);
0803     }
0804 }
0805 
0806 void ProjectExplorer::deselectIndex(const QModelIndex& index) {
0807     if (m_project->isLoading())
0808         return;
0809 
0810     if (m_treeView->selectionModel()->isSelected(index)) {
0811         m_changeSelectionFromView = true;
0812         m_treeView->selectionModel()->select(index, QItemSelectionModel::Deselect | QItemSelectionModel::Rows);
0813     }
0814 }
0815 
0816 void ProjectExplorer::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) {
0817     if (m_project->isLoading())
0818         return;
0819 
0820     // QDEBUG(Q_FUNC_INFO << ", selected/deselected = " << selected << "/" << deselected)
0821 
0822     QModelIndex index;
0823     AbstractAspect* aspect = nullptr;
0824 
0825     // there are four model indices in each row
0826     //-> divide by 4 to obtain the number of selected rows (=aspects)
0827     const auto& sitems = selected.indexes();
0828     for (int i = 0; i < sitems.size() / 4; ++i) {
0829         index = sitems.at(i * 4);
0830         aspect = static_cast<AbstractAspect*>(index.internalPointer());
0831         QDEBUG("sitems ASPECT =" << aspect)
0832         aspect->setSelected(true);
0833     }
0834 
0835     const auto& ditems = deselected.indexes();
0836     for (int i = 0; i < ditems.size() / 4; ++i) {
0837         index = ditems.at(i * 4);
0838         aspect = static_cast<AbstractAspect*>(index.internalPointer());
0839         QDEBUG("ditems ASPECT =" << aspect)
0840         aspect->setSelected(false);
0841     }
0842 
0843     const auto& items = m_treeView->selectionModel()->selectedRows();
0844     QList<AbstractAspect*> selectedAspects;
0845     for (const auto& index : items) {
0846         aspect = static_cast<AbstractAspect*>(index.internalPointer());
0847         QDEBUG("items ASPECT =" << aspect)
0848         selectedAspects << aspect;
0849     }
0850 
0851     // notify GuiObserver about the new selection
0852     Q_EMIT selectedAspectsChanged(selectedAspects);
0853 
0854     // notify MainWin about the new current aspect (last selected aspect).
0855     if (!selectedAspects.isEmpty())
0856         Q_EMIT currentAspectChanged(selectedAspects.last());
0857 
0858     // emitting the signal above is done to show the properties widgets for the selected aspect(s).
0859     // with this the project explorer looses the focus and don't react on the key events like DEL key press, etc.
0860     // If we explicitly select an item in the project explorer (not via a selection in the view), we want to keep the focus here.
0861     // TODO: after the focus is set again we react on DEL in the event filter, but navigation with the arrow keys in the table
0862     // is still not possible. Looks like we need to set the selection again...
0863     if (!m_changeSelectionFromView)
0864         setFocus();
0865     else
0866         m_changeSelectionFromView = false;
0867 }
0868 
0869 void ProjectExplorer::expandSelected() {
0870     const auto& items = m_treeView->selectionModel()->selectedIndexes();
0871     for (const auto& index : items)
0872         m_treeView->setExpanded(index, true);
0873 }
0874 
0875 void ProjectExplorer::collapseSelected() {
0876     const auto& items = m_treeView->selectionModel()->selectedIndexes();
0877     for (const auto& index : items)
0878         m_treeView->setExpanded(index, false);
0879 }
0880 
0881 void ProjectExplorer::changeSelectedVisible() {
0882     const auto& items = m_treeView->selectionModel()->selectedIndexes();
0883 
0884     // determine all selected aspects
0885     QVector<WorksheetElement*> elements;
0886     const auto columnCount = m_treeView->model()->columnCount();
0887     for (int i = 0; i < items.size() / columnCount; ++i) {
0888         const QModelIndex& index = items.at(i * columnCount);
0889         auto* aspect = static_cast<AbstractAspect*>(index.internalPointer());
0890         auto* element = dynamic_cast<WorksheetElement*>(aspect);
0891         if (element)
0892             elements << element;
0893     }
0894 
0895     const int numberElements = elements.size();
0896     if (numberElements == 0)
0897         return;
0898 
0899     // Use first element as reference
0900     const auto& firstElement = elements.constFirst();
0901     const bool newVisible = !firstElement->isVisible();
0902 
0903     if (numberElements > 1) {
0904         firstElement->beginMacro(i18n("%1 elements: set visible: %2").arg(numberElements, newVisible));
0905         for (auto* e : elements)
0906             e->setVisible(newVisible);
0907         firstElement->endMacro();
0908     } else
0909         firstElement->setVisible(newVisible);
0910 }
0911 
0912 void ProjectExplorer::deleteSelected() {
0913     const auto& items = m_treeView->selectionModel()->selectedIndexes();
0914     if (!items.size())
0915         return;
0916 
0917     // determine all selected aspects
0918     QVector<AbstractAspect*> aspects;
0919     const auto columnCount = m_treeView->model()->columnCount();
0920     for (int i = 0; i < items.size() / columnCount; ++i) {
0921         const QModelIndex& index = items.at(i * columnCount);
0922         auto* aspect = static_cast<AbstractAspect*>(index.internalPointer());
0923         aspects << aspect;
0924     }
0925 
0926     QString msg;
0927     if (aspects.size() > 1)
0928         msg = i18n("Do you really want to delete the selected %1 objects?", aspects.size());
0929     else
0930         msg = i18n("Do you really want to delete %1?", aspects.constFirst()->name());
0931 
0932 #if KCOREADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0)
0933     auto status = KMessageBox::warningTwoActions(this,
0934                                                  msg,
0935                                                  i18np("Delete selected object", "Delete selected objects", aspects.size()),
0936                                                  KStandardGuiItem::del(),
0937                                                  KStandardGuiItem::cancel());
0938     if (status == KMessageBox::SecondaryAction)
0939         return;
0940 #else
0941     auto status = KMessageBox::warningYesNo(this, msg, i18np("Delete selected object", "Delete selected objects", aspects.size()));
0942     if (status == KMessageBox::No)
0943         return;
0944 #endif
0945 
0946     m_project->beginMacro(i18np("Project Explorer: delete %1 selected object", "Project Explorer: delete %1 selected objects", items.size() / 4));
0947 
0948     // determine aspects to be deleted:
0949     // it's enough to delete parent items in the selection only,
0950     // skip all selected aspects where one of the parents is also in the selection
0951     // for example selected columns of a selected spreadsheet, etc.
0952     QVector<AbstractAspect*> aspectsToDelete;
0953     for (auto* aspect : aspects) {
0954         auto* parent = aspect->parentAspect();
0955         int parentSelected = false;
0956         while (parent) {
0957             if (aspects.indexOf(parent) != -1) {
0958                 parentSelected = true;
0959                 break;
0960             }
0961 
0962             parent = parent->parentAspect();
0963         }
0964 
0965         if (!parentSelected)
0966             aspectsToDelete << aspect; // parent is not in the selection
0967     }
0968 
0969     for (auto* aspect : aspectsToDelete)
0970         aspect->remove();
0971 
0972     m_project->endMacro();
0973 }
0974 
0975 // ##############################################################################
0976 // ##################  Serialization/Deserialization  ###########################
0977 // ##############################################################################
0978 struct ViewState {
0979     Qt::WindowStates state;
0980     QRect geometry;
0981 };
0982 
0983 /**
0984  * \brief Save the current state of the tree view
0985  * (expanded items and the currently selected item) as XML
0986  */
0987 void ProjectExplorer::save(QXmlStreamWriter* writer) const {
0988     const auto* model = static_cast<AspectTreeModel*>(m_treeView->model());
0989     const auto& selectedRows = m_treeView->selectionModel()->selectedRows();
0990 
0991     writer->writeStartElement(QStringLiteral("state"));
0992 
0993     // check whether the project node itself is expanded or selected or current
0994     const auto& index = m_treeView->model()->index(0, 0);
0995     if (m_treeView->isExpanded(index)) {
0996         writer->writeStartElement(QStringLiteral("expanded"));
0997         writer->writeAttribute(QStringLiteral("path"), m_project->path());
0998         writer->writeEndElement();
0999     }
1000 
1001     if (selectedRows.indexOf(index) != -1) {
1002         writer->writeStartElement(QStringLiteral("selected"));
1003         writer->writeAttribute(QStringLiteral("path"), m_project->path());
1004         writer->writeEndElement();
1005     }
1006 
1007     if (index == m_treeView->currentIndex()) {
1008         writer->writeStartElement(QStringLiteral("current"));
1009         writer->writeAttribute(QStringLiteral("path"), m_project->path());
1010         writer->writeEndElement();
1011     }
1012 
1013     // traverse the children nodes
1014     const auto& children = m_project->children<AbstractAspect>(AbstractAspect::ChildIndexFlag::Recursive);
1015     for (const auto* aspect : children) {
1016         const QString& path = aspect->path();
1017         const auto* part = dynamic_cast<const AbstractPart*>(aspect);
1018 
1019         if (part && part->hasMdiSubWindow()) {
1020             writer->writeStartElement(QStringLiteral("view"));
1021             const auto& geometry = part->dockWidget()->geometry();
1022             writer->writeAttribute(QStringLiteral("path"), path);
1023             writer->writeAttribute(QStringLiteral("state"), QString::number(part->view()->windowState()));
1024             writer->writeAttribute(QStringLiteral("x"), QString::number(geometry.x()));
1025             writer->writeAttribute(QStringLiteral("y"), QString::number(geometry.y()));
1026             writer->writeAttribute(QStringLiteral("width"), QString::number(geometry.width()));
1027             writer->writeAttribute(QStringLiteral("height"), QString::number(geometry.height()));
1028             writer->writeEndElement();
1029         }
1030 
1031         const auto& index = model->modelIndexOfAspect(aspect);
1032         if (model->rowCount(index) > 0 && m_treeView->isExpanded(index)) {
1033             writer->writeStartElement(QStringLiteral("expanded"));
1034             writer->writeAttribute(QStringLiteral("path"), path);
1035             writer->writeEndElement();
1036         }
1037 
1038         if (selectedRows.indexOf(index) != -1) {
1039             writer->writeStartElement(QStringLiteral("selected"));
1040             writer->writeAttribute(QStringLiteral("path"), path);
1041             writer->writeEndElement();
1042         }
1043 
1044         if (index == m_treeView->currentIndex()) {
1045             writer->writeStartElement(QStringLiteral("current"));
1046             writer->writeAttribute(QStringLiteral("path"), path);
1047             writer->writeEndElement();
1048         }
1049     }
1050 
1051     writer->writeEndElement(); //"state"
1052 }
1053 
1054 /**
1055  * \brief Load from XML
1056  */
1057 bool ProjectExplorer::load(XmlStreamReader* reader) {
1058     const auto* model = static_cast<AspectTreeModel*>(m_treeView->model());
1059     QList<QModelIndex> selected;
1060     QList<QModelIndex> expanded;
1061     QModelIndex currentIndex;
1062     QString str;
1063 
1064     // xmlVersion < 3: old logic where the "rows" for the selected and expanded items were saved
1065     // ignoring the sub-folder structure. Remove it later.
1066     if (Project::xmlVersion() < 3) {
1067         const auto& aspects = m_project->children<AbstractAspect>(AbstractAspect::ChildIndexFlag::Recursive);
1068         bool expandedItem = false;
1069         bool selectedItem = false;
1070         bool viewItem = false;
1071         (void)viewItem; // because of a strange g++-warning about unused viewItem
1072         bool currentItem = false;
1073         int row;
1074         QXmlStreamAttributes attribs;
1075 
1076         while (!reader->atEnd()) {
1077             reader->readNext();
1078             if (reader->isEndElement() && reader->name() == QLatin1String("state"))
1079                 break;
1080 
1081             if (!reader->isStartElement())
1082                 continue;
1083 
1084             if (reader->name() == QLatin1String("expanded")) {
1085                 expandedItem = true;
1086                 selectedItem = false;
1087                 viewItem = false;
1088                 currentItem = false;
1089             } else if (reader->name() == QLatin1String("selected")) {
1090                 expandedItem = false;
1091                 selectedItem = true;
1092                 viewItem = false;
1093                 currentItem = false;
1094             } else if (reader->name() == QLatin1String("view")) {
1095                 expandedItem = false;
1096                 selectedItem = false;
1097                 viewItem = true;
1098                 currentItem = false;
1099             } else if (reader->name() == QLatin1String("current")) {
1100                 expandedItem = false;
1101                 selectedItem = false;
1102                 viewItem = false;
1103                 currentItem = true;
1104             } else if (reader->name() == QLatin1String("row")) {
1105                 // we need to read the attributes first and before readElementText() otherwise they are empty
1106                 attribs = reader->attributes();
1107                 row = reader->readElementText().toInt();
1108 
1109                 QModelIndex index;
1110                 if (row == -1)
1111                     index = model->modelIndexOfAspect(m_project); //-1 corresponds to the project-item (s.a. ProjectExplorer::save())
1112                 else if (row >= aspects.size() || row < 0) // checking for <0 to protect against wrong values in the XML
1113                     continue;
1114                 else
1115                     index = model->modelIndexOfAspect(aspects.at(row));
1116 
1117                 if (expandedItem)
1118                     expanded.push_back(index);
1119                 else if (selectedItem)
1120                     selected.push_back(index);
1121                 else if (currentItem)
1122                     currentIndex = index;
1123                 else if (viewItem) {
1124                     if (row < 0) // should never happen, but we need to handle the corrupted file
1125                         continue;
1126 
1127                     auto* part = dynamic_cast<AbstractPart*>(aspects.at(row));
1128                     if (!part)
1129                         continue; // TODO: add error/warning message here?
1130 
1131                     Q_EMIT activateView(part); // request to show the view in MainWin
1132 
1133                     str = attribs.value(QStringLiteral("state")).toString();
1134                     if (str.isEmpty())
1135                         reader->raiseMissingAttributeWarning(QStringLiteral("state"));
1136                     else {
1137                         part->view()->setWindowState(Qt::WindowStates(str.toInt()));
1138                         part->dockWidget()->setWindowState(Qt::WindowStates(str.toInt()));
1139                     }
1140 
1141                     if (str != QLatin1String("0"))
1142                         continue; // no geometry settings required for maximized/minimized windows
1143 
1144                     QRect geometry;
1145                     str = attribs.value(QStringLiteral("x")).toString();
1146                     if (str.isEmpty())
1147                         reader->raiseMissingAttributeWarning(QStringLiteral("x"));
1148                     else
1149                         geometry.setX(str.toInt());
1150 
1151                     str = attribs.value(QStringLiteral("y")).toString();
1152                     if (str.isEmpty())
1153                         reader->raiseMissingAttributeWarning(QStringLiteral("y"));
1154                     else
1155                         geometry.setY(str.toInt());
1156 
1157                     str = attribs.value(QStringLiteral("width")).toString();
1158                     if (str.isEmpty())
1159                         reader->raiseMissingAttributeWarning(QStringLiteral("width"));
1160                     else
1161                         geometry.setWidth(str.toInt());
1162 
1163                     str = attribs.value(QStringLiteral("height")).toString();
1164                     if (str.isEmpty())
1165                         reader->raiseMissingAttributeWarning(QStringLiteral("height"));
1166                     else
1167                         geometry.setHeight(str.toInt());
1168 
1169                     part->dockWidget()->setGeometry(geometry);
1170                 }
1171             }
1172         }
1173 
1174     } else {
1175         while (!reader->atEnd()) {
1176             reader->readNext();
1177             if (reader->isEndElement() && reader->name() == QLatin1String("state"))
1178                 break;
1179 
1180             if (!reader->isStartElement())
1181                 continue;
1182 
1183             const auto& attribs = reader->attributes();
1184             const auto& path = attribs.value(QStringLiteral("path")).toString();
1185             const auto& index = model->modelIndexOfAspect(path);
1186 
1187             if (reader->name() == QLatin1String("expanded"))
1188                 expanded << index;
1189             else if (reader->name() == QLatin1String("selected"))
1190                 selected << index;
1191             else if (reader->name() == QLatin1String("current"))
1192                 currentIndex = index;
1193             else if (reader->name() == QLatin1String("view")) {
1194                 auto* aspect = static_cast<AbstractAspect*>(index.internalPointer());
1195                 auto* part = dynamic_cast<AbstractPart*>(aspect);
1196                 if (!part)
1197                     continue; // TODO: add error/warning message here?
1198 
1199                 Q_EMIT activateView(part); // request to show the view in MainWin
1200 
1201                 str = attribs.value(QStringLiteral("state")).toString();
1202                 if (str.isEmpty())
1203                     reader->raiseMissingAttributeWarning(QStringLiteral("state"));
1204                 else {
1205                     part->view()->setWindowState(Qt::WindowStates(str.toInt()));
1206                     part->dockWidget()->setWindowState(Qt::WindowStates(str.toInt()));
1207                 }
1208 
1209                 if (str != QLatin1String("0"))
1210                     continue; // no geometry settings required for maximized/minimized windows
1211 
1212                 QRect geometry;
1213                 str = attribs.value(QStringLiteral("x")).toString();
1214                 if (str.isEmpty())
1215                     reader->raiseMissingAttributeWarning(QStringLiteral("x"));
1216                 else
1217                     geometry.setX(str.toInt());
1218 
1219                 str = attribs.value(QStringLiteral("y")).toString();
1220                 if (str.isEmpty())
1221                     reader->raiseMissingAttributeWarning(QStringLiteral("y"));
1222                 else
1223                     geometry.setY(str.toInt());
1224 
1225                 str = attribs.value(QStringLiteral("width")).toString();
1226                 if (str.isEmpty())
1227                     reader->raiseMissingAttributeWarning(QStringLiteral("width"));
1228                 else
1229                     geometry.setWidth(str.toInt());
1230 
1231                 str = attribs.value(QStringLiteral("height")).toString();
1232                 if (str.isEmpty())
1233                     reader->raiseMissingAttributeWarning(QStringLiteral("height"));
1234                 else
1235                     geometry.setHeight(str.toInt());
1236 
1237                 part->dockWidget()->setGeometry(geometry);
1238             }
1239         }
1240     }
1241 
1242     for (const auto& index : expanded) {
1243         m_treeView->setExpanded(index, true);
1244         collapseParents(index, expanded); // collapse all parent indices if they are not expanded
1245     }
1246 
1247     for (const auto& index : selected)
1248         m_treeView->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows);
1249 
1250     m_treeView->setCurrentIndex(currentIndex);
1251     m_treeView->scrollTo(currentIndex);
1252     auto* aspect = static_cast<AbstractAspect*>(currentIndex.internalPointer());
1253     if (aspect)
1254         aspect->setSelected(true);
1255     Q_EMIT currentAspectChanged(aspect); // notify MainWin to bring up the proper view
1256     Q_EMIT selectedAspectsChanged(QList<AbstractAspect*>() << aspect); // notify GuiObserver to bring up the proper dock widget
1257 
1258     // when setting the current index above it gets expanded, collapse all parent indices if they were not expanded when saved
1259     collapseParents(currentIndex, expanded);
1260 
1261     // resize the header of the view to adjust to the content
1262     resizeHeader();
1263 
1264     return true;
1265 }
1266 
1267 void ProjectExplorer::collapseParents(const QModelIndex& index, const QList<QModelIndex>& expanded) {
1268     if (index.column() == 0 && index.row() == 0) { // root/project index, doesn't have any parent
1269         if (expanded.indexOf(index) == -1)
1270             m_treeView->collapse(index);
1271     } else {
1272         const auto& parent = index.parent();
1273         if (parent != QModelIndex() && expanded.indexOf(parent) == -1)
1274             m_treeView->collapse(parent);
1275     }
1276 }