File indexing completed on 2024-04-28 04:39:10

0001 /*
0002     SPDX-FileCopyrightText: 2005 Roberto Raggi <roberto@kdevelop.org>
0003     SPDX-FileCopyrightText: 2007 Andreas Pakulat <apaku@gmx.de>
0004     SPDX-FileCopyrightText: 2009 Aleix Pol <aleixpol@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "projecttreeview.h"
0010 
0011 
0012 #include <QAction>
0013 #include <QAbstractProxyModel>
0014 #include <QKeyEvent>
0015 #include <QApplication>
0016 #include <QHeaderView>
0017 #include <QMenu>
0018 #include <QPainter>
0019 
0020 #include <KConfigGroup>
0021 #include <KLocalizedString>
0022 
0023 #include <project/projectmodel.h>
0024 #include <interfaces/contextmenuextension.h>
0025 #include <interfaces/iprojectcontroller.h>
0026 #include <interfaces/iproject.h>
0027 #include <interfaces/context.h>
0028 #include <interfaces/iplugincontroller.h>
0029 #include <interfaces/icore.h>
0030 #include <interfaces/iselectioncontroller.h>
0031 #include <interfaces/isession.h>
0032 #include <project/interfaces/iprojectfilemanager.h>
0033 #include <project/interfaces/ibuildsystemmanager.h>
0034 
0035 #include "projectmanagerviewplugin.h"
0036 #include "projectmodelsaver.h"
0037 #include "projectmodelitemdelegate.h"
0038 #include "debug.h"
0039 #include <project/projectutils.h>
0040 #include <widgetcolorizer.h>
0041 
0042 using namespace KDevelop;
0043 
0044 namespace {
0045 
0046 QString settingsConfigGroup() { return QStringLiteral("ProjectTreeView"); }
0047 
0048 QList<ProjectFileItem*> fileItemsWithin(const QList<ProjectBaseItem*>& items)
0049 {
0050     QList<ProjectFileItem*> fileItems;
0051     fileItems.reserve(items.size());
0052     for (ProjectBaseItem* item : items) {
0053         if (ProjectFileItem *file = item->file())
0054             fileItems.append(file);
0055         else if (item->folder())
0056             fileItems.append(fileItemsWithin(item->children()));
0057     }
0058     return fileItems;
0059 }
0060 
0061 QList<ProjectBaseItem*> topLevelItemsWithin(QList<ProjectBaseItem*> items)
0062 {
0063     std::sort(items.begin(), items.end(), ProjectBaseItem::pathLessThan);
0064     Path lastFolder;
0065     for (int i = items.size() - 1; i >= 0; --i)
0066     {
0067         if (lastFolder.isParentOf(items[i]->path()))
0068             items.removeAt(i);
0069         else if (items[i]->folder())
0070             lastFolder = items[i]->path();
0071     }
0072     return items;
0073 }
0074 
0075 template<class T>
0076 void filterDroppedItems(QList<T*> &items, ProjectBaseItem* dest)
0077 {
0078     for (int i = items.size() - 1; i >= 0; --i)
0079     {
0080         //No drag and drop from and to same location
0081         if (items[i]->parent() == dest)
0082             items.removeAt(i);
0083         //No moving between projects (technically feasible if the projectmanager is the same though...)
0084         else if (items[i]->project() != dest->project())
0085             items.removeAt(i);
0086     }
0087 }
0088 
0089 //TODO test whether this could be replaced by projectbuildsetwidget.cpp::showContextMenu_appendActions
0090 void popupContextMenu_appendActions(QMenu& menu, const QList<QAction*>& actions)
0091 {
0092     menu.addActions(actions);
0093     menu.addSeparator();
0094 }
0095 
0096 }
0097 
0098 ProjectTreeView::ProjectTreeView( QWidget *parent )
0099         : QTreeView( parent ), m_previousSelection ( nullptr )
0100 {
0101     header()->hide();
0102 
0103     setEditTriggers( QAbstractItemView::EditKeyPressed );
0104 
0105     setContextMenuPolicy( Qt::CustomContextMenu );
0106     setSelectionMode( QAbstractItemView::ExtendedSelection );
0107 
0108     setIndentation(10);
0109 
0110     setDragEnabled(true);
0111     setDragDropMode(QAbstractItemView::InternalMove);
0112     setAutoScroll(true);
0113     setAutoExpandDelay(300);
0114     setItemDelegate(new ProjectModelItemDelegate(this));
0115 
0116     connect( this, &ProjectTreeView::customContextMenuRequested, this, &ProjectTreeView::popupContextMenu );
0117     connect( this, &ProjectTreeView::activated, this, &ProjectTreeView::slotActivated );
0118 
0119     connect( ICore::self(), &ICore::aboutToShutdown,
0120              this, &ProjectTreeView::aboutToShutdown);
0121     connect( ICore::self()->projectController(), &IProjectController::projectOpened,
0122              this, &ProjectTreeView::restoreState );
0123     connect( ICore::self()->projectController(), &IProjectController::projectClosed,
0124              this, &ProjectTreeView::projectClosed );
0125 }
0126 
0127 ProjectTreeView::~ProjectTreeView()
0128 {
0129 }
0130 
0131 ProjectBaseItem* ProjectTreeView::itemAtPos(const QPoint& pos) const
0132 {
0133     return indexAt(pos).data(ProjectModel::ProjectItemRole).value<ProjectBaseItem*>();
0134 }
0135 
0136 void ProjectTreeView::dropEvent(QDropEvent* event)
0137 {
0138     auto* selectionCtxt =
0139             static_cast<ProjectItemContext*>(KDevelop::ICore::self()->selectionController()->currentSelection());
0140     ProjectBaseItem* destItem = itemAtPos(event->pos());
0141     if (destItem && (dropIndicatorPosition() == AboveItem || dropIndicatorPosition() == BelowItem))
0142             destItem = destItem->parent();
0143     if (selectionCtxt && destItem)
0144     {
0145         if (ProjectFolderItem *folder = destItem->folder())
0146         {
0147             QMenu dropMenu(this);
0148 
0149             QString seq = QKeySequence( Qt::ShiftModifier ).toString();
0150             seq.chop(1); // chop superfluous '+'
0151             auto* move = new QAction(i18nc("@action:inmenu", "&Move Here") + QLatin1Char('\t') + seq, &dropMenu);
0152             move->setIcon(QIcon::fromTheme(QStringLiteral("edit-move"), QIcon::fromTheme(QStringLiteral("go-jump"))));
0153             dropMenu.addAction(move);
0154 
0155             seq = QKeySequence( Qt::ControlModifier ).toString();
0156             seq.chop(1);
0157             auto* copy = new QAction(i18nc("@action:inmenu", "&Copy Here") + QLatin1Char('\t') + seq, &dropMenu);
0158             copy->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
0159             dropMenu.addAction(copy);
0160 
0161             dropMenu.addSeparator();
0162 
0163             auto* cancel = new QAction(i18nc("@action:inmenu", "C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString(), &dropMenu);
0164             cancel->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
0165             dropMenu.addAction(cancel);
0166 
0167             QAction *executedAction = nullptr;
0168 
0169             Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers();
0170             if (modifiers == Qt::ControlModifier) {
0171                 executedAction = copy;
0172             } else if (modifiers == Qt::ShiftModifier) {
0173                 executedAction = move;
0174             } else {
0175                 executedAction = dropMenu.exec(this->mapToGlobal(event->pos()));
0176             }
0177 
0178             QList<ProjectBaseItem*> usefulItems = topLevelItemsWithin(selectionCtxt->items());
0179             filterDroppedItems(usefulItems, destItem);
0180             Path::List paths;
0181             paths.reserve(usefulItems.size());
0182             for (ProjectBaseItem* i : qAsConst(usefulItems)) {
0183                 paths << i->path();
0184             }
0185             bool success = false;
0186             if (executedAction == copy) {
0187                 success = destItem->project()->projectFileManager()->copyFilesAndFolders(paths, folder);
0188             } else if (executedAction == move) {
0189                 success = destItem->project()->projectFileManager()->moveFilesAndFolders(usefulItems, folder);
0190             }
0191 
0192             if (success) {
0193                 //expand target folder
0194                 expand( mapFromItem(folder));
0195 
0196                 //and select new items
0197                 QItemSelection selection;
0198                 for (const Path& path : qAsConst(paths)) {
0199                     const Path targetPath(folder->path(), path.lastPathSegment());
0200                     const auto folderChildren = folder->children();
0201                     for (ProjectBaseItem* item : folderChildren) {
0202                         if (item->path() == targetPath) {
0203                             QModelIndex indx = mapFromItem( item );
0204                             selection.append(QItemSelectionRange(indx, indx));
0205                             setCurrentIndex(indx);
0206                         }
0207                     }
0208                 }
0209                 selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect);
0210             }
0211         }
0212         else if (destItem->target() && destItem->project()->buildSystemManager())
0213         {
0214             QMenu dropMenu(this);
0215 
0216             QString seq = QKeySequence( Qt::ControlModifier ).toString();
0217             seq.chop(1);
0218             auto* addToTarget = new QAction(i18nc("@action:inmenu", "&Add to Build Target") + QLatin1Char('\t') + seq, &dropMenu);
0219             addToTarget->setIcon(QIcon::fromTheme(QStringLiteral("edit-link")));
0220             dropMenu.addAction(addToTarget);
0221 
0222             dropMenu.addSeparator();
0223 
0224             auto* cancel = new QAction(i18nc("@action:inmenu", "C&ancel") + QLatin1Char('\t') + QKeySequence(Qt::Key_Escape).toString(), &dropMenu);
0225             cancel->setIcon(QIcon::fromTheme(QStringLiteral("process-stop")));
0226             dropMenu.addAction(cancel);
0227 
0228             QAction *executedAction = nullptr;
0229 
0230             Qt::KeyboardModifiers modifiers = QApplication::keyboardModifiers();
0231             if (modifiers == Qt::ControlModifier) {
0232                 executedAction = addToTarget;
0233             } else {
0234                 executedAction = dropMenu.exec(this->mapToGlobal(event->pos()));
0235             }
0236             if (executedAction == addToTarget) {
0237                 QList<ProjectFileItem*> usefulItems = fileItemsWithin(selectionCtxt->items());
0238                 filterDroppedItems(usefulItems, destItem);
0239                 destItem->project()->buildSystemManager()->addFilesToTarget(usefulItems, destItem->target());
0240             }
0241         }
0242     }
0243     event->accept();
0244 }
0245 
0246 QModelIndex ProjectTreeView::mapFromSource(const QAbstractProxyModel* proxy, const QModelIndex& sourceIdx)
0247 {
0248     const QAbstractItemModel* next = proxy->sourceModel();
0249     Q_ASSERT(next == sourceIdx.model() || qobject_cast<const QAbstractProxyModel*>(next));
0250     if(next == sourceIdx.model())
0251         return proxy->mapFromSource(sourceIdx);
0252     else {
0253         const auto* nextProxy = qobject_cast<const QAbstractProxyModel*>(next);
0254         QModelIndex idx = mapFromSource(nextProxy, sourceIdx);
0255         Q_ASSERT(idx.model() == nextProxy);
0256         return proxy->mapFromSource(idx);
0257     }
0258 }
0259 
0260 QModelIndex ProjectTreeView::mapFromItem(const ProjectBaseItem* item)
0261 {
0262     QModelIndex ret = mapFromSource(qobject_cast<const QAbstractProxyModel*>(model()), item->index());
0263     Q_ASSERT(ret.model() == model());
0264     return ret;
0265 }
0266 
0267 void ProjectTreeView::slotActivated( const QModelIndex &index )
0268 {
0269     if ( QApplication::keyboardModifiers() & Qt::CTRL || QApplication::keyboardModifiers() & Qt::SHIFT ) {
0270         // Do not open file when Ctrl or Shift is pressed; that's for selection
0271         return;
0272     }
0273     auto *item = index.data(ProjectModel::ProjectItemRole).value<ProjectBaseItem*>();
0274     if ( item && item->file() )
0275     {
0276         emit activate( item->file()->path() );
0277     }
0278 }
0279 
0280 void ProjectTreeView::projectClosed(KDevelop::IProject* project)
0281 {
0282     if ( project == m_previousSelection )
0283         m_previousSelection = nullptr;
0284 }
0285 
0286 
0287 QList<ProjectBaseItem*> ProjectTreeView::selectedProjects()
0288 {
0289     QList<ProjectBaseItem*> itemlist;
0290     if ( selectionModel()->hasSelection() ) {
0291         const QModelIndexList indexes = selectionModel()->selectedRows();
0292         for ( const QModelIndex& index: indexes ) {
0293             auto* item = index.data( ProjectModel::ProjectItemRole ).value<ProjectBaseItem*>();
0294             if ( item ) {
0295                 itemlist << item;
0296                 m_previousSelection = item->project();
0297             }
0298         }
0299     }
0300 
0301     // add previous selection if nothing is selected right now
0302     if ( itemlist.isEmpty() && m_previousSelection ) {
0303         itemlist << m_previousSelection->projectItem();
0304     }
0305 
0306     return itemlist;
0307 }
0308 
0309 KDevelop::IProject* ProjectTreeView::getCurrentProject()
0310 {
0311     auto itemList = selectedProjects();
0312     if ( !itemList.isEmpty() ) {
0313         return itemList.at( 0 )->project();
0314     }
0315     return nullptr;
0316 }
0317 
0318 void ProjectTreeView::popupContextMenu( const QPoint &pos )
0319 {
0320     QList<ProjectBaseItem*> itemlist;
0321     if ( indexAt( pos ).isValid() ) {
0322         itemlist = selectedProjects();
0323     }
0324     QMenu menu( this );
0325 
0326     KDevelop::ProjectItemContextImpl context(itemlist);
0327     const QList<ContextMenuExtension> extensions = ICore::self()->pluginController()->queryPluginsForContextMenuExtensions(&context, &menu);
0328 
0329     QList<QAction*> buildActions;
0330     QList<QAction*> vcsActions;
0331     QList<QAction*> analyzeActions;
0332     QList<QAction*> extActions;
0333     QList<QAction*> projectActions;
0334     QList<QAction*> fileActions;
0335     QList<QAction*> runActions;
0336     for (const ContextMenuExtension& ext : extensions) {
0337         buildActions += ext.actions(ContextMenuExtension::BuildGroup);
0338         fileActions += ext.actions(ContextMenuExtension::FileGroup);
0339         projectActions += ext.actions(ContextMenuExtension::ProjectGroup);
0340         vcsActions += ext.actions(ContextMenuExtension::VcsGroup);
0341         analyzeActions += ext.actions(ContextMenuExtension::AnalyzeProjectGroup);
0342         extActions += ext.actions(ContextMenuExtension::ExtensionGroup);
0343         runActions += ext.actions(ContextMenuExtension::RunGroup);
0344     }
0345 
0346     if ( analyzeActions.count() )
0347     {
0348         auto* analyzeMenu = new QMenu(i18nc("@title:menu", "Analyze with"), &menu);
0349         analyzeMenu->setIcon(QIcon::fromTheme(QStringLiteral("dialog-ok")));
0350         for (QAction* act : qAsConst(analyzeActions)) {
0351             analyzeMenu->addAction( act );
0352         }
0353         analyzeActions = {analyzeMenu->menuAction()};
0354     }
0355 
0356     popupContextMenu_appendActions(menu, buildActions);
0357     popupContextMenu_appendActions(menu, runActions );
0358     popupContextMenu_appendActions(menu, fileActions);
0359     popupContextMenu_appendActions(menu, vcsActions);
0360     popupContextMenu_appendActions(menu, analyzeActions);
0361     popupContextMenu_appendActions(menu, extActions);
0362 
0363     if (itemlist.size() == 1 && itemlist.first()->folder() && !itemlist.first()->folder()->parent()) {
0364         auto* projectConfig = new QAction(i18nc("@action:inmenu", "Open Configuration..."), &menu);
0365         projectConfig->setIcon(QIcon::fromTheme(QStringLiteral("configure")));
0366         connect( projectConfig, &QAction::triggered, this, &ProjectTreeView::openProjectConfig );
0367         projectActions << projectConfig;
0368     }
0369     popupContextMenu_appendActions(menu, projectActions);
0370 
0371     if ( !menu.isEmpty() ) {
0372         menu.exec(viewport()->mapToGlobal(pos));
0373     }
0374 }
0375 
0376 void ProjectTreeView::openProjectConfig()
0377 {
0378     if ( IProject* project = getCurrentProject() ) {
0379         IProjectController* ip = ICore::self()->projectController();
0380         ip->configureProject( project );
0381     }
0382 }
0383 
0384 void ProjectTreeView::saveState( IProject* project )
0385 {
0386     // nullptr won't create a usable saved state, so spare the effort
0387     if ( !project ) {
0388         return;
0389     }
0390 
0391     KConfigGroup configGroup( ICore::self()->activeSession()->config(),
0392                               settingsConfigGroup() + project->name() );
0393 
0394     ProjectModelSaver saver;
0395     saver.setProject( project );
0396     saver.setView( this );
0397     saver.saveState( configGroup );
0398 }
0399 
0400 void ProjectTreeView::restoreState( IProject* project )
0401 {
0402     if ( !project ) {
0403         return;
0404     }
0405 
0406     KConfigGroup configGroup( ICore::self()->activeSession()->config(),
0407                               settingsConfigGroup() + project->name() );
0408     ProjectModelSaver saver;
0409     saver.setProject( project );
0410     saver.setView( this );
0411     saver.restoreState( configGroup );
0412 }
0413 
0414 void ProjectTreeView::rowsInserted( const QModelIndex& parent, int start, int end )
0415 {
0416     QTreeView::rowsInserted( parent, start, end );
0417 
0418     if ( !parent.model() ) {
0419         const auto& projects = selectedProjects();
0420         for (const auto& project: projects) {
0421             restoreState( project->project() );
0422         }
0423     }
0424 }
0425 
0426 void ProjectTreeView::rowsAboutToBeRemoved( const QModelIndex& parent, int start, int end )
0427 {
0428     if ( !parent.model() ) {
0429         const auto& projects = selectedProjects();
0430         for (const auto& project : projects) {
0431             saveState( project->project() );
0432         }
0433     }
0434 
0435     QTreeView::rowsAboutToBeRemoved( parent, start, end );
0436 }
0437 
0438 void ProjectTreeView::aboutToShutdown()
0439 {
0440     // save all projects, not just the selected ones
0441     const auto projects = ICore::self()->projectController()->projects();
0442     for ( const auto& project: projects ) {
0443         saveState( project );
0444     }
0445 }
0446 
0447 void ProjectTreeView::keyPressEvent(QKeyEvent* event)
0448 {
0449     if (event->key() == Qt::Key_Return && currentIndex().isValid() && state()!=QAbstractItemView::EditingState)
0450     {
0451         event->accept();
0452         slotActivated(currentIndex());
0453     }
0454     else
0455         QTreeView::keyPressEvent(event);
0456 }
0457 
0458 void ProjectTreeView::drawBranches(QPainter* painter, const QRect& rect, const QModelIndex& index) const
0459 {
0460     if (WidgetColorizer::colorizeByProject()) {
0461         const auto projectPath = index.data(ProjectModel::ProjectRole).value<IProject *>()->path();
0462         const QColor color = WidgetColorizer::colorForId(qHash(projectPath), palette(), true);
0463         WidgetColorizer::drawBranches(this, painter, rect, index, color);
0464     }
0465 
0466     QTreeView::drawBranches(painter, rect, index);
0467 }
0468 
0469 #include "moc_projecttreeview.cpp"