1 /*
2     SPDX-FileCopyrightText: 2005 Roberto Raggi <roberto@kdevelop.org>
3     SPDX-FileCopyrightText: 2007 Andreas Pakulat <apaku@gmx.de>
4     SPDX-FileCopyrightText: 2008 Aleix Pol <aleixpol@gmail.com>
5 
6     SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8 
9 #include "projectmanagerview.h"
10 
11 #include <QAction>
12 #include <QHeaderView>
13 #include <QKeyEvent>
14 #include <QUrl>
15 
16 #include <KActionCollection>
17 #include <KLocalizedString>
18 
19 #include <interfaces/iselectioncontroller.h>
20 #include <interfaces/context.h>
21 #include <interfaces/icore.h>
22 #include <interfaces/isession.h>
23 #include <interfaces/iprojectcontroller.h>
24 #include <interfaces/iuicontroller.h>
25 #include <interfaces/idocumentcontroller.h>
26 #include <interfaces/iproject.h>
27 #include <project/projectproxymodel.h>
28 #include <project/projectmodel.h>
29 #include <serialization/indexedstring.h>
30 #include <util/path.h>
31 
32 #include "../openwith/iopenwith.h"
33 
34 #include <sublime/mainwindow.h>
35 #include <sublime/area.h>
36 
37 #include "projectmanagerviewplugin.h"
38 #include "vcsoverlayproxymodel.h"
39 #include "ui_projectmanagerview.h"
40 #include "debug.h"
41 
42 
43 using namespace KDevelop;
44 
ProjectManagerViewItemContext(const QList<ProjectBaseItem * > & items,ProjectManagerView * view)45 ProjectManagerViewItemContext::ProjectManagerViewItemContext(const QList< ProjectBaseItem* >& items, ProjectManagerView* view)
46     : ProjectItemContextImpl(items), m_view(view)
47 {
48 }
49 
view() const50 ProjectManagerView *ProjectManagerViewItemContext::view() const
51 {
52     return m_view;
53 }
54 
55 
56 static const char sessionConfigGroup[] = "ProjectManagerView";
57 static const char splitterStateConfigKey[] = "splitterState";
58 static const char syncCurrentDocumentKey[] = "syncCurrentDocument";
59 static const char targetsVisibleConfigKey[] = "targetsVisible";
60 static const int projectTreeViewStrechFactor = 75; // %
61 static const int projectBuildSetStrechFactor = 25; // %
62 
ProjectManagerView(ProjectManagerViewPlugin * plugin,QWidget * parent)63 ProjectManagerView::ProjectManagerView( ProjectManagerViewPlugin* plugin, QWidget *parent )
64         : QWidget( parent ), m_ui(new Ui::ProjectManagerView), m_plugin(plugin)
65 {
66     m_ui->setupUi( this );
67     setFocusProxy(m_ui->projectTreeView);
68 
69     m_ui->projectTreeView->installEventFilter(this);
70 
71     setWindowIcon( QIcon::fromTheme( QStringLiteral("project-development"), windowIcon() ) );
72     setWindowTitle(i18nc("@title:window", "Projects"));
73 
74     KConfigGroup pmviewConfig(ICore::self()->activeSession()->config(), sessionConfigGroup);
75     if (pmviewConfig.hasKey(splitterStateConfigKey)) {
76         QByteArray geometry = pmviewConfig.readEntry<QByteArray>(splitterStateConfigKey, QByteArray());
77         m_ui->splitter->restoreState(geometry);
78     } else {
79         m_ui->splitter->setStretchFactor(0, projectTreeViewStrechFactor);
80         m_ui->splitter->setStretchFactor(1, projectBuildSetStrechFactor);
81     }
82 
83     // keep the project tree view from collapsing (would confuse users)
84     m_ui->splitter->setCollapsible(0, false);
85 
86     m_syncAction = plugin->actionCollection()->action(QStringLiteral("locate_document"));
87     Q_ASSERT(m_syncAction);
88     m_syncAction->setCheckable(true);
89     m_syncAction->setChecked(pmviewConfig.readEntry<bool>(syncCurrentDocumentKey, true));
90     m_syncAction->setShortcutContext(Qt::WidgetWithChildrenShortcut);
91     m_syncAction->setText(i18nc("@action", "Locate Current Document"));
92     m_syncAction->setToolTip(i18nc("@info:tooltip", "Locates the current document in the project tree and selects it."));
93     m_syncAction->setIcon(QIcon::fromTheme(QStringLiteral("dirsync")));
94     m_syncAction->setShortcut(Qt::CTRL | Qt::Key_Less);
95     connect(m_syncAction, &QAction::triggered, this, &ProjectManagerView::toggleSyncCurrentDocument);
96     connect(ICore::self()->documentController(), &KDevelop::IDocumentController::documentActivated, this, [this]{
97         if (m_syncAction->isChecked()) {
98             locateCurrentDocument();
99         }
100     });
101     addAction(m_syncAction);
102     updateSyncAction();
103 
104     m_toggleTargetsAction = new QAction(i18nc("@action", "Show Build Targets"), this);
105     m_toggleTargetsAction->setCheckable(true);
106     m_toggleTargetsAction->setChecked(pmviewConfig.readEntry<bool>(targetsVisibleConfigKey, true));
107     m_toggleTargetsAction->setIcon(QIcon::fromTheme(QStringLiteral("system-run")));
108     connect(m_toggleTargetsAction, &QAction::triggered, this, &ProjectManagerView::toggleHideTargets);
109     addAction(m_toggleTargetsAction);
110 
111     addAction(plugin->actionCollection()->action(QStringLiteral("project_build")));
112     addAction(plugin->actionCollection()->action(QStringLiteral("project_install")));
113     addAction(plugin->actionCollection()->action(QStringLiteral("project_clean")));
114 
115     connect(m_ui->projectTreeView, &ProjectTreeView::activate, this, &ProjectManagerView::open);
116 
117     m_ui->buildSetView->setProjectView( this );
118 
119     m_modelFilter = new ProjectProxyModel( this );
120     m_modelFilter->showTargets(m_toggleTargetsAction->isChecked());
121     m_modelFilter->setSourceModel(ICore::self()->projectController()->projectModel());
122     m_overlayProxy = new VcsOverlayProxyModel( this );
123     m_overlayProxy->setSourceModel(m_modelFilter);
124 
125     m_ui->projectTreeView->setModel( m_overlayProxy );
126 
127     connect( m_ui->projectTreeView->selectionModel(), &QItemSelectionModel::selectionChanged,
128              this, &ProjectManagerView::selectionChanged );
129     connect( KDevelop::ICore::self()->documentController(), &IDocumentController::documentClosed,
130              this, &ProjectManagerView::updateSyncAction);
131     connect( KDevelop::ICore::self()->documentController(), &IDocumentController::documentActivated,
132              this, &ProjectManagerView::updateSyncAction);
133     connect( qobject_cast<Sublime::MainWindow*>(KDevelop::ICore::self()->uiController()->activeMainWindow()), &Sublime::MainWindow::areaChanged,
134              this, &ProjectManagerView::updateSyncAction);
135     selectionChanged();
136 
137     //Update the "sync" button after the initialization has completed, to see whether there already is some open documents
138     QMetaObject::invokeMethod(this, "updateSyncAction", Qt::QueuedConnection);
139 
140     // Need to set this to get horizontal scrollbar. Also needs to be done after
141     // the setModel call
142     m_ui->projectTreeView->header()->setSectionResizeMode( QHeaderView::ResizeToContents );
143 }
144 
eventFilter(QObject * obj,QEvent * event)145 bool ProjectManagerView::eventFilter(QObject* obj, QEvent* event)
146 {
147     if (obj == m_ui->projectTreeView) {
148         if (event->type() == QEvent::KeyRelease) {
149             auto* keyEvent = static_cast<QKeyEvent*>(event);
150             if (keyEvent->key() == Qt::Key_Delete && keyEvent->modifiers() == Qt::NoModifier) {
151                 m_plugin->removeItems(selectedItems());
152                 return true;
153             } else if (keyEvent->key() == Qt::Key_F2 && keyEvent->modifiers() == Qt::NoModifier) {
154                 m_plugin->renameItems(selectedItems());
155                 return true;
156             } else if (keyEvent->key() == Qt::Key_C && keyEvent->modifiers() == Qt::ControlModifier) {
157                 m_plugin->copyFromContextMenu();
158                 return true;
159             } else if (keyEvent->key() == Qt::Key_V && keyEvent->modifiers() == Qt::ControlModifier) {
160                 m_plugin->pasteFromContextMenu();
161                 return true;
162             }
163         }
164     }
165     return QObject::eventFilter(obj, event);
166 }
167 
selectionChanged()168 void ProjectManagerView::selectionChanged()
169 {
170     m_ui->buildSetView->selectionChanged();
171     QList<ProjectBaseItem*> selected;
172     const auto selectedRows = m_ui->projectTreeView->selectionModel()->selectedRows();
173     selected.reserve(selectedRows.size());
174     for (const auto& idx : selectedRows) {
175         selected << ICore::self()->projectController()->projectModel()->itemFromIndex(indexFromView( idx ));
176     }
177     selected.removeAll(nullptr);
178     KDevelop::ICore::self()->selectionController()->updateSelection( new ProjectManagerViewItemContext( selected, this ) );
179 }
180 
updateSyncAction()181 void ProjectManagerView::updateSyncAction()
182 {
183     m_syncAction->setEnabled( KDevelop::ICore::self()->documentController()->activeDocument() );
184 }
185 
~ProjectManagerView()186 ProjectManagerView::~ProjectManagerView()
187 {
188     KConfigGroup pmviewConfig(ICore::self()->activeSession()->config(), sessionConfigGroup);
189     pmviewConfig.writeEntry(splitterStateConfigKey, m_ui->splitter->saveState());
190     pmviewConfig.sync();
191 
192     delete m_ui;
193 }
194 
selectedItems() const195 QList<KDevelop::ProjectBaseItem*> ProjectManagerView::selectedItems() const
196 {
197     QList<KDevelop::ProjectBaseItem*> items;
198     const auto selectedIndexes = m_ui->projectTreeView->selectionModel()->selectedIndexes();
199     for (const QModelIndex& idx : selectedIndexes) {
200         KDevelop::ProjectBaseItem* item = ICore::self()->projectController()->projectModel()->itemFromIndex(indexFromView(idx));
201         if( item )
202             items << item;
203         else
204             qCDebug(PLUGIN_PROJECTMANAGERVIEW) << "adding an unknown item";
205     }
206     return items;
207 }
208 
selectItems(const QList<ProjectBaseItem * > & items)209 void ProjectManagerView::selectItems(const QList< ProjectBaseItem* >& items)
210 {
211     QItemSelection selection;
212     selection.reserve(items.size());
213     for (ProjectBaseItem *item : items) {
214         QModelIndex indx = indexToView(item->index());
215         selection.append(QItemSelectionRange(indx, indx));
216         m_ui->projectTreeView->setCurrentIndex(indx);
217     }
218     m_ui->projectTreeView->selectionModel()->select(selection, QItemSelectionModel::ClearAndSelect);
219 }
220 
expandItem(ProjectBaseItem * item)221 void ProjectManagerView::expandItem(ProjectBaseItem* item)
222 {
223     m_ui->projectTreeView->expand( indexToView(item->index()));
224 }
225 
toggleHideTargets(bool visible)226 void ProjectManagerView::toggleHideTargets(bool visible)
227 {
228     KConfigGroup pmviewConfig(ICore::self()->activeSession()->config(), sessionConfigGroup);
229     pmviewConfig.writeEntry<bool>(targetsVisibleConfigKey, visible);
230     m_modelFilter->showTargets(visible);
231 }
232 
toggleSyncCurrentDocument(bool sync)233 void ProjectManagerView::toggleSyncCurrentDocument(bool sync)
234 {
235     KConfigGroup pmviewConfig(ICore::self()->activeSession()->config(), sessionConfigGroup);
236     pmviewConfig.writeEntry<bool>(syncCurrentDocumentKey, sync);
237     if (sync) {
238         locateCurrentDocument();
239     }
240 }
241 
locateCurrentDocument()242 void ProjectManagerView::locateCurrentDocument()
243 {
244     ICore::self()->uiController()->raiseToolView(this);
245 
246     KDevelop::IDocument *doc = ICore::self()->documentController()->activeDocument();
247 
248     if (!doc) {
249         // in theory we should never get a null pointer as the action is only enabled
250         // when there is an active document.
251         // but: in practice it can happen that you close the last document and press
252         // the shortcut to locate a doc or vice versa... so just do the failsafe thing here...
253         return;
254     }
255 
256     QModelIndex bestMatch;
257     const auto projects = ICore::self()->projectController()->projects();
258     for (IProject* proj : projects) {
259         const auto files = proj->filesForPath(IndexedString(doc->url()));
260         for (KDevelop::ProjectFileItem* item : files) {
261             QModelIndex index = indexToView(item->index());
262             if (index.isValid()) {
263                 if (!bestMatch.isValid()) {
264                     bestMatch = index;
265                 } else if (KDevelop::ProjectBaseItem* parent = item->parent()) {
266                     // prefer files in their real folders over the 'copies' in the target folders
267                     if (!parent->target()) {
268                         bestMatch = index;
269                         break;
270                     }
271                 }
272             }
273         }
274     }
275     if (bestMatch.isValid()) {
276         m_ui->projectTreeView->clearSelection();
277         m_ui->projectTreeView->setCurrentIndex(bestMatch);
278         m_ui->projectTreeView->expand(bestMatch);
279         m_ui->projectTreeView->scrollTo(bestMatch);
280     }
281 }
282 
open(const Path & path)283 void ProjectManagerView::open( const Path& path )
284 {
285     IOpenWith::openFiles(QList<QUrl>() << path.toUrl());
286 }
287 
indexFromView(const QModelIndex & index) const288 QModelIndex ProjectManagerView::indexFromView(const QModelIndex& index) const
289 {
290     return m_modelFilter->mapToSource( m_overlayProxy->mapToSource(index) );
291 }
292 
indexToView(const QModelIndex & index) const293 QModelIndex ProjectManagerView::indexToView(const QModelIndex& index) const
294 {
295     return m_overlayProxy->mapFromSource( m_modelFilter->mapFromSource(index) );
296 }
297 
298