File indexing completed on 2024-05-19 08:31:41
0001 /* 0002 SPDX-FileCopyrightText: 2009 Aleix Pol <aleixpol@kde.org> 0003 SPDX-FileCopyrightText: 2009 David Nolden <david.nolden.kdevelop@art-master.de> 0004 SPDX-FileCopyrightText: 2010 Benjamin Port <port.benjamin@gmail.com> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "qthelpdocumentation.h" 0010 0011 #include <QLabel> 0012 #include <QUrl> 0013 #include <QTreeView> 0014 #include <QHelpContentModel> 0015 #include <QHeaderView> 0016 #include <QMenu> 0017 #include <QMouseEvent> 0018 #include <QRegularExpression> 0019 #include <QActionGroup> 0020 0021 #include <KLocalizedString> 0022 0023 #include <interfaces/icore.h> 0024 #include <interfaces/idocumentationcontroller.h> 0025 #include <documentation/standarddocumentationview.h> 0026 #include "qthelpnetwork.h" 0027 #include "qthelpproviderabstract.h" 0028 0029 #include <algorithm> 0030 0031 using namespace KDevelop; 0032 0033 QtHelpProviderAbstract* QtHelpDocumentation::s_provider=nullptr; 0034 0035 QtHelpDocumentation::QtHelpDocumentation(const QString& name, const QList<QHelpLink>& info) 0036 : m_provider(s_provider) 0037 , m_name(name) 0038 , m_info(info) 0039 , m_current(info.constBegin()) 0040 , lastView(nullptr) 0041 { 0042 } 0043 0044 namespace { 0045 QList<QHelpLink>::const_iterator findTitle(const QList<QHelpLink>& links, const QString& title) 0046 { 0047 return std::find_if(links.begin(), links.end(), [title](const QHelpLink& helpLink) { 0048 return helpLink.title == title; 0049 }); 0050 } 0051 } 0052 0053 QtHelpDocumentation::QtHelpDocumentation(const QString& name, const QList<QHelpLink>& info, const QString& key) 0054 : m_provider(s_provider) 0055 , m_name(name) 0056 , m_info(info) 0057 , m_current(::findTitle(m_info, key)) 0058 , lastView(nullptr) 0059 { 0060 Q_ASSERT(m_current!=m_info.constEnd()); 0061 } 0062 0063 QString QtHelpDocumentation::description() const 0064 { 0065 const QUrl url = currentUrl(); 0066 //Extract a short description from the html data 0067 const QString dataString = QString::fromLatin1(m_provider->engine()->fileData(url)); ///@todo encoding 0068 0069 const QString fragment = url.fragment(); 0070 const QString p = QStringLiteral("((\\\")|(\\\'))"); 0071 const QString optionalSpace = QStringLiteral(" *"); 0072 const QString exp = QString(QLatin1String("< a name = ") + p + fragment + p + QLatin1String(" > < / a >")).replace(QLatin1Char(' '), optionalSpace); 0073 0074 const QRegularExpression findFragment(exp); 0075 QRegularExpressionMatch findFragmentMatch; 0076 int pos = dataString.indexOf(findFragment, 0, &findFragmentMatch); 0077 0078 if(fragment.isEmpty()) { 0079 pos = 0; 0080 } else { 0081 0082 //Check if there is a title opening-tag right before the fragment, and if yes add it, so we have a nicely formatted caption 0083 const QString titleRegExp = QStringLiteral("< h\\d class = \".*\" >").replace(QLatin1Char(' '), optionalSpace); 0084 const QRegularExpression findTitle(titleRegExp); 0085 const QRegularExpressionMatch match = findTitle.match(dataString, pos); 0086 const int titleStart = match.capturedStart(); 0087 const int titleEnd = titleStart + match.capturedEnd(); 0088 if(titleStart != -1) { 0089 const QStringRef between = dataString.midRef(titleEnd, pos-titleEnd).trimmed(); 0090 if(between.isEmpty()) 0091 pos = titleStart; 0092 } 0093 } 0094 0095 if(pos != -1) { 0096 const QString exp = QString(QStringLiteral("< a name = ") + p + QStringLiteral("((\\S)*)") + p + QStringLiteral(" > < / a >")).replace(QLatin1Char(' '), optionalSpace); 0097 const QRegularExpression nextFragmentExpression(exp); 0098 int endPos = dataString.indexOf(nextFragmentExpression, pos+(fragment.size() ? findFragmentMatch.capturedLength() : 0)); 0099 if(endPos == -1) { 0100 endPos = dataString.size(); 0101 } 0102 0103 { 0104 //Find the end of the last paragraph or newline, so we don't add prefixes of the following fragment 0105 const QString newLineRegExp = QStringLiteral ("< br / > | < / p >").replace(QLatin1Char(' '), optionalSpace); 0106 const QRegularExpression lastNewLine(newLineRegExp); 0107 QRegularExpressionMatch match; 0108 const int newEnd = dataString.lastIndexOf(lastNewLine, endPos, &match); 0109 if(match.isValid() && newEnd > pos) 0110 endPos = newEnd + match.capturedLength(); 0111 } 0112 0113 { 0114 //Find the title, and start from there 0115 const QString titleRegExp = QStringLiteral("< h\\d class = \"title\" >").replace(QLatin1Char(' '), optionalSpace); 0116 const QRegularExpression findTitle(titleRegExp); 0117 const QRegularExpressionMatch match = findTitle.match(dataString); 0118 if (match.isValid()) 0119 pos = qBound(pos, match.capturedStart(), endPos); 0120 } 0121 0122 0123 QString thisFragment = dataString.mid(pos, endPos - pos); 0124 0125 { 0126 //Completely remove the first large header found, since we don't need a header 0127 const QString headerRegExp = QStringLiteral("< h\\d.*>.*?< / h\\d >").replace(QLatin1Char(' '), optionalSpace); 0128 const QRegularExpression findHeader(headerRegExp); 0129 const QRegularExpressionMatch match = findHeader.match(thisFragment); 0130 if(match.isValid()) { 0131 thisFragment.remove(match.capturedStart(), match.capturedLength()); 0132 } 0133 } 0134 0135 { 0136 //Replace all gigantic header-font sizes with <big> 0137 { 0138 const QString sizeRegExp = QStringLiteral("< h\\d ").replace(QLatin1Char(' '), optionalSpace); 0139 const QRegularExpression findSize(sizeRegExp); 0140 thisFragment.replace(findSize, QStringLiteral("<big ")); 0141 } 0142 { 0143 const QString sizeCloseRegExp = QStringLiteral("< / h\\d >").replace(QLatin1Char(' '), optionalSpace); 0144 const QRegularExpression closeSize(sizeCloseRegExp); 0145 thisFragment.replace(closeSize, QStringLiteral("</big><br />")); 0146 } 0147 } 0148 0149 { 0150 //Replace paragraphs by newlines 0151 const QString begin = QStringLiteral("< p >").replace(QLatin1Char(' '), optionalSpace); 0152 const QRegularExpression findBegin(begin); 0153 thisFragment.replace(findBegin, {}); 0154 0155 const QString end = QStringLiteral("< /p >").replace(QLatin1Char(' '), optionalSpace); 0156 const QRegularExpression findEnd(end); 0157 thisFragment.replace(findEnd, QStringLiteral("<br />")); 0158 } 0159 0160 { 0161 //Remove links, because they won't work 0162 const QString link = QString(QStringLiteral("< a href = ") + p + QStringLiteral(".*?") + p).replace(QLatin1Char(' '), optionalSpace); 0163 const QRegularExpression exp(link, QRegularExpression::CaseInsensitiveOption); 0164 thisFragment.replace(exp, QStringLiteral("<a ")); 0165 } 0166 0167 return thisFragment; 0168 } 0169 0170 QStringList titles; 0171 titles.reserve(m_info.size()); 0172 for (auto& link : qAsConst(m_info)) { 0173 titles.append(link.title); 0174 } 0175 return titles.join(QLatin1String(", ")); 0176 } 0177 0178 QWidget* QtHelpDocumentation::documentationWidget(DocumentationFindWidget* findWidget, QWidget* parent) 0179 { 0180 if(m_info.isEmpty()) { //QtHelp sometimes has empty info maps. e.g. availableaudioeffects i 4.5.2 0181 return new QLabel(i18n("Could not find any documentation for '%1'", m_name), parent); 0182 } else { 0183 auto* view = new StandardDocumentationView(findWidget, parent); 0184 view->initZoom(m_provider->name()); 0185 view->setDelegateLinks(true); 0186 view->setNetworkAccessManager(m_provider->networkAccess()); 0187 view->setContextMenuPolicy(Qt::CustomContextMenu); 0188 QObject::connect(view, &StandardDocumentationView::linkClicked, this, &QtHelpDocumentation::jumpedTo); 0189 connect(view, &StandardDocumentationView::customContextMenuRequested, this, &QtHelpDocumentation::viewContextMenuRequested); 0190 0191 view->load(currentUrl()); 0192 lastView = view; 0193 return view; 0194 } 0195 } 0196 0197 void QtHelpDocumentation::viewContextMenuRequested(const QPoint& pos) 0198 { 0199 auto* view = qobject_cast<StandardDocumentationView*>(sender()); 0200 if (!view) 0201 return; 0202 0203 auto menu = view->createStandardContextMenu(); 0204 0205 if (m_info.count() > 1) { 0206 if (!menu->isEmpty()) { 0207 menu->addSeparator(); 0208 } 0209 0210 auto* actionGroup = new QActionGroup(menu); 0211 for (auto it = m_info.constBegin(), end = m_info.constEnd(); it != end; ++it) { 0212 const QString& name = it->title; 0213 auto* act=new QtHelpAlternativeLink(name, this, actionGroup); 0214 act->setCheckable(true); 0215 act->setChecked(name==currentTitle()); 0216 menu->addAction(act); 0217 } 0218 } 0219 0220 menu->setAttribute(Qt::WA_DeleteOnClose); 0221 menu->exec(view->mapToGlobal(pos)); 0222 } 0223 0224 0225 void QtHelpDocumentation::jumpedTo(const QUrl& newUrl) 0226 { 0227 Q_ASSERT(lastView); 0228 m_provider->jumpedTo(newUrl); 0229 } 0230 0231 IDocumentationProvider* QtHelpDocumentation::provider() const 0232 { 0233 return m_provider; 0234 } 0235 0236 QtHelpAlternativeLink::QtHelpAlternativeLink(const QString& name, const QtHelpDocumentation* doc, QObject* parent) 0237 : QAction(name, parent), mDoc(doc), mName(name) 0238 { 0239 connect(this, &QtHelpAlternativeLink::triggered, this, &QtHelpAlternativeLink::showUrl); 0240 } 0241 0242 void QtHelpAlternativeLink::showUrl() 0243 { 0244 IDocumentation::Ptr newDoc(new QtHelpDocumentation(mName, mDoc->info(), mName)); 0245 ICore::self()->documentationController()->showDocumentation(newDoc); 0246 } 0247 0248 HomeDocumentation::HomeDocumentation() : m_provider(QtHelpDocumentation::s_provider) 0249 { 0250 } 0251 0252 QWidget* HomeDocumentation::documentationWidget(DocumentationFindWidget*, QWidget* parent) 0253 { 0254 auto* w=new QTreeView(parent); 0255 // install an event filter to get the mouse events out of it 0256 w->viewport()->installEventFilter(this); 0257 w->header()->setVisible(false); 0258 w->setModel(m_provider->engine()->contentModel()); 0259 0260 connect(w, &QTreeView::clicked, this, &HomeDocumentation::clicked); 0261 return w; 0262 } 0263 0264 void HomeDocumentation::clicked(const QModelIndex& idx) 0265 { 0266 QHelpContentModel* model = m_provider->engine()->contentModel(); 0267 QHelpContentItem* it=model->contentItemAt(idx); 0268 0269 const QList<QHelpLink> info{{it->url(), it->title()}}; 0270 IDocumentation::Ptr newDoc(new QtHelpDocumentation(it->title(), info)); 0271 ICore::self()->documentationController()->showDocumentation(newDoc); 0272 } 0273 0274 QString HomeDocumentation::name() const 0275 { 0276 return i18n("QtHelp Home Page"); 0277 } 0278 0279 IDocumentationProvider* HomeDocumentation::provider() const 0280 { 0281 return m_provider; 0282 } 0283 0284 bool HomeDocumentation::eventFilter(QObject* obj, QEvent* event) 0285 { 0286 if(event->type() == QEvent::MouseButtonPress) { 0287 // Here we need to set accpeted to false to let it propagate up 0288 event->setAccepted(false); 0289 } 0290 return QObject::eventFilter(obj, event); 0291 } 0292 0293 #include "moc_qthelpdocumentation.cpp"