File indexing completed on 2024-06-16 04:23:14

0001 /*
0002     SPDX-FileCopyrightText: 2007 David Nolden <david.nolden.kdevelop@art-master.de>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-only
0005 */
0006 
0007 #include "abstractnavigationwidget.h"
0008 
0009 #include <QWheelEvent>
0010 #include <QApplication>
0011 #include <QVBoxLayout>
0012 #include <QMetaObject>
0013 #include <QScrollBar>
0014 #include <QTextBrowser>
0015 
0016 #include <KLocalizedString>
0017 
0018 #include "../duchainlock.h"
0019 
0020 #include <util/widgetcolorizer.h>
0021 
0022 #include <debug.h>
0023 
0024 namespace {
0025 const int maxNavigationWidgetWidth = 580;
0026 const int maxNavigationWidgetHeight = 400;
0027 }
0028 
0029 namespace KDevelop {
0030 class AbstractNavigationWidgetPrivate
0031 {
0032 public:
0033     AbstractNavigationWidgetPrivate(AbstractNavigationWidget* q) : q(q) {}
0034 
0035     void anchorClicked(const QUrl&);
0036 
0037     AbstractNavigationWidget* q;
0038 
0039     NavigationContextPointer m_startContext;
0040 
0041     QPointer<QTextBrowser> m_browser;
0042     QWidget* m_currentWidget = nullptr;
0043     QString m_currentText;
0044     mutable QSize m_idealTextSize;
0045     AbstractNavigationWidget::DisplayHints m_hints = AbstractNavigationWidget::NoHints;
0046 
0047     NavigationContextPointer m_context;
0048 };
0049 
0050 AbstractNavigationWidget::AbstractNavigationWidget()
0051     : d_ptr(new AbstractNavigationWidgetPrivate(this))
0052 {
0053     setPalette(QApplication::palette());
0054     setFocusPolicy(Qt::NoFocus);
0055     resize(100, 100);
0056 }
0057 
0058 QSize AbstractNavigationWidget::sizeHint() const
0059 {
0060     Q_D(const AbstractNavigationWidget);
0061 
0062     if (d->m_browser) {
0063         updateIdealSize();
0064         QSize ret = QSize(qMin(d->m_idealTextSize.width(), maxNavigationWidgetWidth),
0065                           qMin(d->m_idealTextSize.height(), maxNavigationWidgetHeight));
0066         if (d->m_idealTextSize.height() >= maxNavigationWidgetHeight) {
0067             //make space for the scrollbar in case it's not fitting
0068             ret.rwidth() += 17; //m_browser->verticalScrollBar()->width() returns 100, for some reason
0069         }
0070 
0071         if (d->m_currentWidget) {
0072             ret.setHeight(ret.height() + d->m_currentWidget->sizeHint().height());
0073             if (d->m_currentWidget->sizeHint().width() > ret.width())
0074                 ret.setWidth(d->m_currentWidget->sizeHint().width());
0075             if (ret.width() < 500) //When we embed a widget, give it some space, even if it doesn't have a large size-hint
0076                 ret.setWidth(500);
0077         }
0078         return ret;
0079     } else
0080         return QWidget::sizeHint();
0081 }
0082 
0083 void AbstractNavigationWidget::initBrowser(int height)
0084 {
0085     Q_D(AbstractNavigationWidget);
0086 
0087     Q_ASSERT(!d->m_browser);
0088     Q_UNUSED(height);
0089     d->m_browser = new QTextBrowser;
0090 
0091     d->m_browser->setOpenLinks(false);
0092     d->m_browser->setOpenExternalLinks(false);
0093 
0094     auto* layout = new QVBoxLayout;
0095     layout->addWidget(d->m_browser);
0096     layout->setContentsMargins(0, 0, 0, 0);
0097     setLayout(layout);
0098 
0099     connect(d->m_browser.data(), &QTextEdit::selectionChanged, this, [this]() {
0100             Q_D(AbstractNavigationWidget);
0101             d->m_browser->copy();
0102         });
0103     connect(d->m_browser.data(), &QTextBrowser::anchorClicked, this, [this](const QUrl& url) {
0104             Q_D(AbstractNavigationWidget);
0105             d->anchorClicked(url);
0106         });
0107 
0108     const auto childWidgets = findChildren<QWidget*>();
0109     for (QWidget* w : childWidgets) {
0110         w->setContextMenuPolicy(Qt::NoContextMenu);
0111     }
0112 }
0113 
0114 AbstractNavigationWidget::~AbstractNavigationWidget()
0115 {
0116     Q_D(AbstractNavigationWidget);
0117 
0118     if (d->m_currentWidget)
0119         layout()->removeWidget(d->m_currentWidget);
0120 }
0121 
0122 void AbstractNavigationWidget::setContext(NavigationContextPointer context, int initBrows)
0123 {
0124     Q_D(AbstractNavigationWidget);
0125 
0126     if (d->m_browser == nullptr)
0127         initBrowser(initBrows);
0128 
0129     if (!context) {
0130         qCDebug(LANGUAGE) << "no new context created";
0131         return;
0132     }
0133     if (context == d->m_context && (!context || context->alreadyComputed()))
0134         return;
0135 
0136     if (!d->m_startContext) {
0137         d->m_startContext = context;
0138     }
0139 
0140     bool wasInitial = (d->m_context == d->m_startContext);
0141 
0142     d->m_context = context;
0143     update();
0144 
0145     emit contextChanged(wasInitial, d->m_context == d->m_startContext);
0146     emit sizeHintChanged();
0147 }
0148 
0149 void AbstractNavigationWidget::updateIdealSize() const
0150 {
0151     Q_D(const AbstractNavigationWidget);
0152 
0153     if (d->m_context && !d->m_idealTextSize.isValid()) {
0154         QTextDocument doc;
0155         doc.setHtml(d->m_currentText);
0156         if (doc.idealWidth() > maxNavigationWidgetWidth) {
0157             doc.setTextWidth(maxNavigationWidgetWidth);
0158             d->m_idealTextSize.setWidth(maxNavigationWidgetWidth);
0159         } else {
0160             d->m_idealTextSize.setWidth(doc.idealWidth());
0161         }
0162         d->m_idealTextSize.setHeight(doc.size().height());
0163     }
0164 }
0165 
0166 void AbstractNavigationWidget::setDisplayHints(DisplayHints hints)
0167 {
0168     Q_D(AbstractNavigationWidget);
0169 
0170     d->m_hints = hints;
0171 }
0172 
0173 void AbstractNavigationWidget::update()
0174 {
0175     Q_D(AbstractNavigationWidget);
0176 
0177     setUpdatesEnabled(false);
0178     Q_ASSERT(d->m_context);
0179 
0180     QString html;
0181     {
0182         DUChainReadLocker lock;
0183         html = d->m_context->html();
0184     }
0185 
0186     if (!html.isEmpty()) {
0187         int scrollPos = d->m_browser->verticalScrollBar()->value();
0188 
0189         if (!(d->m_hints & EmbeddableWidget)) {
0190             // TODO: Only show that the first time, or the first few times this context is shown?
0191             html += QStringLiteral("<p><small>");
0192             if (d->m_context->linkCount() > 0) {
0193                 html +=
0194                     i18n("(Hold <em>Alt</em> to show. Navigate via arrow keys, activate by pressing <em>Enter</em>)");
0195             } else {
0196                 html += i18n("(Hold <em>Alt</em> to show this tooltip)");
0197             }
0198             html += QStringLiteral("</small></p>");
0199         }
0200 
0201         d->m_browser->setHtml(html);
0202 
0203         WidgetColorizer::convertDocumentToDarkTheme(d->m_browser->document());
0204 
0205         d->m_currentText = html;
0206 
0207         d->m_idealTextSize = QSize();
0208 
0209         QSize hint = sizeHint();
0210         if (hint.height() >= d->m_idealTextSize.height()) {
0211             d->m_browser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0212         } else {
0213             d->m_browser->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
0214         }
0215 
0216         d->m_browser->verticalScrollBar()->setValue(scrollPos);
0217         d->m_browser->scrollToAnchor(QStringLiteral("currentPosition"));
0218         d->m_browser->show();
0219     } else {
0220         d->m_browser->hide();
0221     }
0222 
0223     if (d->m_currentWidget) {
0224         layout()->removeWidget(d->m_currentWidget);
0225         d->m_currentWidget->setParent(nullptr);
0226     }
0227 
0228     d->m_currentWidget = d->m_context->widget();
0229 
0230     d->m_browser->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
0231     d->m_browser->setMaximumHeight(10000);
0232 
0233     if (d->m_currentWidget) {
0234         const auto signalSignature = QMetaObject::normalizedSignature(
0235             "navigateDeclaration(KDevelop::IndexedDeclaration)");
0236         if (d->m_currentWidget->metaObject()->indexOfSignal(signalSignature.constData()) != -1) {
0237             connect(d->m_currentWidget, SIGNAL(navigateDeclaration(KDevelop::IndexedDeclaration)),
0238                     this, SLOT(navigateDeclaration(KDevelop::IndexedDeclaration)));
0239         }
0240         layout()->addWidget(d->m_currentWidget);
0241         if (d->m_context->isWidgetMaximized()) {
0242             //Leave unused room to the widget
0243             d->m_browser->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
0244             d->m_browser->setMaximumHeight(d->m_idealTextSize.height());
0245         }
0246     }
0247 
0248     setUpdatesEnabled(true);
0249 }
0250 
0251 NavigationContextPointer AbstractNavigationWidget::context() const
0252 {
0253     Q_D(const AbstractNavigationWidget);
0254 
0255     return d->m_context;
0256 }
0257 
0258 void AbstractNavigationWidget::navigateDeclaration(const IndexedDeclaration& decl)
0259 {
0260     Q_D(AbstractNavigationWidget);
0261 
0262     setContext(d->m_context->accept(decl));
0263 }
0264 
0265 void AbstractNavigationWidgetPrivate::anchorClicked(const QUrl& url)
0266 {
0267     //We may get deleted while the call to acceptLink, so make sure we don't crash in that case
0268     QPointer<AbstractNavigationWidget> thisPtr(q);
0269     NavigationContextPointer nextContext = m_context->acceptLink(url.toString());
0270 
0271     if (thisPtr)
0272         q->setContext(nextContext);
0273 }
0274 
0275 bool AbstractNavigationWidget::next()
0276 {
0277     Q_D(AbstractNavigationWidget);
0278 
0279     Q_ASSERT(d->m_context);
0280     auto ret = d->m_context->nextLink();
0281     update();
0282     return ret;
0283 }
0284 
0285 bool AbstractNavigationWidget::previous()
0286 {
0287     Q_D(AbstractNavigationWidget);
0288 
0289     Q_ASSERT(d->m_context);
0290     auto ret = d->m_context->previousLink();
0291     update();
0292     return ret;
0293 }
0294 
0295 void AbstractNavigationWidget::accept()
0296 {
0297     Q_D(AbstractNavigationWidget);
0298 
0299     Q_ASSERT(d->m_context);
0300 
0301     QPointer<AbstractNavigationWidget> thisPtr(this);
0302     NavigationContextPointer nextContext = d->m_context->accept();
0303 
0304     if (thisPtr)
0305         setContext(nextContext);
0306 }
0307 
0308 void AbstractNavigationWidget::back()
0309 {
0310     Q_D(AbstractNavigationWidget);
0311 
0312     QPointer<AbstractNavigationWidget> thisPtr(this);
0313     NavigationContextPointer nextContext = d->m_context->back();
0314 
0315     if (thisPtr)
0316         setContext(nextContext);
0317 }
0318 
0319 bool AbstractNavigationWidget::up()
0320 {
0321     Q_D(AbstractNavigationWidget);
0322 
0323     auto ret = d->m_context->up();
0324     update();
0325     return ret;
0326 }
0327 
0328 bool AbstractNavigationWidget::down()
0329 {
0330     Q_D(AbstractNavigationWidget);
0331 
0332     auto ret = d->m_context->down();
0333     update();
0334     return ret;
0335 }
0336 
0337 void AbstractNavigationWidget::resetNavigationState()
0338 {
0339     Q_D(AbstractNavigationWidget);
0340 
0341     d->m_context->resetNavigation();
0342     update();
0343 }
0344 
0345 void AbstractNavigationWidget::embeddedWidgetAccept()
0346 {
0347     accept();
0348 }
0349 void AbstractNavigationWidget::embeddedWidgetDown()
0350 {
0351     down();
0352 }
0353 
0354 void AbstractNavigationWidget::embeddedWidgetRight()
0355 {
0356     next();
0357 }
0358 
0359 void AbstractNavigationWidget::embeddedWidgetLeft()
0360 {
0361     previous();
0362 }
0363 
0364 void AbstractNavigationWidget::embeddedWidgetUp()
0365 {
0366     up();
0367 }
0368 
0369 void AbstractNavigationWidget::wheelEvent(QWheelEvent* event)
0370 {
0371     QWidget::wheelEvent(event);
0372     event->accept();
0373 }
0374 }
0375 
0376 #include "moc_abstractnavigationwidget.cpp"