File indexing completed on 2024-04-28 04:38:21

0001 /*
0002     SPDX-FileCopyrightText: 2008 David Nolden <david.nolden.kdevelop@art-master.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "browsemanager.h"
0008 
0009 #include <QApplication>
0010 #include <QMouseEvent>
0011 #include <QTimer>
0012 #include <QMenuBar>
0013 
0014 #include <interfaces/icore.h>
0015 #include <interfaces/idocumentcontroller.h>
0016 #include "contextbrowserview.h"
0017 #include <interfaces/iuicontroller.h>
0018 #include <interfaces/ilanguagecontroller.h>
0019 #include <interfaces/iprojectcontroller.h>
0020 #include <language/interfaces/ilanguagesupport.h>
0021 #include <language/duchain/duchainutils.h>
0022 #include <language/duchain/duchainlock.h>
0023 #include <language/duchain/duchain.h>
0024 #include <language/duchain/declaration.h>
0025 #include <language/duchain/navigation/abstractnavigationwidget.h>
0026 #include <language/duchain/functiondefinition.h>
0027 #include <language/duchain/forwarddeclaration.h>
0028 
0029 #include "contextbrowser.h"
0030 #include "debug.h"
0031 
0032 #include <KParts/MainWindow>
0033 #include <KTextEditor/Document>
0034 #include <KTextEditor/View>
0035 #include <KTextEditor/CodeCompletionInterface>
0036 
0037 using namespace KDevelop;
0038 using namespace KTextEditor;
0039 
0040 EditorViewWatcher::EditorViewWatcher(QObject* parent)
0041     : QObject(parent)
0042 {
0043     connect(
0044         ICore::self()->documentController(), &IDocumentController::textDocumentCreated, this,
0045         &EditorViewWatcher::documentCreated);
0046     const auto documents = ICore::self()->documentController()->openDocuments();
0047     for (KDevelop::IDocument* document : documents) {
0048         documentCreated(document);
0049     }
0050 }
0051 
0052 void EditorViewWatcher::documentCreated(KDevelop::IDocument* document)
0053 {
0054     KTextEditor::Document* textDocument = document->textDocument();
0055     if (textDocument) {
0056         connect(textDocument, &Document::viewCreated, this, &EditorViewWatcher::viewCreated);
0057         const auto views = textDocument->views();
0058         for (KTextEditor::View* view : views) {
0059             Q_ASSERT(view->parentWidget());
0060             addViewInternal(view);
0061         }
0062     }
0063 }
0064 
0065 void EditorViewWatcher::addViewInternal(KTextEditor::View* view)
0066 {
0067     m_views << view;
0068     viewAdded(view);
0069     connect(view, &View::destroyed, this, &EditorViewWatcher::viewDestroyed);
0070 }
0071 
0072 void EditorViewWatcher::viewAdded(KTextEditor::View*)
0073 {
0074 }
0075 
0076 void EditorViewWatcher::viewDestroyed(QObject* view)
0077 {
0078     m_views.removeAll(static_cast<KTextEditor::View*>(view));
0079 }
0080 
0081 void EditorViewWatcher::viewCreated(KTextEditor::Document* /*doc*/, KTextEditor::View* view)
0082 {
0083     Q_ASSERT(view->parentWidget());
0084     addViewInternal(view);
0085 }
0086 
0087 QList<KTextEditor::View*> EditorViewWatcher::allViews()
0088 {
0089     return m_views;
0090 }
0091 
0092 void BrowseManager::eventuallyStartDelayedBrowsing()
0093 {
0094     avoidMenuAltFocus();
0095 
0096     if (m_browsingByKey == Qt::Key_Alt && m_browsingStartedInView)
0097         emit startDelayedBrowsing(m_browsingStartedInView);
0098 }
0099 
0100 BrowseManager::BrowseManager(ContextBrowserPlugin* controller)
0101     : QObject(controller)
0102     , m_plugin(controller)
0103     , m_browsing(false)
0104     , m_browsingByKey(0)
0105     , m_watcher(this)
0106 {
0107     m_delayedBrowsingTimer = new QTimer(this);
0108     m_delayedBrowsingTimer->setSingleShot(true);
0109     m_delayedBrowsingTimer->setInterval(300);
0110 
0111     connect(m_delayedBrowsingTimer, &QTimer::timeout, this, &BrowseManager::eventuallyStartDelayedBrowsing);
0112 
0113     const auto views = m_watcher.allViews();
0114     for (KTextEditor::View* view : views) {
0115         viewAdded(view);
0116     }
0117 }
0118 
0119 KTextEditor::View* viewFromWidget(QWidget* widget)
0120 {
0121     if (!widget)
0122         return nullptr;
0123     auto* view = qobject_cast<KTextEditor::View*>(widget);
0124     if (view)
0125         return view;
0126     else
0127         return viewFromWidget(widget->parentWidget());
0128 }
0129 
0130 BrowseManager::JumpLocation BrowseManager::determineJumpLoc(KTextEditor::Cursor textCursor, const QUrl& viewUrl) const
0131 {
0132     // @todo find out why this is needed, fix the code in kate
0133     if (textCursor.column() > 0) {
0134         textCursor.setColumn(textCursor.column() - 1);
0135     }
0136 
0137     // Step 1: Look for a special language object(Macro, included header, etc.)
0138     const auto languages = ICore::self()->languageController()->languagesForUrl(viewUrl);
0139     for (const auto& language : languages) {
0140         auto jumpTo = language->specialLanguageObjectJumpCursor(viewUrl, textCursor);
0141         if (jumpTo.first.isValid() && jumpTo.second.isValid()) {
0142             return {
0143                        jumpTo.first, jumpTo.second
0144             };
0145         }
0146     }
0147 
0148     // Step 2: Look for a declaration/use
0149     DUChainReadLocker lock;
0150     // Jump to definition by default, unless a definition itself was selected,
0151     // in which case jump to declaration.
0152     // Also, when the definition is in a file for which we don't know the project,
0153     // that usually means it's a generated file and we rather want to open the declaration
0154     // after all. Unless the viewUrl and textCursor points at the declaration already.
0155     if (auto selectedDeclaration = DUChainUtils::itemUnderCursor(viewUrl, textCursor).declaration) {
0156         auto jumpDestination = selectedDeclaration;
0157         if (selectedDeclaration->isDefinition()) {
0158             // A definition was clicked directly - jump to declaration instead.
0159             if (auto declaration = DUChainUtils::declarationForDefinition(selectedDeclaration)) {
0160                 jumpDestination = declaration;
0161             }
0162         } else if (selectedDeclaration == DUChainUtils::declarationForDefinition(selectedDeclaration)) {
0163             // Clicked the declaration - jump to definition
0164             // unless that isn't in a project
0165             if (auto definition = FunctionDefinition::definition(selectedDeclaration)) {
0166                 const auto preferDefinition = [&]() -> bool {
0167                     // when we are jumping from declaration, we definitely want to go to the definition
0168                     if (viewUrl == selectedDeclaration->url().toUrl()
0169                         && selectedDeclaration->rangeInCurrentRevision().contains(textCursor))
0170                     {
0171                         return true;
0172                     }
0173                     // otherwise only jump to the definition when we know the project
0174                     // for the file in which the definition lives
0175                     // this won't be the case for generated files which are usually not interesting
0176                     // like e.g. Qt moc files - there, we want to jump to the header first
0177                     return ICore::self()->projectController()->findProjectForUrl(definition->url().toUrl());
0178                 }();
0179                 if (preferDefinition) {
0180                     jumpDestination = definition;
0181                 }
0182             }
0183         }
0184         return {
0185                    jumpDestination->url().toUrl(), jumpDestination->rangeInCurrentRevision().start()
0186         };
0187     }
0188 
0189     return {};
0190 }
0191 
0192 bool BrowseManager::eventFilter(QObject* watched, QEvent* event)
0193 {
0194     QWidget* widget = qobject_cast<QWidget*>(watched);
0195     Q_ASSERT(widget);
0196 
0197     auto* keyEvent = dynamic_cast<QKeyEvent*>(event);
0198 
0199     const int browseKey = Qt::Key_Control;
0200     const int magicModifier = Qt::Key_Alt;
0201 
0202     KTextEditor::View* view = viewFromWidget(widget);
0203 
0204     //Eventually start key-browsing
0205     if (keyEvent && (keyEvent->key() == browseKey || keyEvent->key() == magicModifier) && !m_browsingByKey &&
0206         keyEvent->type() == QEvent::KeyPress) {
0207         m_delayedBrowsingTimer->start(); // always start the timer, to get consistent behavior regarding the ALT key and the menu activation
0208         m_browsingByKey = keyEvent->key();
0209 
0210         if (!view) {
0211             return false;
0212         }
0213 
0214         if (keyEvent->key() == magicModifier) {
0215             if (qobject_cast<KTextEditor::CodeCompletionInterface*>(view) &&
0216                 qobject_cast<KTextEditor::CodeCompletionInterface*>(view)->isCompletionActive()) {
0217                 //Completion is active.
0218                 avoidMenuAltFocus();
0219                 m_delayedBrowsingTimer->stop();
0220             } else {
0221                 m_browsingStartedInView = view;
0222             }
0223         }
0224     }
0225 
0226     if (keyEvent && m_browsingByKey && m_browsingStartedInView && keyEvent->type() == QEvent::KeyPress) {
0227         if (keyEvent->key() >= Qt::Key_1 && keyEvent->key() <= Qt::Key_9) {
0228             // user wants to trigger an action in the code browser
0229             const int index = keyEvent->key() - Qt::Key_1;
0230             emit invokeAction(index);
0231             emit stopDelayedBrowsing();
0232             return true;
0233         }
0234     }
0235 
0236     if (!view) {
0237         return false;
0238     }
0239 
0240     auto* focusEvent = dynamic_cast<QFocusEvent*>(event);
0241     //Eventually stop key-browsing
0242     if ((keyEvent && m_browsingByKey && ( keyEvent->key() == m_browsingByKey || keyEvent->modifiers() == Qt::ControlModifier ) && keyEvent->type() == QEvent::KeyRelease)
0243         || (focusEvent && focusEvent->lostFocus()) || event->type() == QEvent::WindowDeactivate) {
0244         m_browsingByKey = 0;
0245         emit stopDelayedBrowsing();
0246     }
0247 
0248     auto* mouseEvent = dynamic_cast<QMouseEvent*>(event);
0249 
0250     if (mouseEvent) {
0251         if (mouseEvent->type() == QEvent::MouseButtonPress && mouseEvent->button() == Qt::XButton1) {
0252             m_plugin->historyPrevious();
0253             return true;
0254         }
0255         if (mouseEvent->type() == QEvent::MouseButtonPress && mouseEvent->button() == Qt::XButton2) {
0256             m_plugin->historyNext();
0257             return true;
0258         }
0259     }
0260 
0261     if (!m_browsing && !m_browsingByKey) {
0262         resetChangedCursor();
0263         return false;
0264     }
0265 
0266     if (mouseEvent) {
0267         QPoint coordinatesInView = widget->mapTo(view, mouseEvent->pos());
0268 
0269         KTextEditor::Cursor textCursor = view->coordinatesToCursor(coordinatesInView);
0270         if (textCursor.isValid()) {
0271             JumpLocation jumpTo = determineJumpLoc(textCursor, view->document()->url());
0272             if (jumpTo.isValid()) {
0273                 if (mouseEvent->button() == Qt::LeftButton) {
0274                     if (mouseEvent->type() == QEvent::MouseButtonPress) {
0275                         m_buttonPressPosition = textCursor;
0276 //                         view->setCursorPosition(textCursor);
0277 //                         return false;
0278                     } else if (mouseEvent->type() == QEvent::MouseButtonRelease &&
0279                                textCursor == m_buttonPressPosition) {
0280                         ICore::self()->documentController()->openDocument(jumpTo.url, jumpTo.cursor);
0281 //                         event->accept();
0282 //                         return true;
0283                     }
0284                 } else if (mouseEvent->type() == QEvent::MouseMove) {
0285                     //Make the cursor a "hand"
0286                     setHandCursor(widget);
0287                     return false;
0288                 }
0289             }
0290         }
0291         resetChangedCursor();
0292     }
0293     return false;
0294 }
0295 
0296 void BrowseManager::resetChangedCursor()
0297 {
0298     QMap<QPointer<QWidget>, QCursor> cursors = m_oldCursors;
0299     m_oldCursors.clear();
0300 
0301     for (QMap<QPointer<QWidget>, QCursor>::iterator it = cursors.begin(); it != cursors.end(); ++it)
0302         if (it.key())
0303             it.key()->setCursor(QCursor(Qt::IBeamCursor));
0304 }
0305 
0306 void BrowseManager::setHandCursor(QWidget* widget)
0307 {
0308     if (m_oldCursors.contains(widget))
0309         return; //Nothing to do
0310     m_oldCursors[widget] = widget->cursor();
0311     widget->setCursor(QCursor(Qt::PointingHandCursor));
0312 }
0313 
0314 void BrowseManager::avoidMenuAltFocus()
0315 {
0316     auto mainWindow = ICore::self()->uiController()->activeMainWindow();
0317     if (!mainWindow)
0318         return;
0319 
0320     // send an invalid key event to the main menu bar. The menu bar will
0321     // stop listening when observing another key than ALT between the press
0322     // and the release.
0323     QKeyEvent event1(QEvent::KeyPress, 0, Qt::NoModifier);
0324     QApplication::sendEvent(mainWindow->menuBar(), &event1);
0325     QKeyEvent event2(QEvent::KeyRelease, 0, Qt::NoModifier);
0326     QApplication::sendEvent(mainWindow->menuBar(), &event2);
0327 }
0328 
0329 void BrowseManager::applyEventFilter(QWidget* object, bool install)
0330 {
0331     if (install)
0332         object->installEventFilter(this);
0333     else
0334         object->removeEventFilter(this);
0335 
0336     const auto children = object->children();
0337     for (QObject* child : children) {
0338         if (qobject_cast<QWidget*>(child))
0339             applyEventFilter(qobject_cast<QWidget*>(child), install);
0340     }
0341 }
0342 
0343 void BrowseManager::viewAdded(KTextEditor::View* view)
0344 {
0345     applyEventFilter(view, true);
0346     //We need to listen for cursorPositionChanged, to clear the shift-detector. The problem: Kate listens for the arrow-keys using shortcuts,
0347     //so those keys are not passed to the event-filter
0348 
0349     // can't use new signal/slot syntax here, these signals are only defined in KateView
0350     // TODO: should we really depend on kate internals here?
0351     connect(view, SIGNAL(navigateLeft()), m_plugin, SLOT(navigateLeft()));
0352     connect(view, SIGNAL(navigateRight()), m_plugin, SLOT(navigateRight()));
0353     connect(view, SIGNAL(navigateUp()), m_plugin, SLOT(navigateUp()));
0354     connect(view, SIGNAL(navigateDown()), m_plugin, SLOT(navigateDown()));
0355     connect(view, SIGNAL(navigateAccept()), m_plugin, SLOT(navigateAccept()));
0356     connect(view, SIGNAL(navigateBack()), m_plugin, SLOT(navigateBack()));
0357 }
0358 
0359 void Watcher::viewAdded(KTextEditor::View* view)
0360 {
0361     m_manager->viewAdded(view);
0362 }
0363 
0364 void BrowseManager::setBrowsing(bool enabled)
0365 {
0366     if (enabled == m_browsing)
0367         return;
0368     m_browsing = enabled;
0369 
0370     //This collects all the views
0371     if (enabled) {
0372         qCDebug(PLUGIN_CONTEXTBROWSER) << "Enabled browsing-mode";
0373     } else {
0374         qCDebug(PLUGIN_CONTEXTBROWSER) << "Disabled browsing-mode";
0375         resetChangedCursor();
0376     }
0377 }
0378 
0379 Watcher::Watcher(BrowseManager* manager)
0380     : EditorViewWatcher(manager)
0381     , m_manager(manager)
0382 {
0383     const auto views = allViews();
0384     for (KTextEditor::View* view : views) {
0385         m_manager->applyEventFilter(view, true);
0386     }
0387 }
0388 
0389 #include "moc_browsemanager.cpp"