File indexing completed on 2024-11-10 11:07:45
0001 /*************************************************************************** 0002 * Copyright (C) 2003-2006 by David Saxton * 0003 * david@bluehaze.org * 0004 * * 0005 * This program is free software; you can redistribute it and/or modify * 0006 * it under the terms of the GNU General Public License as published by * 0007 * the Free Software Foundation; either version 2 of the License, or * 0008 * (at your option) any later version. * 0009 ***************************************************************************/ 0010 0011 #include "contexthelp.h" 0012 #include "cnitem.h" 0013 #include "cnitemgroup.h" 0014 #include "docmanager.h" 0015 #include "itemlibrary.h" 0016 #include "itemselector.h" 0017 #include "katemdi.h" 0018 #include "libraryitem.h" 0019 #include "richtexteditor.h" 0020 0021 #include <KIO/Global> 0022 #include <KIconLoader> 0023 #include <KLocalizedString> 0024 #include <KMessageBox> 0025 // #include <k3popupmenu.h> 0026 #include <KRun> 0027 // #include <k3iconview.h> 0028 0029 #include <QDropEvent> 0030 #include <QEvent> 0031 #include <QFile> 0032 #include <QFileDialog> 0033 #include <QIcon> 0034 #include <QLabel> 0035 #include <QRegExp> 0036 #include <QTimer> 0037 #include <QToolButton> 0038 #include <QValidator> 0039 // #include <q3widgetstack.h> 0040 #include <QMimeData> 0041 #include <QStandardPaths> 0042 #include <QTextBrowser> 0043 0044 #include <cassert> 0045 0046 #include <ktechlab_debug.h> 0047 0048 ContextHelp *ContextHelp::m_pSelf = nullptr; 0049 0050 ContextHelp *ContextHelp::self(KateMDI::ToolView *parent) 0051 { 0052 if (!m_pSelf) { 0053 assert(parent); 0054 m_pSelf = new ContextHelp(parent); 0055 } 0056 return m_pSelf; 0057 } 0058 0059 ContextHelp::ContextHelp(KateMDI::ToolView *parent) 0060 : QWidget(parent) 0061 , Ui::ContextHelpWidget(/* parent */) 0062 { 0063 setupUi(this); 0064 0065 setWhatsThis(i18n("Provides context-sensitive help relevant to the current editing being performed.")); 0066 setAcceptDrops(true); 0067 0068 if (parent->layout()) { 0069 parent->layout()->addWidget(this); 0070 qCDebug(KTL_LOG) << " added item selector to parent's layout " << parent; 0071 } else { 0072 qCWarning(KTL_LOG) << " unexpected null layout on parent " << parent; 0073 } 0074 0075 QFont font; 0076 font.setBold(true); 0077 if (font.pointSize() != 0) 0078 font.setPointSize(int(font.pointSize() * 1.4)); 0079 m_pNameLabel->setFont(font); 0080 m_pNameLabel->setTextFormat(Qt::RichText); 0081 0082 m_pBrowserView = new QTextBrowser; 0083 m_pBrowserView->setOpenLinks(false); 0084 0085 m_pBrowserView->setFocusPolicy(Qt::NoFocus); 0086 m_pBrowserLayout->addWidget(m_pBrowserView); 0087 0088 connect(m_pBrowserView, &QTextBrowser::anchorClicked, this, &ContextHelp::openURL); 0089 0090 m_pEditor = new RichTextEditor(m_pWidgetStack->widget(1)); 0091 m_pEditor->setObjectName("ContextHelpEditor"); 0092 m_pTopLayout->addWidget(m_pEditor); 0093 0094 m_pEditor->installEventFilter(this); 0095 m_pEditor->editorViewport()->installEventFilter(this); 0096 slotClear(); 0097 0098 connect(m_pEditButton, &QPushButton::clicked, this, &ContextHelp::slotEdit); 0099 connect(m_pSaveButton, &QPushButton::clicked, this, &ContextHelp::slotSave); 0100 connect(m_pResetButton, &QToolButton::clicked, this, &ContextHelp::slotEditReset); 0101 connect(m_pChangeDescriptionsDirectory, &QToolButton::clicked, this, &ContextHelp::requestItemDescriptionsDirectory); 0102 0103 connect(m_pLanguageSelect, qOverload<int>(&QComboBox::activated), this, &ContextHelp::setCurrentLanguage); 0104 0105 m_pResetButton->setIcon(QIcon::fromTheme("dialog-cancel")); 0106 m_pChangeDescriptionsDirectory->setIcon(QIcon::fromTheme("folder")); 0107 0108 connect(ComponentSelector::self(), &ComponentSelector::itemSelected, this, &ContextHelp::setBrowserItem); 0109 0110 connect(FlowPartSelector::self(), &FlowPartSelector::itemSelected, this, &ContextHelp::setBrowserItem); 0111 #ifdef MECHANICS 0112 connect(MechanicsSelector::self(), &MechanicsSelector::itemSelected, this, &MechanicsSelector::setBrowserItem); 0113 #endif 0114 0115 QTimer::singleShot(10, this, &ContextHelp::slotInitializeLanguageList); 0116 } 0117 0118 ContextHelp::~ContextHelp() 0119 { 0120 } 0121 0122 bool ContextHelp::eventFilter(QObject *watched, QEvent *e) 0123 { 0124 // qCDebug(KTL_LOG) << "watched="<<watched; 0125 0126 if ((watched != m_pEditor) && (watched != m_pEditor->editorViewport())) 0127 return false; 0128 0129 switch (e->type()) { 0130 case QEvent::DragEnter: { 0131 QDragEnterEvent *dragEnter = static_cast<QDragEnterEvent *>(e); 0132 if (!dragEnter->mimeData()->text().startsWith("ktechlab/")) 0133 break; 0134 0135 // dragEnter->acceptAction(); // 2018.12.07 0136 dragEnter->acceptProposedAction(); 0137 return true; 0138 } 0139 0140 case QEvent::Drop: { 0141 QDropEvent *dropEvent = static_cast<QDropEvent *>(e); 0142 const QMimeData *mimeData = dropEvent->mimeData(); 0143 0144 if (!mimeData->text().startsWith("ktechlab/")) 0145 break; 0146 0147 dropEvent->accept(); 0148 0149 QString type; 0150 QDataStream stream(mimeData->data(mimeData->text()) /*, QIODevice::ReadOnly */); 0151 stream >> type; 0152 0153 LibraryItem *li = itemLibrary()->libraryItem(type); 0154 if (!li) 0155 return true; 0156 0157 m_pEditor->insertURL("ktechlab-help:///" + type, li->name()); 0158 return true; 0159 } 0160 0161 default: 0162 break; 0163 } 0164 0165 return false; 0166 } 0167 0168 void ContextHelp::slotInitializeLanguageList() 0169 { 0170 const QStringList descriptionLanguages = itemLibrary()->descriptionLanguages(); 0171 for (const QString &languageCode : descriptionLanguages) { 0172 QString text = languageCode; 0173 QLocale locale(languageCode); 0174 if (locale != QLocale::c()) { 0175 text = locale.nativeLanguageName(); 0176 // For some languages the native name might be empty. 0177 // In this case use the non native language name as fallback. 0178 // See: QTBUG-51323 0179 if (text.isEmpty()) { 0180 text = QLocale::languageToString(locale.language()); 0181 } 0182 } 0183 m_pLanguageSelect->addItem(text, languageCode); 0184 } 0185 m_currentLanguage = QLocale().name(); 0186 const int currentIndex = m_pLanguageSelect->findData(m_currentLanguage); 0187 m_pLanguageSelect->setCurrentIndex(currentIndex); 0188 } 0189 0190 bool ContextHelp::isEditChanged() 0191 { 0192 if (m_lastItemType.isEmpty()) 0193 return false; 0194 0195 // Is the edit widget raised? 0196 if (m_pWidgetStack->currentIndex() != 1) { 0197 return false; 0198 } 0199 0200 // We are currently editing an item. Is it changed? 0201 return (m_pEditor->text() != itemLibrary()->description(m_lastItemType, m_currentLanguage).trimmed()); 0202 } 0203 0204 void ContextHelp::slotUpdate(Item *item) 0205 { 0206 if (isEditChanged()) { 0207 return; 0208 } 0209 0210 m_lastItemType = item ? item->type() : QString(); 0211 m_pEditButton->setEnabled(item); 0212 0213 if (!item) { 0214 slotClear(); 0215 return; 0216 } 0217 0218 m_pWidgetStack->setCurrentIndex(0); 0219 setContextHelp(item->name(), itemLibrary()->description(m_lastItemType, QLocale().name())); 0220 } 0221 0222 void ContextHelp::setBrowserItem(const QString &type) 0223 { 0224 if (isEditChanged()) 0225 return; 0226 0227 QString description = itemLibrary()->description(type, QLocale().name()); 0228 if (description.isEmpty()) 0229 return; 0230 0231 QString name; 0232 LibraryItem *li = itemLibrary()->libraryItem(type); 0233 if (li) 0234 name = li->name(); 0235 else 0236 name = type; 0237 0238 m_lastItemType = type; 0239 setContextHelp(name, description); 0240 m_pEditButton->setEnabled(true); 0241 } 0242 0243 void ContextHelp::slotClear() 0244 { 0245 setContextHelp(i18n("No Item Selected"), nullptr); 0246 m_pEditButton->setEnabled(false); 0247 0248 // Can we go hide the edit widget? 0249 if (!isEditChanged()) 0250 m_pWidgetStack->setCurrentIndex(0); 0251 } 0252 0253 void ContextHelp::slotMultipleSelected() 0254 { 0255 setContextHelp(i18n("Multiple Items"), nullptr); 0256 } 0257 0258 void ContextHelp::setContextHelp(QString name, QString help) 0259 { 0260 // BEGIN modify help string as appropriate 0261 help = help.trimmed(); 0262 parseInfo(help); 0263 RichTextEditor::makeUseStandardFont(&help); 0264 addLinkTypeAppearances(&help); 0265 // END modify help string as appropriate 0266 0267 m_pNameLabel->setText(name); 0268 m_pBrowserView->setSearchPaths({itemLibrary()->itemDescriptionsDirectory()}); 0269 m_pBrowserView->clear(); 0270 if (help.startsWith("<html>")) { 0271 m_pBrowserView->insertHtml(help); 0272 } else { 0273 m_pBrowserView->insertPlainText(help); 0274 } 0275 } 0276 0277 void ContextHelp::parseInfo(QString &info) 0278 { 0279 info.replace("<example>", "<br><br><b>Example:</b><blockquote>"); 0280 info.replace("</example>", "</blockquote>"); 0281 } 0282 0283 void ContextHelp::slotEdit() 0284 { 0285 if (m_lastItemType.isEmpty()) 0286 return; 0287 0288 QStringList resourcePaths; 0289 QString currentResourcePath = itemLibrary()->itemDescriptionsDirectory(); 0290 QString defaultResourcePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, "contexthelp/", QStandardPaths::LocateDirectory); 0291 0292 resourcePaths << currentResourcePath; 0293 if (currentResourcePath != defaultResourcePath) 0294 resourcePaths << defaultResourcePath; 0295 0296 m_pEditor->setResourcePaths(resourcePaths); 0297 QString description = itemLibrary()->description(m_lastItemType, m_currentLanguage); 0298 m_pEditor->setText(description); 0299 m_pWidgetStack->setCurrentIndex(1); 0300 } 0301 0302 void ContextHelp::setCurrentLanguage(int languageIndex) 0303 { 0304 const QString language = m_pLanguageSelect->itemData(languageIndex).toString(); 0305 if (!saveDescription(m_currentLanguage)) { 0306 m_pLanguageSelect->blockSignals(true); 0307 const int currentIndex = m_pLanguageSelect->findData(m_currentLanguage); 0308 m_pLanguageSelect->setCurrentIndex(currentIndex); 0309 m_pLanguageSelect->blockSignals(false); 0310 return; 0311 } 0312 0313 m_currentLanguage = language; 0314 slotEdit(); 0315 } 0316 0317 void ContextHelp::requestItemDescriptionsDirectory() 0318 { 0319 const QString path = QFileDialog::getExistingDirectory(nullptr, QString(), itemLibrary()->itemDescriptionsDirectory()); 0320 if (!path.isEmpty()) { 0321 itemLibrary()->setItemDescriptionsDirectory(path); 0322 } 0323 } 0324 0325 void ContextHelp::slotEditReset() 0326 { 0327 if (isEditChanged()) { 0328 KGuiItem continueItem = KStandardGuiItem::cont(); // KStandardGuiItem::cont(); 0329 continueItem.setText(i18n("Reset")); 0330 int answer = KMessageBox::warningContinueCancel(this, i18n("Reset item help to last saved changes?"), i18n("Reset"), continueItem); 0331 if (answer == KMessageBox::Cancel) 0332 return; 0333 } 0334 0335 m_pWidgetStack->setCurrentIndex(0); 0336 } 0337 0338 void ContextHelp::slotSave() 0339 { 0340 if (!saveDescription(m_currentLanguage)) 0341 return; 0342 0343 setContextHelp(m_pNameLabel->text(), itemLibrary()->description(m_lastItemType, QLocale().name())); 0344 m_pWidgetStack->setCurrentIndex(0); 0345 } 0346 0347 bool ContextHelp::saveDescription(const QString &language) 0348 { 0349 if (m_lastItemType.isEmpty()) { 0350 KMessageBox::error(nullptr, i18n("Cannot save item description.")); 0351 return false; 0352 } 0353 0354 return itemLibrary()->setDescription(m_lastItemType, m_pEditor->text(), language); 0355 } 0356 0357 // static function 0358 void ContextHelp::addLinkTypeAppearances(QString *html) 0359 { 0360 QRegExp rx("<a href=\"([^\"]*)\">([^<]*)</a>"); 0361 0362 int pos = 0; 0363 0364 while ((pos = rx.indexIn(*html, pos)) >= 0) { 0365 QString anchorText = rx.cap(0); // contains e.g.: <a href="http://ktechlab.org/">KTechlab website</a> 0366 const QString urlString = rx.cap(1); // contains e.g.: http://ktechlab.org/ 0367 QString text = rx.cap(2); // contains e.g.: KTechlab website 0368 0369 int length = anchorText.length(); 0370 0371 const QUrl url(urlString); 0372 LinkType lt = extractLinkType(url); 0373 0374 QColor color; // default constructor gives an "invalid" color 0375 QString imageURL; 0376 0377 switch (lt) { 0378 case HelpLink: 0379 break; 0380 0381 case NewHelpLink: 0382 color = Qt::red; 0383 break; 0384 0385 case ExampleLink: { 0386 // QString iconName = KMimeType::iconNameForURL( examplePathToFullPath( KUrl( url ).path() ) ); 0387 QString iconName = KIO::iconNameForUrl(QUrl::fromLocalFile(examplePathToFullPath(url.path()))); 0388 imageURL = KIconLoader::global()->iconPath(iconName, -KIconLoader::SizeSmall); 0389 break; 0390 } 0391 0392 case ExternalLink: { 0393 imageURL = QStandardPaths::locate(QStandardPaths::AppDataLocation, "icons/external_link.png"); 0394 break; 0395 } 0396 } 0397 0398 QString newAnchorText; 0399 0400 if (color.isValid()) { 0401 newAnchorText = QString("<a href=\"%1\" style=\"color: %2;\">%3</a>").arg(urlString, color.name(), text); 0402 } else if (!imageURL.isEmpty()) { 0403 newAnchorText = anchorText; 0404 newAnchorText += QString(" <img src=\"%1\"/>").arg(imageURL); 0405 } 0406 0407 if (!newAnchorText.isEmpty()) 0408 html->replace(pos, length, newAnchorText); 0409 0410 pos++; // avoid the string we just found 0411 } 0412 } 0413 0414 // static function 0415 ContextHelp::LinkType ContextHelp::extractLinkType(const QUrl &url) 0416 { 0417 QString path = url.path(); 0418 0419 if (url.scheme() == "ktechlab-help") { 0420 if (itemLibrary()->haveDescription(path, QLocale().name())) 0421 return HelpLink; 0422 else 0423 return NewHelpLink; 0424 } 0425 0426 if (url.scheme() == "ktechlab-example") 0427 return ExampleLink; 0428 0429 return ExternalLink; 0430 } 0431 0432 // static function 0433 QString ContextHelp::examplePathToFullPath(QString path) 0434 { 0435 // quick security check 0436 path.remove(".."); 0437 0438 if (path.startsWith("/")) 0439 path.remove(0, 1); 0440 0441 return QStandardPaths::locate(QStandardPaths::AppDataLocation, "examples/" + path); 0442 } 0443 0444 void ContextHelp::openURL(const QUrl &url /*, const KParts::OpenUrlArguments & */) 0445 { 0446 QString path = url.path(); 0447 0448 switch (extractLinkType(url)) { 0449 case HelpLink: 0450 case NewHelpLink: 0451 setBrowserItem(path); 0452 break; 0453 0454 case ExampleLink: 0455 DocManager::self()->openURL(QUrl::fromLocalFile(examplePathToFullPath(path))); 0456 break; 0457 0458 case ExternalLink: { 0459 // external url 0460 KRun *r = new KRun(url, this); 0461 connect(r, &KRun::finished, r, &KRun::deleteLater); 0462 connect(r, &KRun::error, r, &KRun::deleteLater); 0463 break; 0464 } 0465 } 0466 } 0467 0468 #include "moc_contexthelp.cpp"