File indexing completed on 2024-04-28 05:49:09

0001 /*  This file is part of the Kate project.
0002  *
0003  *  SPDX-FileCopyrightText: 2010 Christoph Cullmann <cullmann@kde.org>
0004  *
0005  *  SPDX-License-Identifier: LGPL-2.0-or-later
0006  */
0007 
0008 #include "kateprojectpluginview.h"
0009 #include "branchcheckoutdialog.h"
0010 #include "currentgitbranchbutton.h"
0011 #include "fileutil.h"
0012 #include "gitprocess.h"
0013 #include "gitwidget.h"
0014 #include "kateproject.h"
0015 #include "kateprojectinfoview.h"
0016 #include "kateprojectinfoviewindex.h"
0017 #include "kateprojectplugin.h"
0018 #include "kateprojectview.h"
0019 #include "ktexteditor_utils.h"
0020 
0021 #include <KTextEditor/Command>
0022 #include <ktexteditor/application.h>
0023 #include <ktexteditor/document.h>
0024 #include <ktexteditor/editor.h>
0025 #include <ktexteditor/view.h>
0026 
0027 #include <KActionCollection>
0028 #include <KActionMenu>
0029 #include <KLocalizedString>
0030 #include <KPluginFactory>
0031 #include <KStandardAction>
0032 #include <KStringHandler>
0033 #include <KXMLGUIFactory>
0034 #include <KXmlGuiWindow>
0035 
0036 #include <QAction>
0037 #include <QFileDialog>
0038 #include <QHBoxLayout>
0039 #include <QKeyEvent>
0040 
0041 #define PROJECTCLOSEICON "window-close"
0042 
0043 K_PLUGIN_FACTORY_WITH_JSON(KateProjectPluginFactory, "kateprojectplugin.json", registerPlugin<KateProjectPlugin>();)
0044 
0045 KateProjectPluginView::KateProjectPluginView(KateProjectPlugin *plugin, KTextEditor::MainWindow *mainWin)
0046     : QObject(mainWin)
0047     , m_plugin(plugin)
0048     , m_mainWindow(mainWin)
0049     , m_toolView(nullptr)
0050     , m_toolInfoView(nullptr)
0051     , m_toolMultiView(nullptr)
0052     , m_lookupAction(nullptr)
0053     , m_gotoSymbolAction(nullptr)
0054     , m_gotoSymbolActionAppMenu(nullptr)
0055 {
0056     KXMLGUIClient::setComponentName(QStringLiteral("kateproject"), i18n("Project Manager"));
0057     setXMLFile(QStringLiteral("ui.rc"));
0058 
0059     /**
0060      * create toolviews
0061      */
0062     m_toolView = m_mainWindow->createToolView(m_plugin,
0063                                               QStringLiteral("kateproject"),
0064                                               KTextEditor::MainWindow::Left,
0065                                               QIcon::fromTheme(QStringLiteral("project-open")),
0066                                               i18n("Projects"));
0067     m_gitToolView.reset(m_mainWindow->createToolView(m_plugin, QStringLiteral("kateprojectgit"), KTextEditor::MainWindow::Left, gitIcon(), i18n("Git")));
0068     m_toolInfoView = m_mainWindow->createToolView(m_plugin,
0069                                                   QStringLiteral("kateprojectinfo"),
0070                                                   KTextEditor::MainWindow::Bottom,
0071                                                   QIcon::fromTheme(QStringLiteral("view-choose")),
0072                                                   i18n("Project"));
0073 
0074     /**
0075      * create the combo + buttons for the toolViews + stacked widgets
0076      */
0077     m_projectsCombo = new QComboBox(m_toolView);
0078     m_projectsCombo->setToolTip(i18n("Open projects list"));
0079     m_projectsCombo->setFrame(false);
0080     m_reloadButton = new QToolButton(m_toolView);
0081     m_reloadButton->setAutoRaise(true);
0082     m_reloadButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
0083     m_reloadButton->setToolTip(i18n("Reload project"));
0084     m_closeProjectButton = new QToolButton(m_toolView);
0085     m_closeProjectButton->setAutoRaise(true);
0086     m_closeProjectButton->setToolTip(i18n("Close project"));
0087     m_closeProjectButton->setIcon(QIcon::fromTheme(QStringLiteral(PROJECTCLOSEICON)));
0088     QHBoxLayout *layout = new QHBoxLayout();
0089     layout->setSpacing(0);
0090     layout->addWidget(m_projectsCombo);
0091     layout->addWidget(m_reloadButton);
0092     layout->addWidget(m_closeProjectButton);
0093     m_toolView->layout()->addItem(layout);
0094     m_toolView->layout()->setSpacing(0);
0095 
0096     auto separator = new QFrame(m_toolView);
0097     separator->setFrameShape(QFrame::HLine);
0098     separator->setEnabled(false);
0099     m_toolView->layout()->addWidget(separator);
0100 
0101     m_projectsComboGit = new QComboBox(m_gitToolView.get());
0102     m_projectsComboGit->setFrame(false);
0103     m_gitStatusRefreshButton = new QToolButton(m_gitToolView.get());
0104     m_gitStatusRefreshButton->setAutoRaise(true);
0105     m_gitStatusRefreshButton->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
0106     m_gitStatusRefreshButton->setToolTip(i18n("Refresh git status"));
0107     layout = new QHBoxLayout();
0108     layout->setSpacing(0);
0109     layout->addWidget(m_projectsComboGit);
0110     layout->addWidget(m_gitStatusRefreshButton);
0111     m_gitToolView->layout()->addItem(layout);
0112     m_gitToolView->layout()->setSpacing(0);
0113 
0114     m_stackedProjectViews = new QStackedWidget(m_toolView);
0115     m_stackedProjectInfoViews = new QStackedWidget(m_toolInfoView);
0116     m_stackedGitViews = new QStackedWidget(m_gitToolView.get());
0117 
0118     connect(m_projectsCombo,
0119             static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged),
0120             m_projectsComboGit,
0121             static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged));
0122     connect(m_projectsComboGit, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, [this](int index) {
0123         m_projectsCombo->setCurrentIndex(index);
0124     });
0125     connect(m_projectsCombo, static_cast<void (QComboBox::*)(int)>(&QComboBox::currentIndexChanged), this, &KateProjectPluginView::slotCurrentChanged);
0126     connect(m_reloadButton, &QToolButton::clicked, this, &KateProjectPluginView::slotProjectReload);
0127 
0128     connect(m_closeProjectButton, &QToolButton::clicked, this, &KateProjectPluginView::slotCloseProject);
0129     connect(m_plugin, &KateProjectPlugin::pluginViewProjectClosing, this, &KateProjectPluginView::slotHandleProjectClosing);
0130 
0131     connect(m_gitStatusRefreshButton, &QToolButton::clicked, this, [this] {
0132         if (auto widget = m_stackedGitViews->currentWidget()) {
0133             qobject_cast<GitWidget *>(widget)->updateStatus();
0134         }
0135     });
0136 
0137     connect(&m_plugin->fileWatcher(), &QFileSystemWatcher::fileChanged, this, [this](const QString &path) {
0138         if (m_gitChangedWatcherFile == path) {
0139             slotUpdateStatus(true);
0140         }
0141     });
0142 
0143     /**
0144      * create views for all already existing projects
0145      * will create toolviews on demand!
0146      */
0147     const auto projectList = m_plugin->projects();
0148     for (KateProject *project : projectList) {
0149         viewForProject(project);
0150     }
0151     // If the list of projects is empty we do not want to restore the tool view from the last session, BUG: 432296
0152     if (projectList.isEmpty()) {
0153         // We have to call this in the next iteration of the event loop, after the session is restored
0154         QTimer::singleShot(0, this, [this]() {
0155             m_mainWindow->hideToolView(m_toolView);
0156             m_mainWindow->hideToolView(m_gitToolView.get());
0157             m_mainWindow->hideToolView(m_toolInfoView);
0158             if (m_toolMultiView) {
0159                 m_mainWindow->hideToolView(m_toolMultiView);
0160             }
0161         });
0162     }
0163 
0164     /**
0165      * connect to important signals, e.g. for auto project view creation
0166      */
0167     connect(m_plugin, &KateProjectPlugin::projectCreated, this, &KateProjectPluginView::viewForProject);
0168     connect(m_plugin, &KateProjectPlugin::configUpdated, this, &KateProjectPluginView::slotConfigUpdated);
0169     connect(m_mainWindow, &KTextEditor::MainWindow::viewChanged, this, &KateProjectPluginView::slotViewChanged);
0170     connect(m_mainWindow, &KTextEditor::MainWindow::viewCreated, this, &KateProjectPluginView::slotViewCreated);
0171 
0172     /**
0173      * connect for all already existing views
0174      */
0175     const auto views = m_mainWindow->views();
0176     for (KTextEditor::View *view : views) {
0177         slotViewCreated(view);
0178     }
0179 
0180     /**
0181      * back + forward
0182      */
0183     auto a = actionCollection()->addAction(QStringLiteral("projects_open_project"), this, SLOT(openDirectoryOrProject()));
0184     a->setText(i18n("Open Folder..."));
0185     a->setIcon(QIcon::fromTheme(QStringLiteral("document-open-folder")));
0186     actionCollection()->setDefaultShortcut(a, QKeySequence(QKeySequence(QStringLiteral("Ctrl+T, O"), QKeySequence::PortableText)));
0187 
0188     m_projectTodosAction = a = actionCollection()->addAction(QStringLiteral("projects_todos"), this, SLOT(showProjectTodos()));
0189     a->setText(i18n("Project TODOs"));
0190     a->setIcon(QIcon::fromTheme(QStringLiteral("korg-todo")));
0191 
0192     m_projectPrevAction = a = actionCollection()->addAction(QStringLiteral("projects_prev_project"), this, SLOT(slotProjectPrev()));
0193     a->setText(i18n("Activate Previous Project"));
0194     a->setIcon(QIcon::fromTheme(QStringLiteral("arrow-left")));
0195     actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Left));
0196 
0197     m_projectNextAction = a = actionCollection()->addAction(QStringLiteral("projects_next_project"), this, SLOT(slotProjectNext()));
0198     a->setText(i18n("Activate Next Project"));
0199     a->setIcon(QIcon::fromTheme(QStringLiteral("arrow-right")));
0200     actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::CTRL | Qt::ALT | Qt::Key_Right));
0201 
0202     m_projectGotoIndexAction = a = actionCollection()->addAction(QStringLiteral("projects_goto_index"), this, SLOT(slotProjectIndex()));
0203     a->setText(i18n("Lookup"));
0204     actionCollection()->setDefaultShortcut(a, QKeySequence(Qt::ALT | Qt::Key_1));
0205 
0206     m_projectCloseAction = a = actionCollection()->addAction(QStringLiteral("projects_close"), this, SLOT(slotCloseProject()));
0207     a->setText(i18n("Close Project"));
0208     a->setIcon(QIcon::fromTheme(QStringLiteral(PROJECTCLOSEICON)));
0209 
0210     m_projectCloseAllAction = a = actionCollection()->addAction(QStringLiteral("projects_close_all"), this, SLOT(slotCloseAllProjects()));
0211     a->setText(i18n("Close All Projects"));
0212     a->setIcon(QIcon::fromTheme(QStringLiteral(PROJECTCLOSEICON)));
0213 
0214     m_projectCloseWithoutDocumentsAction = a =
0215         actionCollection()->addAction(QStringLiteral("projects_close_without_open_documents"), this, SLOT(slotCloseAllProjectsWithoutDocuments()));
0216     a->setText(i18n("Close Orphaned Projects"));
0217     a->setIcon(QIcon::fromTheme(QStringLiteral(PROJECTCLOSEICON)));
0218 
0219     m_projectReloadAction = a = actionCollection()->addAction(QStringLiteral("project_reload"), this, SLOT(slotProjectReload()));
0220     a->setText(i18n("Reload Project"));
0221     a->setIcon(QIcon::fromTheme(QStringLiteral("view-refresh")));
0222 
0223     m_gotoSymbolActionAppMenu = a = actionCollection()->addAction(KStandardAction::Goto, QStringLiteral("projects_goto_symbol"), this, SLOT(slotGotoSymbol()));
0224 
0225     auto chckbrAct = actionCollection()->addAction(QStringLiteral("checkout_branch"), this, [this] {
0226         BranchCheckoutDialog bd(mainWindow()->window(), projectBaseDir());
0227         bd.openDialog();
0228     });
0229     chckbrAct->setIcon(QIcon::fromTheme(QStringLiteral("vcs-branch")));
0230     chckbrAct->setText(i18n("Checkout Git Branch"));
0231 
0232     // popup menu
0233     auto popup = new KActionMenu(i18n("Project"), this);
0234     actionCollection()->addAction(QStringLiteral("popup_project"), popup);
0235 
0236     m_lookupAction = popup->menu()->addAction(i18n("Lookup: %1", QString()), this, &KateProjectPluginView::slotProjectIndex);
0237     m_gotoSymbolAction = popup->menu()->addAction(i18n("Goto: %1", QString()), this, &KateProjectPluginView::slotGotoSymbol);
0238 
0239     connect(popup->menu(), &QMenu::aboutToShow, this, &KateProjectPluginView::slotContextMenuAboutToShow);
0240 
0241     connect(m_mainWindow, &KTextEditor::MainWindow::unhandledShortcutOverride, this, &KateProjectPluginView::handleEsc);
0242 
0243     // update git status on toolview visibility change
0244     connect(m_gitToolView.get(), SIGNAL(toolVisibleChanged(bool)), this, SLOT(slotUpdateStatus(bool)));
0245 
0246     /**
0247      * add us to gui
0248      */
0249     m_mainWindow->guiFactory()->addClient(this);
0250 
0251     /**
0252      * align to current config
0253      */
0254     slotConfigUpdated();
0255 
0256     /**
0257      * trigger once view change, to highlight right document
0258      */
0259     slotViewChanged();
0260 
0261     /**
0262      * ensure proper action update, to enable/disable stuff
0263      */
0264     connect(this, &KateProjectPluginView::projectMapChanged, this, &KateProjectPluginView::updateActions);
0265     updateActions();
0266 }
0267 
0268 KateProjectPluginView::~KateProjectPluginView()
0269 {
0270     /**
0271      * cleanup for all views
0272      */
0273     for (QObject *view : qAsConst(m_textViews)) {
0274         KTextEditor::View *v = qobject_cast<KTextEditor::View *>(view);
0275         if (v) {
0276             v->unregisterCompletionModel(m_plugin->completion());
0277         }
0278     }
0279 
0280     /**
0281      * cu toolviews
0282      */
0283     delete m_toolView;
0284     m_toolView = nullptr;
0285     delete m_toolInfoView;
0286     m_toolInfoView = nullptr;
0287     delete m_toolMultiView;
0288     m_toolMultiView = nullptr;
0289 
0290     /**
0291      * cu gui client
0292      */
0293     m_mainWindow->guiFactory()->removeClient(this);
0294 
0295     // Don't watch what nobody use, the old project...
0296     if (!m_gitChangedWatcherFile.isEmpty()) {
0297         m_plugin->fileWatcher().removePath(m_gitChangedWatcherFile);
0298     }
0299 }
0300 
0301 void KateProjectPluginView::slotConfigUpdated()
0302 {
0303     if (!m_plugin->multiProjectGoto()) {
0304         delete m_toolMultiView;
0305         m_toolMultiView = nullptr;
0306     } else if (!m_toolMultiView) {
0307         m_toolMultiView = m_mainWindow->createToolView(m_plugin,
0308                                                        QStringLiteral("kateprojectmulti"),
0309                                                        KTextEditor::MainWindow::Bottom,
0310                                                        QIcon::fromTheme(QStringLiteral("view-choose")),
0311                                                        i18n("Projects Index"));
0312         auto gotoindex = new KateProjectInfoViewIndex(this, nullptr, m_toolMultiView);
0313         m_toolMultiView->layout()->addWidget(gotoindex);
0314     }
0315 
0316     // update action state
0317     updateActions();
0318 }
0319 
0320 QPair<KateProjectView *, KateProjectInfoView *> KateProjectPluginView::viewForProject(KateProject *project)
0321 {
0322     /**
0323      * needs valid project
0324      */
0325     Q_ASSERT(project);
0326 
0327     /**
0328      * existing view?
0329      */
0330     if (m_project2View.contains(project)) {
0331         return m_project2View.value(project);
0332     }
0333 
0334     /**
0335      * create new views
0336      */
0337     KateProjectView *view = new KateProjectView(this, project);
0338     KateProjectInfoView *infoView = new KateProjectInfoView(this, project);
0339     GitWidget *gitView = new GitWidget(project, m_mainWindow, this);
0340 
0341     /**
0342      * attach to toolboxes
0343      * first the views, then the combo, that triggers signals
0344      */
0345     m_stackedProjectViews->addWidget(view);
0346     m_stackedProjectInfoViews->addWidget(infoView);
0347     m_stackedGitViews->addWidget(gitView);
0348     m_projectsCombo->addItem(QIcon::fromTheme(QStringLiteral("project-open")), project->name(), project->fileName());
0349     m_projectsComboGit->addItem(QIcon::fromTheme(QStringLiteral("project-open")), project->name(), project->fileName());
0350     connect(project, &KateProject::projectMapChanged, this, [this] {
0351         auto widget = m_stackedProjectViews->currentWidget();
0352         auto project = static_cast<KateProjectView *>(widget)->project();
0353         if (widget && project == sender()) {
0354             Q_EMIT projectMapChanged();
0355 
0356             int index = m_projectsCombo->findData(project->fileName());
0357             Q_ASSERT(index == m_projectsCombo->currentIndex());
0358             if (index != -1) {
0359                 m_projectsCombo->setItemText(index, project->name());
0360             }
0361         }
0362     });
0363 
0364     /*
0365      * inform onward
0366      */
0367     Q_EMIT pluginProjectAdded(project->baseDir(), project->name());
0368 
0369     /**
0370      * remember and return it
0371      */
0372     return (m_project2View[project] = QPair<KateProjectView *, KateProjectInfoView *>(view, infoView));
0373 }
0374 
0375 QString KateProjectPluginView::projectFileName() const
0376 {
0377     QWidget *active = m_stackedProjectViews->currentWidget();
0378     if (!active) {
0379         return QString();
0380     }
0381 
0382     return static_cast<KateProjectView *>(active)->project()->fileName();
0383 }
0384 
0385 QString KateProjectPluginView::projectName() const
0386 {
0387     QWidget *active = m_stackedProjectViews->currentWidget();
0388     if (!active) {
0389         return QString();
0390     }
0391 
0392     return static_cast<KateProjectView *>(active)->project()->name();
0393 }
0394 
0395 QString KateProjectPluginView::projectBaseDir() const
0396 {
0397     QWidget *active = m_stackedProjectViews->currentWidget();
0398     if (!active) {
0399         return QString();
0400     }
0401 
0402     return static_cast<KateProjectView *>(active)->project()->baseDir();
0403 }
0404 
0405 QVariantMap KateProjectPluginView::projectMap() const
0406 {
0407     QWidget *active = m_stackedProjectViews->currentWidget();
0408     if (!active) {
0409         return QVariantMap();
0410     }
0411 
0412     return static_cast<KateProjectView *>(active)->project()->projectMap();
0413 }
0414 
0415 QStringList KateProjectPluginView::projectFiles() const
0416 {
0417     KateProjectView *active = static_cast<KateProjectView *>(m_stackedProjectViews->currentWidget());
0418     if (!active) {
0419         return QStringList();
0420     }
0421 
0422     return active->project()->files();
0423 }
0424 
0425 QString KateProjectPluginView::allProjectsCommonBaseDir() const
0426 {
0427     auto projects = m_plugin->projects();
0428 
0429     if (projects.empty()) {
0430         return QString();
0431     }
0432 
0433     if (projects.size() == 1) {
0434         return projects[0]->baseDir();
0435     }
0436 
0437     QString commonParent1 = FileUtil::commonParent(projects[0]->baseDir(), projects[1]->baseDir());
0438 
0439     for (int i = 2; i < projects.size(); i++) {
0440         commonParent1 = FileUtil::commonParent(commonParent1, projects[i]->baseDir());
0441     }
0442 
0443     return commonParent1;
0444 }
0445 
0446 QStringList KateProjectPluginView::allProjectsFiles() const
0447 {
0448     QStringList fileList;
0449 
0450     const auto projectList = m_plugin->projects();
0451     for (auto project : projectList) {
0452         fileList.append(project->files());
0453     }
0454 
0455     return fileList;
0456 }
0457 
0458 QMap<QString, QString> KateProjectPluginView::allProjects() const
0459 {
0460     QMap<QString, QString> projectMap;
0461 
0462     const auto projectList = m_plugin->projects();
0463     for (auto project : projectList) {
0464         projectMap[project->baseDir()] = project->name();
0465     }
0466     return projectMap;
0467 }
0468 
0469 void KateProjectPluginView::slotViewChanged()
0470 {
0471     /**
0472      * get active view
0473      */
0474     KTextEditor::View *activeView = m_mainWindow->activeView();
0475 
0476     /**
0477      * update pointer, maybe disconnect before
0478      */
0479     if (m_activeTextEditorView) {
0480         // but only url changed
0481         disconnect(m_activeTextEditorView->document(), &KTextEditor::Document::documentUrlChanged, this, &KateProjectPluginView::slotDocumentUrlChanged);
0482     }
0483     m_activeTextEditorView = activeView;
0484 
0485     /**
0486      * no current active view, return
0487      */
0488     if (!m_activeTextEditorView) {
0489         return;
0490     }
0491 
0492     /**
0493      * connect to url changed, for auto load
0494      */
0495     connect(m_activeTextEditorView->document(), &KTextEditor::Document::documentUrlChanged, this, &KateProjectPluginView::slotDocumentUrlChanged);
0496 
0497     /**
0498      * Watch any document, as long as we live, if it's saved
0499      */
0500     connect(m_activeTextEditorView->document(),
0501             &KTextEditor::Document::documentSavedOrUploaded,
0502             this,
0503             &KateProjectPluginView::slotDocumentSaved,
0504             Qt::UniqueConnection);
0505 
0506     /**
0507      * trigger slot once
0508      */
0509     slotDocumentUrlChanged(m_activeTextEditorView->document());
0510 }
0511 
0512 void KateProjectPluginView::slotDocumentSaved()
0513 {
0514     slotUpdateStatus(true);
0515 }
0516 
0517 void KateProjectPluginView::slotCurrentChanged(int index)
0518 {
0519     // trigger change of stacked widgets
0520     m_stackedProjectViews->setCurrentIndex(index);
0521     m_stackedProjectInfoViews->setCurrentIndex(index);
0522     m_stackedGitViews->setCurrentIndex(index);
0523 
0524     {
0525         const QSignalBlocker blocker(m_projectsComboGit);
0526         m_projectsComboGit->setCurrentIndex(index);
0527     }
0528 
0529     // update focus proxy + open currently selected document
0530     if (QWidget *current = m_stackedProjectViews->currentWidget()) {
0531         m_stackedProjectViews->setFocusProxy(current);
0532         static_cast<KateProjectView *>(current)->openSelectedDocument();
0533     }
0534 
0535     // update focus proxy
0536     if (QWidget *current = m_stackedProjectInfoViews->currentWidget()) {
0537         m_stackedProjectInfoViews->setFocusProxy(current);
0538     }
0539 
0540     // update git focus proxy + update status
0541     if (QWidget *current = m_stackedGitViews->currentWidget()) {
0542         m_stackedGitViews->setFocusProxy(current);
0543     }
0544 
0545     // Don't watch what nobody use, the old project...
0546     if (!m_gitChangedWatcherFile.isEmpty()) {
0547         m_plugin->fileWatcher().removePath(m_gitChangedWatcherFile);
0548         m_gitChangedWatcherFile.clear();
0549     }
0550 
0551     // ...and start watching the new one
0552     slotUpdateStatus(true);
0553 
0554     // project file name might have changed
0555     Q_EMIT projectFileNameChanged();
0556     Q_EMIT projectMapChanged();
0557 }
0558 
0559 void KateProjectPluginView::slotDocumentUrlChanged(KTextEditor::Document *document)
0560 {
0561     /**
0562      * abort if empty url or no local path
0563      */
0564     if (document->url().isEmpty() || !document->url().isLocalFile()) {
0565         return;
0566     }
0567 
0568     /**
0569      * search matching project
0570      */
0571     KateProject *project = m_plugin->projectForUrl(document->url());
0572     if (!project) {
0573         return;
0574     }
0575 
0576     /**
0577      * select the file FIRST
0578      */
0579     m_project2View.value(project).first->selectFile(document->url().toLocalFile());
0580 
0581     /**
0582      * get active project view and switch it, if it is for a different project
0583      * do this AFTER file selection
0584      */
0585     KateProjectView *active = static_cast<KateProjectView *>(m_stackedProjectViews->currentWidget());
0586     if (active != m_project2View.value(project).first) {
0587         int index = m_projectsCombo->findData(project->fileName());
0588         if (index >= 0) {
0589             m_projectsCombo->setCurrentIndex(index);
0590         }
0591     }
0592 }
0593 
0594 void KateProjectPluginView::switchToProject(const QDir &dir)
0595 {
0596     /**
0597      * search matching project
0598      */
0599     KateProject *project = m_plugin->projectForDir(dir);
0600     if (!project) {
0601         return;
0602     }
0603 
0604     /**
0605      * get active project view and switch it, if it is for a different project
0606      * do this AFTER file selection
0607      */
0608     KateProjectView *active = static_cast<KateProjectView *>(m_stackedProjectViews->currentWidget());
0609     if (active != m_project2View.value(project).first) {
0610         int index = m_projectsCombo->findData(project->fileName());
0611         if (index >= 0) {
0612             m_projectsCombo->setCurrentIndex(index);
0613         }
0614     }
0615 }
0616 
0617 void KateProjectPluginView::runCmdInTerminal(const QString &workingDir, const QString &cmd)
0618 {
0619     m_mainWindow->showToolView(m_toolInfoView);
0620     auto widget = static_cast<KateProjectInfoView *>(m_stackedProjectInfoViews->currentWidget());
0621     if (!widget) {
0622         return;
0623     }
0624     widget->runCmdInTerminal(workingDir, cmd);
0625 }
0626 
0627 void KateProjectPluginView::slotViewCreated(KTextEditor::View *view)
0628 {
0629     /**
0630      * connect to destroyed
0631      */
0632     connect(view, &KTextEditor::View::destroyed, this, &KateProjectPluginView::slotViewDestroyed);
0633 
0634     /**
0635      * add completion model if possible
0636      */
0637     view->registerCompletionModel(m_plugin->completion());
0638 
0639     /**
0640      * remember for this view we need to cleanup!
0641      */
0642     m_textViews.insert(view);
0643 }
0644 
0645 void KateProjectPluginView::slotViewDestroyed(QObject *view)
0646 {
0647     /**
0648      * remove remembered views for which we need to cleanup on exit!
0649      */
0650     m_textViews.remove(view);
0651 }
0652 
0653 void KateProjectPluginView::slotProjectPrev()
0654 {
0655     if (!m_projectsCombo->count()) {
0656         return;
0657     }
0658 
0659     if (m_projectsCombo->currentIndex() == 0) {
0660         m_projectsCombo->setCurrentIndex(m_projectsCombo->count() - 1);
0661     } else {
0662         m_projectsCombo->setCurrentIndex(m_projectsCombo->currentIndex() - 1);
0663     }
0664 }
0665 
0666 void KateProjectPluginView::slotProjectNext()
0667 {
0668     if (!m_projectsCombo->count()) {
0669         return;
0670     }
0671 
0672     if (m_projectsCombo->currentIndex() + 1 == m_projectsCombo->count()) {
0673         m_projectsCombo->setCurrentIndex(0);
0674     } else {
0675         m_projectsCombo->setCurrentIndex(m_projectsCombo->currentIndex() + 1);
0676     }
0677 }
0678 
0679 void KateProjectPluginView::slotProjectReload()
0680 {
0681     /**
0682      * force reload if any active project
0683      */
0684     if (QWidget *current = m_stackedProjectViews->currentWidget()) {
0685         static_cast<KateProjectView *>(current)->project()->reload(true);
0686     }
0687     /**
0688      * Refresh git status
0689      */
0690     if (auto widget = m_stackedGitViews->currentWidget()) {
0691         qobject_cast<GitWidget *>(widget)->updateStatus();
0692     }
0693 }
0694 
0695 void KateProjectPluginView::slotCloseProject()
0696 {
0697     if (QWidget *current = m_stackedProjectViews->currentWidget()) {
0698         m_plugin->closeProject(static_cast<KateProjectView *>(current)->project());
0699     }
0700 }
0701 
0702 void KateProjectPluginView::slotCloseAllProjects()
0703 {
0704     // we must close project after project
0705     // project closing might activate a different one and then would load the files of that project again
0706     const auto copiedProjects = m_plugin->projects();
0707     for (auto project : copiedProjects) {
0708         m_plugin->closeProject(project);
0709     }
0710 }
0711 
0712 void KateProjectPluginView::slotCloseAllProjectsWithoutDocuments()
0713 {
0714     // we must close project after project
0715     // project closing might activate a different one and then would load the files of that project again
0716     const auto copiedProjects = m_plugin->projects();
0717     for (auto project : copiedProjects) {
0718         if (!m_plugin->projectHasOpenDocuments(project)) {
0719             m_plugin->closeProject(project);
0720         }
0721     }
0722 }
0723 
0724 void KateProjectPluginView::slotHandleProjectClosing(KateProject *project)
0725 {
0726     const int index = m_plugin->projects().indexOf(project);
0727     m_project2View.erase(m_project2View.find(project));
0728 
0729     QWidget *stackedProjectViewsWidget = m_stackedProjectViews->widget(index);
0730     m_stackedProjectViews->removeWidget(stackedProjectViewsWidget);
0731     delete stackedProjectViewsWidget;
0732 
0733     QWidget *stackedProjectInfoViewsWidget = m_stackedProjectInfoViews->widget(index);
0734     m_stackedProjectInfoViews->removeWidget(stackedProjectInfoViewsWidget);
0735     delete stackedProjectInfoViewsWidget;
0736 
0737     QWidget *stackedgitViewsWidget = m_stackedGitViews->widget(index);
0738     m_stackedGitViews->removeWidget(stackedgitViewsWidget);
0739     delete stackedgitViewsWidget;
0740 
0741     m_projectsCombo->removeItem(index);
0742     m_projectsComboGit->removeItem(index);
0743 
0744     // Stop watching what no one is interesting anymore
0745     if (!m_gitChangedWatcherFile.isEmpty()) {
0746         m_plugin->fileWatcher().removePath(m_gitChangedWatcherFile);
0747         m_gitChangedWatcherFile.clear();
0748     }
0749 
0750     // inform onward
0751     Q_EMIT pluginProjectRemoved(project->baseDir(), project->name());
0752 
0753     // update actions, e.g. the close all stuff needs this
0754     updateActions();
0755 }
0756 
0757 QString KateProjectPluginView::currentWord() const
0758 {
0759     KTextEditor::View *kv = m_activeTextEditorView;
0760     if (!kv) {
0761         return QString();
0762     }
0763 
0764     if (kv->selection() && kv->selectionRange().onSingleLine()) {
0765         return kv->selectionText();
0766     }
0767 
0768     return kv->document()->wordAt(kv->cursorPosition());
0769 }
0770 
0771 void KateProjectPluginView::slotProjectIndex()
0772 {
0773     const QString word = currentWord();
0774     if (!word.isEmpty()) {
0775         auto tabView = qobject_cast<QTabWidget *>(m_stackedProjectInfoViews->currentWidget());
0776         if (tabView) {
0777             if (auto codeIndex = tabView->findChild<KateProjectInfoViewIndex *>()) {
0778                 tabView->setCurrentWidget(codeIndex);
0779             }
0780         }
0781         m_mainWindow->showToolView(m_toolInfoView);
0782         Q_EMIT projectLookupWord(word);
0783     }
0784 }
0785 
0786 void KateProjectPluginView::slotGotoSymbol()
0787 {
0788     if (!m_toolMultiView) {
0789         return;
0790     }
0791 
0792     const QString word = currentWord();
0793     if (!word.isEmpty()) {
0794         int results = 0;
0795         Q_EMIT gotoSymbol(word, results);
0796         if (results > 1) {
0797             m_mainWindow->showToolView(m_toolMultiView);
0798         }
0799     }
0800 }
0801 
0802 void KateProjectPluginView::slotContextMenuAboutToShow()
0803 {
0804     const QString word = currentWord();
0805     if (word.isEmpty()) {
0806         return;
0807     }
0808 
0809     const QString squeezed = KStringHandler::csqueeze(word, 30);
0810     m_lookupAction->setText(i18n("Lookup: %1", squeezed));
0811     m_gotoSymbolAction->setText(i18n("Goto: %1", squeezed));
0812 }
0813 
0814 void KateProjectPluginView::handleEsc(QEvent *e)
0815 {
0816     if (!m_mainWindow) {
0817         return;
0818     }
0819 
0820     QKeyEvent *k = static_cast<QKeyEvent *>(e);
0821     if (k->key() == Qt::Key_Escape && k->modifiers() == Qt::NoModifier) {
0822         const auto infoView = qobject_cast<const KateProjectInfoView *>(m_stackedProjectInfoViews->currentWidget());
0823         if (m_toolInfoView->isVisible() && (!infoView || !infoView->ignoreEsc())) {
0824             m_mainWindow->hideToolView(m_toolInfoView);
0825         }
0826     }
0827 }
0828 
0829 void KateProjectPluginView::slotUpdateStatus(bool visible)
0830 {
0831     if (!visible) {
0832         return;
0833     }
0834 
0835     if (auto widget = static_cast<GitWidget *>(m_stackedGitViews->currentWidget())) {
0836         // To support separate-git-dir always use dotGitPath
0837         // We need to add the path every time again because it's always a different file
0838         if (!m_gitChangedWatcherFile.isEmpty()) {
0839             m_plugin->fileWatcher().removePath(m_gitChangedWatcherFile);
0840         }
0841         m_gitChangedWatcherFile = widget->indexPath();
0842         if (!m_gitChangedWatcherFile.isEmpty()) {
0843             m_plugin->fileWatcher().addPath(m_gitChangedWatcherFile);
0844         }
0845         widget->updateStatus();
0846     }
0847 }
0848 
0849 void KateProjectPluginView::openDirectoryOrProject()
0850 {
0851     // get dir or do nothing
0852     QFileDialog::Options opts;
0853     opts.setFlag(QFileDialog::ShowDirsOnly);
0854     opts.setFlag(QFileDialog::ReadOnly);
0855     const QString dir = QFileDialog::getExistingDirectory(m_mainWindow->window(), i18n("Choose a directory"), QDir::currentPath(), opts);
0856     if (dir.isEmpty()) {
0857         return;
0858     }
0859 
0860     openDirectoryOrProject(dir);
0861 }
0862 
0863 void KateProjectPluginView::openDirectoryOrProject(const QDir &dir)
0864 {
0865     // switch to this project if there
0866     if (auto project = m_plugin->projectForDir(dir, true)) {
0867         openProject(project);
0868     }
0869 }
0870 
0871 void KateProjectPluginView::openProject(KateProject *project)
0872 {
0873     // just activate the right plugin in the toolview
0874     slotActivateProject(project);
0875 
0876     // this is a user action, ensure the toolview is visible
0877     mainWindow()->showToolView(m_toolView);
0878 
0879     // add the project to the recently opened items list
0880     if (auto *parentClient = qobject_cast<KXmlGuiWindow *>(m_mainWindow->window())) {
0881         if (auto *openRecentAction = parentClient->action(KStandardAction::name(KStandardAction::StandardAction::OpenRecent))) {
0882             if (auto *recentFilesAction = qobject_cast<KRecentFilesAction *>(openRecentAction)) {
0883                 recentFilesAction->addUrl(QUrl::fromLocalFile(project->baseDir()));
0884             }
0885         }
0886     }
0887 }
0888 
0889 void KateProjectPluginView::showProjectTodos()
0890 {
0891     KTextEditor::Command *pgrep = KTextEditor::Editor::instance()->queryCommand(QStringLiteral("pgrep"));
0892     if (!pgrep) {
0893         return;
0894     }
0895     QString msg;
0896     pgrep->exec(nullptr, QStringLiteral("preg (TODO|FIXME)\\b"), msg);
0897 }
0898 
0899 void KateProjectPluginView::openTerminal(const QString &dirPath, KateProject *project)
0900 {
0901     m_mainWindow->showToolView(m_toolInfoView);
0902 
0903     if (m_project2View.contains(project)) {
0904         m_project2View.find(project)->second->resetTerminal(dirPath);
0905     }
0906 }
0907 
0908 void KateProjectPluginView::updateActions()
0909 {
0910     const bool hasMultipleProjects = m_projectsCombo->count() > 1;
0911     // currently some project active?
0912     const bool projectActive = !projectBaseDir().isEmpty();
0913     m_projectsCombo->setEnabled(projectActive);
0914     m_projectsComboGit->setEnabled(projectActive);
0915     m_reloadButton->setEnabled(projectActive);
0916     m_closeProjectButton->setEnabled(projectActive);
0917     m_gitStatusRefreshButton->setEnabled(projectActive);
0918     m_projectTodosAction->setEnabled(projectActive);
0919     m_projectPrevAction->setEnabled(projectActive && hasMultipleProjects);
0920     m_projectNextAction->setEnabled(projectActive && hasMultipleProjects);
0921     m_projectCloseAction->setEnabled(projectActive);
0922     m_projectCloseAllAction->setEnabled(hasMultipleProjects);
0923     m_projectCloseWithoutDocumentsAction->setEnabled(m_projectsCombo->count() > 0);
0924 
0925     const bool hasIndex = projectActive && m_plugin->getIndexEnabled();
0926     m_lookupAction->setVisible(hasIndex);
0927     m_gotoSymbolAction->setVisible(hasIndex);
0928     m_projectGotoIndexAction->setVisible(hasIndex);
0929     m_gotoSymbolActionAppMenu->setVisible(hasIndex);
0930     actionCollection()->action(QStringLiteral("popup_project"))->setVisible(hasIndex);
0931 }
0932 
0933 void KateProjectPluginView::slotActivateProject(KateProject *project)
0934 {
0935     const int index = m_projectsCombo->findData(project->fileName());
0936     if (index >= 0) {
0937         m_projectsCombo->setCurrentIndex(index);
0938     }
0939 }
0940 
0941 void KateProjectPluginView::updateGitBranchButton(KateProject *project)
0942 {
0943     if (!m_branchBtn) {
0944         m_branchBtn.reset(new CurrentGitBranchButton(mainWindow(), nullptr));
0945         auto a = actionCollection()->action(QStringLiteral("checkout_branch"));
0946         Q_ASSERT(a);
0947         m_branchBtn->setDefaultAction(a);
0948         Utils::insertWidgetInStatusbar(m_branchBtn.get(), mainWindow());
0949     }
0950 
0951     if (!project || project->baseDir() != projectBaseDir()) {
0952         return;
0953     }
0954 
0955     static_cast<CurrentGitBranchButton *>(m_branchBtn.get())->refresh();
0956 }
0957 
0958 #include "kateprojectpluginview.moc"
0959 #include "moc_kateprojectpluginview.cpp"