File indexing completed on 2024-07-14 05:32:48

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"