// Copyright (C) 2016 Marc Reilly // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "resourcepreviewhoverhandler.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace Core; using namespace TextEditor; using namespace Utils; namespace ProjectExplorer { /* * finds a quoted sub-string around the pos in the given string */ static QString extractString(const QString &s, int pos) { if (s.size() < 2 || pos < 0 || pos >= s.size()) return QString(); const int firstQuote = s.lastIndexOf('"', pos); if (firstQuote >= 0) { int endQuote = s.indexOf('"', firstQuote + 1); if (endQuote > firstQuote) return s.mid(firstQuote + 1, endQuote - firstQuote - 1); } return QString(); } static QString makeResourcePath(const QStringList &prefixList, const QString &file) { QTC_ASSERT(!prefixList.isEmpty(), return QString()); const QChar sep = '/'; const QString prefix = prefixList.join(sep); if (prefix == sep) return prefix + file; return prefix + sep + file; } /* * tries to match a resource within a given .qrc file, including by alias * * note: resource name should not have any semi-colon in front of it */ static QString findResourceInFile(const QString &resName, const FilePath &qrcFile) { const Result res = qrcFile.fileContents(); if (!res) return {}; QXmlStreamReader xmlr(*res); QStringList prefixStack; while (!xmlr.atEnd() && !xmlr.hasError()) { const QXmlStreamReader::TokenType token = xmlr.readNext(); if (token == QXmlStreamReader::StartElement) { if (xmlr.name() == QLatin1String("qresource")) { const QXmlStreamAttributes sa = xmlr.attributes(); QString prefixName = sa.value("prefix").toString(); if (!prefixName.isEmpty()) { // non-root prefixes will get a '/' when joined in makeResourcePath // it avoids "//" which make comparisons below return false if (prefixName != "/" && prefixName.back() == '/') prefixName.chop(1); prefixStack.push_back(prefixName); } } else if (xmlr.name() == QLatin1String("file")) { const QXmlStreamAttributes sa = xmlr.attributes(); const QString aliasName = sa.value("alias").toString(); const QString fileName = xmlr.readElementText(); if (!aliasName.isEmpty()) { const QString fullAliasName = makeResourcePath(prefixStack, aliasName); if (resName == fullAliasName) return fileName; } const QString fullResName = makeResourcePath(prefixStack, fileName); if (resName == fullResName) return fileName; } } else if (token == QXmlStreamReader::EndElement) { if (xmlr.name() == QLatin1String("qresource")) { if (!prefixStack.isEmpty()) prefixStack.pop_back(); } } } return QString(); } /* * A more efficient way to do this would be to parse the relevant project files * before hand, or cache them as we go - but this works well enough so far. */ static QString findResourceInProject(const QString &resName) { QString s = resName; if (s.startsWith(":/")) s.remove(0, 1); else if (s.startsWith("qrc://")) s.remove(0, 5); else return QString(); if (const Project *project = ProjectTree::currentProject()) { const FilePaths files = project->files( [](const Node *n) { return n->filePath().endsWith(".qrc"); }); for (const FilePath &file : files) { const QFileInfo fi = file.toFileInfo(); if (!fi.isReadable()) continue; const QString fileName = findResourceInFile(s, file); if (fileName.isEmpty()) continue; if (QFileInfo::exists(fileName)) return fileName; QString ret = fi.absolutePath(); if (!ret.endsWith('/')) ret.append('/'); ret.append(fileName); return ret; } } return QString(); } class ResourcePreviewHoverHandler final : public BaseHoverHandler { private: void identifyMatch(TextEditorWidget *editorWidget, int pos, ReportPriority report) final; void operateTooltip(TextEditorWidget *editorWidget, const QPoint &point) final; private: QString makeTooltip() const; QString m_path; }; void ResourcePreviewHoverHandler::identifyMatch(TextEditorWidget *editorWidget, int pos, ReportPriority report) { const QScopeGuard cleanup([this, report] { report(priority()); }); if (editorWidget->extraSelectionTooltip(pos).isEmpty()) { const QTextBlock tb = editorWidget->document()->findBlock(pos); const int tbpos = pos - tb.position(); const QString tbtext = tb.text(); const QString str = extractString(tbtext, tbpos); m_path = findResourceInProject(str); // If resource does not exit, try to fallback to local files if (m_path.isEmpty()) { if (QFileInfo::exists(str)) m_path = str; else if (QUrl(str).isLocalFile()) m_path = str; } setPriority(m_path.isEmpty() ? Priority_None : Priority_Diagnostic + 1); } } void ResourcePreviewHoverHandler::operateTooltip(TextEditorWidget *editorWidget, const QPoint &point) { const QString tt = makeTooltip(); if (!tt.isEmpty()) Utils::ToolTip::show(point, tt, Qt::MarkdownText, editorWidget); else Utils::ToolTip::hide(); } QString ResourcePreviewHoverHandler::makeTooltip() const { if (m_path.isEmpty()) return QString(); QString ret; const Utils::MimeType mimeType = Utils::mimeTypeForFile(m_path); if (mimeType.name().startsWith("image", Qt::CaseInsensitive)) ret += QString("![image](%1) \n").arg(m_path); ret += QString("[%1](%2)").arg(QDir::toNativeSeparators(m_path), m_path); return ret; } BaseHoverHandler &resourcePreviewHoverHandler() { static ResourcePreviewHoverHandler theResourcePreviewHoverHandler; return theResourcePreviewHoverHandler; } } // namespace ProjectExplorer