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