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"