File indexing completed on 2024-05-19 08:31:05
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"