diff --git a/src/plugins/coreplugin/helpitem.cpp b/src/plugins/coreplugin/helpitem.cpp index 268c00ce67b..b8794481cfe 100644 --- a/src/plugins/coreplugin/helpitem.cpp +++ b/src/plugins/coreplugin/helpitem.cpp @@ -104,7 +104,7 @@ bool HelpItem::isValid() const { if (m_helpUrl.isEmpty() && m_helpIds.isEmpty()) return false; - return !links().isEmpty(); + return !links().empty(); } QString HelpItem::extractContent(bool extended) const @@ -116,7 +116,8 @@ QString HelpItem::extractContent(bool extended) const htmlExtractor.setMode(Utils::HtmlDocExtractor::FirstParagraph); QString contents; - for (const QUrl &url : links()) { + for (const Link &item : links()) { + const QUrl url = item.second; const QString html = QString::fromUtf8(Core::HelpManager::fileData(url)); switch (m_category) { case Brief: @@ -157,50 +158,124 @@ QString HelpItem::extractContent(bool extended) const return contents; } -const QMap &HelpItem::links() const +static std::pair extractVersion(const QUrl &url) +{ + const QString host = url.host(); + const QStringList hostParts = host.split('.'); + if (hostParts.size() == 4 && (host.startsWith("com.trolltech.") + || host.startsWith("org.qt-project."))) { + bool ok = false; + // the following is only correct under the specific current conditions, and it will + // always be quite some guessing as long as the version information does not + // include separators for major vs minor vs patch version + const int version = hostParts.at(3).toInt(&ok); + if (ok) { + QUrl urlWithoutVersion(url); + urlWithoutVersion.setHost(hostParts.mid(0, 3).join('.')); + return {urlWithoutVersion, version}; + } + } + return {url, 0}; +} + +// sort primary by "url without version" and seconday by "version" +static bool helpUrlLessThan(const QUrl &a, const QUrl &b) +{ + const std::pair va = extractVersion(a); + const std::pair vb = extractVersion(b); + const QString sa = va.first.toString(); + const QString sb = vb.first.toString(); + if (sa == sb) + return va.second > vb.second; + return sa < sb; +} + +static bool linkLessThan(const HelpItem::Link &a, const HelpItem::Link &b) +{ + return helpUrlLessThan(a.second, b.second); +} + +// links are sorted with highest "version" first (for Qt help urls) +const HelpItem::Links &HelpItem::links() const { if (!m_helpLinks) { if (!m_helpUrl.isEmpty()) { - m_helpLinks.emplace(QMap({{m_helpUrl.toString(), m_helpUrl}})); + m_keyword = m_helpUrl.toString(); + m_helpLinks.emplace(Links{{m_keyword, m_helpUrl}}); } else { m_helpLinks.emplace(); // set a value even if there are no help IDs + QMap helpLinks; for (const QString &id : m_helpIds) { - m_helpLinks = Core::HelpManager::linksForIdentifier(id); - if (!m_helpLinks->isEmpty()) + helpLinks = Core::HelpManager::linksForIdentifier(id); + if (!helpLinks.isEmpty()) { + m_keyword = id; break; + } + } + if (helpLinks.isEmpty()) { // perform keyword lookup as well as a fallback + for (const QString &id : m_helpIds) { + helpLinks = Core::HelpManager::linksForKeyword(id); + if (!helpLinks.isEmpty()) { + m_keyword = id; + m_isFuzzyMatch = true; + break; + } + } + } + QMapIterator it(helpLinks); + while (it.hasNext()) { + it.next(); + m_helpLinks->emplace_back(it.key(), it.value()); } } + Utils::sort(*m_helpLinks, linkLessThan); } return *m_helpLinks; } -static QUrl findBestLink(const QMap &links) +static const HelpItem::Links getBestLinks(const HelpItem::Links &links) { - if (links.isEmpty()) - return QUrl(); - if (links.size() == 1) - return links.first(); - QUrl source = links.first(); - // workaround to show the latest Qt version - int version = 0; - QRegExp exp("(\\d+)"); - foreach (const QUrl &link, links) { - const QString &authority = link.authority(); - if (authority.startsWith("com.trolltech.") - || authority.startsWith("org.qt-project.")) { - if (exp.indexIn(authority) >= 0) { - const int tmpVersion = exp.cap(1).toInt(); - if (tmpVersion > version) { - source = link; - version = tmpVersion; - } - } + // extract the highest version (== first) link of each individual topic + HelpItem::Links bestLinks; + QUrl currentUnversionedUrl; + for (const HelpItem::Link &link : links) { + const QUrl unversionedUrl = extractVersion(link.second).first; + if (unversionedUrl != currentUnversionedUrl) { + currentUnversionedUrl = unversionedUrl; + bestLinks.push_back(link); } } - return source; + return bestLinks; } -const QUrl HelpItem::bestLink() const +static const HelpItem::Links getBestLink(const HelpItem::Links &links) { - return findBestLink(links()); + if (links.empty()) + return {}; + // Extract single link with highest version, from all topics. + // This is to ensure that if we succeeded with an ID lookup, and we have e.g. Qt5 and Qt4 + // documentation, that we only return the Qt5 link even though the Qt5 and Qt4 URLs look + // different. + int highestVersion = -1; + HelpItem::Link bestLink; + for (const HelpItem::Link &link : links) { + const int version = extractVersion(link.second).second; + if (version > highestVersion) { + highestVersion = version; + bestLink = link; + } + } + return {bestLink}; +} + +const HelpItem::Links HelpItem::bestLinks() const +{ + if (m_isFuzzyMatch) + return getBestLinks(links()); + return getBestLink(links()); +} + +const QString HelpItem::keyword() const +{ + return m_keyword; } diff --git a/src/plugins/coreplugin/helpitem.h b/src/plugins/coreplugin/helpitem.h index 5468659b2a4..4d9ab153fb8 100644 --- a/src/plugins/coreplugin/helpitem.h +++ b/src/plugins/coreplugin/helpitem.h @@ -29,16 +29,20 @@ #include -#include #include #include #include +#include + namespace Core { class CORE_EXPORT HelpItem { public: + using Link = std::pair; + using Links = std::vector; + enum Category { ClassOrNamespace, Enum, @@ -77,15 +81,18 @@ public: QString extractContent(bool extended) const; - const QMap &links() const; - const QUrl bestLink() const; + const Links &links() const; + const Links bestLinks() const; + const QString keyword() const; private: QUrl m_helpUrl; QStringList m_helpIds; QString m_docMark; Category m_category = Unknown; - mutable Utils::optional> m_helpLinks; // cached help links + mutable Utils::optional m_helpLinks; // cached help links + mutable QString m_keyword; + mutable bool m_isFuzzyMatch = false; }; } // namespace Core diff --git a/src/plugins/coreplugin/helpmanager.cpp b/src/plugins/coreplugin/helpmanager.cpp index 68c976fbfa0..20c6adf83b7 100644 --- a/src/plugins/coreplugin/helpmanager.cpp +++ b/src/plugins/coreplugin/helpmanager.cpp @@ -91,6 +91,11 @@ QMap linksForIdentifier(const QString &id) return checkInstance() ? m_instance->linksForIdentifier(id) : QMap(); } +QMap linksForKeyword(const QString &keyword) +{ + return checkInstance() ? m_instance->linksForKeyword(keyword) : QMap(); +} + QByteArray fileData(const QUrl &url) { return checkInstance() ? m_instance->fileData(url) : QByteArray(); diff --git a/src/plugins/coreplugin/helpmanager.h b/src/plugins/coreplugin/helpmanager.h index 00273365049..4f4484c65db 100644 --- a/src/plugins/coreplugin/helpmanager.h +++ b/src/plugins/coreplugin/helpmanager.h @@ -64,6 +64,7 @@ CORE_EXPORT void registerDocumentation(const QStringList &fileNames); CORE_EXPORT void unregisterDocumentation(const QStringList &nameSpaces); CORE_EXPORT QMap linksForIdentifier(const QString &id); +CORE_EXPORT QMap linksForKeyword(const QString &id); CORE_EXPORT QByteArray fileData(const QUrl &url); CORE_EXPORT void showHelpUrl(const QUrl &url, HelpViewerLocation location = HelpModeAlways); diff --git a/src/plugins/coreplugin/helpmanager_implementation.h b/src/plugins/coreplugin/helpmanager_implementation.h index 47afa96f348..6c160c7ce3f 100644 --- a/src/plugins/coreplugin/helpmanager_implementation.h +++ b/src/plugins/coreplugin/helpmanager_implementation.h @@ -41,6 +41,7 @@ public: virtual void registerDocumentation(const QStringList &fileNames) = 0; virtual void unregisterDocumentation(const QStringList &nameSpaces) = 0; virtual QMap linksForIdentifier(const QString &id) = 0; + virtual QMap linksForKeyword(const QString &keyword) = 0; virtual QByteArray fileData(const QUrl &url) = 0; virtual void showHelpUrl(const QUrl &url, HelpViewerLocation location = HelpModeAlways) = 0; }; diff --git a/src/plugins/help/helpindexfilter.cpp b/src/plugins/help/helpindexfilter.cpp index e205359cf4d..42467934b79 100644 --- a/src/plugins/help/helpindexfilter.cpp +++ b/src/plugins/help/helpindexfilter.cpp @@ -141,7 +141,7 @@ void HelpIndexFilter::accept(LocatorFilterEntry selection, Q_UNUSED(selectionStart) Q_UNUSED(selectionLength) const QString &key = selection.displayName; - const QMap &links = HelpManager::linksForKeyword(key); + const QMap &links = HelpManager::instance()->linksForKeyword(key); emit linksActivated(links, key); } diff --git a/src/plugins/help/helpmanager.h b/src/plugins/help/helpmanager.h index 825a27d0467..3a65b0f33dc 100644 --- a/src/plugins/help/helpmanager.h +++ b/src/plugins/help/helpmanager.h @@ -54,8 +54,8 @@ public: static void registerUserDocumentation(const QStringList &filePaths); static QSet userDocumentationPaths(); - static QMap linksForKeyword(const QString &key); QMap linksForIdentifier(const QString &id) override; + QMap linksForKeyword(const QString &key) override; static QUrl findFile(const QUrl &url); QByteArray fileData(const QUrl &url) override; diff --git a/src/plugins/help/helpplugin.cpp b/src/plugins/help/helpplugin.cpp index ada46f3b3ec..7703541670e 100644 --- a/src/plugins/help/helpplugin.cpp +++ b/src/plugins/help/helpplugin.cpp @@ -44,6 +44,7 @@ #include "searchwidget.h" #include "searchtaskhandler.h" #include "textbrowserhelpviewer.h" +#include "topicchooser.h" #ifdef QTC_MAC_NATIVE_HELPVIEWER #include "macwebkithelpviewer.h" @@ -636,8 +637,8 @@ void HelpPluginPrivate::requestContextHelp() void HelpPluginPrivate::showContextHelp(const HelpItem &contextHelp) { - const QUrl source = contextHelp.bestLink(); - if (!source.isValid()) { + const HelpItem::Links links = contextHelp.bestLinks(); + if (links.empty()) { // No link found or no context object HelpViewer *viewer = showHelpUrl(QUrl(Help::Constants::AboutBlank), LocalHelpManager::contextHelpOption()); @@ -653,8 +654,19 @@ void HelpPluginPrivate::showContextHelp(const HelpItem &contextHelp) .arg(contextHelp.helpIds().join(", ")) .arg(HelpPlugin::tr("No documentation available."))); } + } else if (links.size() == 1) { + showHelpUrl(links.front().second, LocalHelpManager::contextHelpOption()); } else { - showHelpUrl(source, LocalHelpManager::contextHelpOption()); + QMap map; + for (const HelpItem::Link &link : links) + map.insert(link.first, link.second); + auto tc = new TopicChooser(ICore::dialogParent(), contextHelp.keyword(), map); + tc->setModal(true); + connect(tc, &QDialog::accepted, this, [this, tc] { + showHelpUrl(tc->link(), LocalHelpManager::contextHelpOption()); + }); + connect(tc, &QDialog::finished, tc, [tc] { tc->deleteLater(); }); + tc->show(); } } diff --git a/src/plugins/qmljseditor/qmljshoverhandler.cpp b/src/plugins/qmljseditor/qmljshoverhandler.cpp index 53b3422c80e..6479ba13ad1 100644 --- a/src/plugins/qmljseditor/qmljshoverhandler.cpp +++ b/src/plugins/qmljseditor/qmljshoverhandler.cpp @@ -147,7 +147,6 @@ bool QmlJSHoverHandler::setQmlTypeHelp(const ScopeChain &scopeChain, const Docum { QString moduleName = getModuleName(scopeChain, qmlDocument, value); - QString helpId; QStringList helpIdCandidates; QStringList helpIdPieces(qName); @@ -168,7 +167,7 @@ bool QmlJSHoverHandler::setQmlTypeHelp(const ScopeChain &scopeChain, const Docum helpIdCandidates += helpIdPieces.join('.'); const HelpItem helpItem(helpIdCandidates, qName.join('.'), HelpItem::QmlComponent); - const QMap urlMap = helpItem.links(); + const HelpItem::Links links = helpItem.links(); // Check if the module name contains a major version. QRegularExpression version("^([^\\d]*)(\\d+)\\.*\\d*$"); @@ -176,10 +175,10 @@ bool QmlJSHoverHandler::setQmlTypeHelp(const ScopeChain &scopeChain, const Docum if (m.hasMatch()) { QMap filteredUrlMap; QStringRef maj = m.capturedRef(2); - for (auto x = urlMap.begin(); x != urlMap.end(); ++x) { - QString urlModuleName = x.value().path().split('/')[1]; + for (const HelpItem::Link &link : links) { + QString urlModuleName = link.second.path().split('/')[1]; if (urlModuleName.contains(maj)) - filteredUrlMap.insert(x.key(), x.value()); + filteredUrlMap.insert(link.first, link.second); } if (!filteredUrlMap.isEmpty()) { // Use the URL, to disambiguate different versions