File indexing completed on 2024-05-05 17:15:15

0001 /***************************************************************************
0002   Copyright (C) 2005-2007 by Holger Danielsson (holger.danielsson@t-online.de)
0003             (C) 2014-2022 by Michel Ludwig (michel.ludwig@kdemail.net)
0004  ***************************************************************************/
0005 
0006 /***************************************************************************
0007  *                                                                         *
0008  *   This program is free software; you can redistribute it and/or modify  *
0009  *   it under the terms of the GNU General Public License as published by  *
0010  *   the Free Software Foundation; either version 2 of the License, or     *
0011  *   (at your option) any later version.                                   *
0012  *                                                                         *
0013  ***************************************************************************/
0014 
0015 #include "dialogs/texdocumentationdialog.h"
0016 #include "kileconstants.h"
0017 #include "kiledebug.h"
0018 
0019 #include <KConfigGroup>
0020 #include <KIO/ApplicationLauncherJob>
0021 #include <KIO/JobUiDelegateFactory>
0022 #include <KLocalizedString>
0023 #include <KJobUiDelegate>
0024 #include <KMessageBox>
0025 #include <KApplicationTrader>
0026 #include <KProcess>
0027 #include <KService>
0028 
0029 #include <QBoxLayout>
0030 #include <QDialogButtonBox>
0031 #include <QDir>
0032 #include <QEvent>
0033 #include <QFile>
0034 #include <QFileInfo>
0035 #include <QGroupBox>
0036 #include <QHBoxLayout>
0037 #include <QKeyEvent>
0038 #include <QLabel>
0039 #include <QLayout>
0040 #include <QLineEdit>
0041 #include <QMimeDatabase>
0042 #include <QMimeType>
0043 #include <QPushButton>
0044 #include <QRegExp>
0045 #include <QTemporaryFile>
0046 #include <QTreeWidget>
0047 #include <QUrl>
0048 #include <QVBoxLayout>
0049 
0050 namespace KileDialog
0051 {
0052 
0053 TexDocDialog::TexDocDialog(QWidget *parent)
0054     : QDialog(parent)
0055     , m_buttonBox(new QDialogButtonBox(QDialogButtonBox::RestoreDefaults|QDialogButtonBox::Close))
0056     , m_tempfile(Q_NULLPTR)
0057     , m_proc(Q_NULLPTR)
0058 {
0059     setWindowTitle(i18n("Documentation Browser"));
0060     setModal(true);
0061     QVBoxLayout *mainLayout = new QVBoxLayout;
0062     setLayout(mainLayout);
0063 
0064     // listview
0065     m_texdocs = new QTreeWidget(this);
0066     mainLayout->addWidget(m_texdocs);
0067     m_texdocs->setRootIsDecorated(true);
0068     m_texdocs->setHeaderLabel(i18n("Table of Contents"));
0069 
0070     // groupbox
0071     QGroupBox *groupbox = new QGroupBox(i18n("Search"), this);
0072     mainLayout->addWidget(groupbox);
0073     QHBoxLayout *groupboxLayout = new QHBoxLayout();
0074     groupboxLayout->setAlignment(Qt::AlignTop);
0075     groupbox->setLayout(groupboxLayout);
0076 
0077     m_leKeywords = new QLineEdit(groupbox);
0078     m_leKeywords->setPlaceholderText("Keyword");
0079     m_leKeywords->setClearButtonEnabled(true);
0080     m_pbSearch = new QPushButton(i18n("&Search"), groupbox);
0081 
0082     groupboxLayout->addWidget(m_leKeywords);
0083     groupboxLayout->addWidget(m_pbSearch);
0084 
0085     m_texdocs->setWhatsThis(i18n("<p>A list of the documentation provided by the installed TeX distribution.</p>"
0086                                  "<p>Double clicking on an item or pressing the space key will open a viewer to show the corresponding file.</p>"
0087                                  "<p>Items that are grayed out are not installed.</p>"));
0088     m_leKeywords->setWhatsThis(i18n("You can choose a keyword to show only document files that are related to this keyword."));
0089     m_pbSearch->setWhatsThis(i18n("Start the search for the chosen keyword."));
0090     m_pbSearch->setEnabled(false);
0091     m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setWhatsThis(i18n("Reset list to all available documentation files."));
0092     m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setText(i18n("Cancel &Search"));
0093     m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(false);
0094     connect(m_buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked,
0095             this, &TexDocDialog::slotResetSearch);
0096 
0097     // catch some Return/Enter events
0098     m_texdocs->installEventFilter(this);
0099     m_leKeywords->installEventFilter(this);
0100 
0101     connect(m_texdocs, &QTreeWidget::itemDoubleClicked, this, &TexDocDialog::slotListViewDoubleClicked);
0102     connect(m_pbSearch, &QPushButton::clicked, this, &TexDocDialog::slotSearchClicked);
0103     connect(m_leKeywords, &QLineEdit::textChanged, this, &TexDocDialog::slotTextChanged);
0104 
0105     m_texmfPath.clear();
0106     m_texmfdocPath.clear();
0107     m_texdoctkPath.clear();
0108 
0109     connect(this, &TexDocDialog::processFinished, this, &TexDocDialog::slotInitToc);
0110     executeScript(
0111         "kpsewhich --progname=texdoctk --format='other text files' texdoctk.dat && "
0112         "kpsewhich --expand-path='$TEXMF/doc' && "
0113         "kpsewhich --expand-path='$TEXMF'"
0114     );
0115 
0116     mainLayout->addWidget(m_texdocs);
0117     mainLayout->addWidget(groupbox);
0118     mainLayout->addWidget(m_buttonBox);
0119 
0120     connect(m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
0121     connect(m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0122 
0123     resize(sizeHint() + m_texdocs->sizeHint());
0124 }
0125 
0126 TexDocDialog::~TexDocDialog()
0127 {
0128     delete m_proc;
0129     delete m_tempfile;
0130 }
0131 
0132 ////////////////////// TOC //////////////////////
0133 
0134 void TexDocDialog::readToc()
0135 {
0136     // open to read
0137     QFile fin(m_texdoctkPath);
0138     if (!fin.exists() || !fin.open(QIODevice::ReadOnly)) {
0139         KMessageBox::error(this, i18n("Could not read 'texdoctk.dat'."));
0140         return;
0141     }
0142 
0143     // use a textstream to read all data
0144     QString textline;
0145     QTextStream data(&fin);
0146     while (!data.atEnd()) {
0147         textline = data.readLine();
0148         if (!(textline.isEmpty() || textline[0] == '#')) {
0149             // save the whole entry
0150             m_tocList.append(textline);
0151 
0152             // list entries 0,1,basename(2),3 are needed for keyword search
0153             // (key,title,filepath,keywords)
0154             QStringList list = textline.split(';', Qt::KeepEmptyParts);
0155 
0156             // get basename of help file
0157             QString basename;
0158             if (list.count() > 2) {
0159                 QFileInfo fi(list[2]);
0160                 basename = fi.baseName().toLower();
0161             }
0162             else {
0163                 if (list.count() < 2) {
0164                     continue;
0165                 }
0166             }
0167             QString entry = list[0] + ';' + list[1];
0168             if (!basename.isEmpty()) {
0169                 entry += ';' + basename;
0170             }
0171             if (list.count() > 3) {
0172                 entry += ';' + list[3];
0173             }
0174             m_tocSearchList.append(entry);
0175         }
0176     }
0177 }
0178 
0179 void TexDocDialog::showToc(const QString &caption, const QStringList &doclist, bool toc)
0180 {
0181     QString section;
0182     QStringList keylist;
0183     QTreeWidgetItem *itemsection = Q_NULLPTR;
0184 
0185     setUpdatesEnabled(false);
0186     m_texdocs->setHeaderLabel(caption);
0187 
0188     for (int i = 0; i < doclist.count(); ++i) {
0189         if (doclist[i][0] == '@') {
0190             section = doclist[i];
0191             itemsection = new QTreeWidgetItem(m_texdocs, QStringList(section.remove(0, 1)));
0192         }
0193         else {
0194             keylist = doclist[i].split(';', Qt::KeepEmptyParts);
0195             if (keylist.size() < 4) {
0196                 continue;
0197             }
0198             if (itemsection) {
0199                 QTreeWidgetItem *item = new QTreeWidgetItem(itemsection, QStringList() << keylist[1] << keylist[0]);
0200                 item->setIcon(0, QIcon::fromTheme(getIconName(keylist[2])));
0201 
0202                 QString filename = findFile(keylist[2]);
0203                 if(filename.isEmpty()) {
0204                     item->setDisabled(true);
0205                 }
0206 
0207                 // save filename in dictionary
0208                 m_dictDocuments[keylist[0]] = filename;
0209             }
0210         }
0211     }
0212     setUpdatesEnabled(true);
0213 
0214     if (toc) {
0215         m_pbSearch->setEnabled(false);
0216     }
0217     m_buttonBox->button(QDialogButtonBox::RestoreDefaults)->setEnabled(!toc);
0218     m_texdocs->setFocus();
0219 
0220     if (m_texdocs->topLevelItemCount() == 1) {
0221         m_texdocs->expandAll();
0222     }
0223 }
0224 
0225 bool TexDocDialog::eventFilter(QObject *o, QEvent *e)
0226 {
0227     // catch KeyPress events
0228     if (e->type() == QEvent::KeyPress) {
0229         QKeyEvent *kev = static_cast<QKeyEvent*>(e);
0230 
0231         // ListView:
0232         //  - space:  enable start of viewer
0233         //  - return: ignore
0234         if(o == m_texdocs) {
0235             if(kev->key() == Qt::Key_Space) {
0236                 slotListViewDoubleClicked(m_texdocs->currentItem());
0237                 return true;
0238             }
0239             if(kev->key() == Qt::Key_Return || kev->key() == Qt::Key_Enter) {
0240                 return true;
0241             }
0242         }
0243 
0244         // LineEdit
0245         //  - return: start search, if button is enabled
0246         if (o == m_leKeywords) {
0247             if(kev->key() == Qt::Key_Return || kev->key() == Qt::Key_Enter) {
0248                 callSearch();
0249                 return true;
0250             }
0251         }
0252     }
0253 
0254     return false;
0255 }
0256 
0257 ////////////////////// prepare document file //////////////////////
0258 
0259 QString TexDocDialog::searchFile(const QString &docfilename, const QString &listofpaths, const QString &subdir)
0260 {
0261     const QStringList pathlist  = listofpaths.split(LIST_SEPARATOR);
0262     const QString extensions[] = {"", QLatin1String(".gz"), QLatin1String(".bz2")};
0263 
0264     QString filename;
0265     for(const QString& itp : pathlist) {
0266         for(const QString& ite : extensions) {
0267             filename = (subdir.isEmpty()) ? itp + '/' + docfilename + ite
0268                        : itp + '/' + subdir + '/' + docfilename + ite;
0269 
0270             if(QFile::exists(filename)) {
0271                 return filename;
0272             }
0273         }
0274     }
0275 
0276     return QString();
0277 }
0278 
0279 QString TexDocDialog::findFile(const QString &docfilename)
0280 {
0281     QString filename = searchFile(docfilename, m_texmfdocPath);
0282     if(filename.isEmpty()) {
0283         // not found: search it elsewhere
0284         filename = searchFile(docfilename, m_texmfPath, "tex");
0285         if(filename.isEmpty()) {
0286             return QString();
0287         }
0288         return filename;
0289     }
0290     return filename;
0291 }
0292 
0293 void TexDocDialog::showFile(const QString &filename)
0294 {
0295     KILE_DEBUG_MAIN << "\tshow file: " << filename << Qt::endl;
0296     if (QFile::exists(filename)) {
0297         QUrl url;
0298         url.setPath(filename);
0299 
0300         KService::List offers = KApplicationTrader::queryByMimeType(getMimeType(filename));
0301         if(offers.isEmpty()) {
0302             KMessageBox::error(this, i18n("No KDE service found for this file."));
0303             return;
0304         }
0305         QList<QUrl> lst;
0306         lst.append(url);
0307         auto *job = new KIO::ApplicationLauncherJob(offers.first());
0308         job->setUrls(lst);
0309         job->setUiDelegate(KIO::createDefaultJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, this));
0310         job->setRunFlags(KIO::ApplicationLauncherJob::DeleteTemporaryFiles);
0311         job->start();
0312     }
0313 }
0314 
0315 
0316 ////////////////////// Slots //////////////////////
0317 
0318 void TexDocDialog::slotListViewDoubleClicked(QTreeWidgetItem *item)
0319 {
0320     if (!item->parent() || item->isDisabled()) {
0321         return;
0322     }
0323 
0324     QString package = item->text(1);
0325     KILE_DEBUG_MAIN << "\tselect child: "  << item->text(0) << Qt::endl
0326                     << "\tis package: " << package << Qt::endl;
0327     if (!m_dictDocuments.contains(package)) {
0328         return;
0329     }
0330 
0331     QString filename = m_dictDocuments[package];
0332     KILE_DEBUG_MAIN << "\tgot filename:" << filename << Qt::endl;
0333     if(filename.isEmpty()) {
0334         KMessageBox::error(this, i18n("Could not find the documentation file '%1'", filename));
0335         return;
0336     }
0337 
0338     showFile(filename);
0339 }
0340 
0341 void TexDocDialog::slotTextChanged(const QString &text)
0342 {
0343     m_pbSearch->setEnabled(! text.trimmed().isEmpty());
0344 }
0345 
0346 void TexDocDialog::slotSearchClicked()
0347 {
0348     QString keyword = m_leKeywords->text().trimmed();
0349     if (keyword.isEmpty()) {
0350         KMessageBox::error(this, i18n("No keyword given."));
0351         return;
0352     }
0353 
0354     QString section;
0355     bool writesection = true;
0356     QStringList searchlist;
0357 
0358     for (int i = 0; i < m_tocList.count(); i++) {
0359         if (m_tocList[i][0] == '@') {
0360             section = m_tocList[i];
0361             writesection = true;
0362         }
0363         else {
0364             if (i < m_tocSearchList.count() && m_tocSearchList[i].indexOf(keyword, 0, Qt::CaseInsensitive) > -1) {
0365                 if (writesection) {
0366                     searchlist.append(section);
0367                 }
0368                 searchlist.append(m_tocList[i]);
0369                 writesection = false;
0370             }
0371         }
0372     }
0373 
0374     if (searchlist.count() > 0) {
0375         m_texdocs->clear();
0376         showToc(i18n("Search results for keyword '%1'", keyword), searchlist, false);
0377     }
0378     else {
0379         KMessageBox::error(this, i18n("No documents found for keyword '%1'.", keyword));
0380     }
0381 }
0382 
0383 void TexDocDialog::slotResetSearch()
0384 {
0385     m_leKeywords->setText(QString());
0386     m_texdocs->clear();
0387     showToc(i18n("Table of Contents"), m_tocList, true);
0388 }
0389 
0390 void TexDocDialog::callSearch()
0391 {
0392     if(m_pbSearch->isEnabled()) {
0393         slotSearchClicked();
0394     }
0395 }
0396 
0397 ////////////////////// execute shell script //////////////////////
0398 
0399 void TexDocDialog::executeScript(const QString &command)
0400 {
0401     if (m_proc) {
0402         delete m_proc;
0403     }
0404 
0405     m_proc = new KProcess();
0406     m_proc->setShellCommand(command);
0407     m_proc->setOutputChannelMode(KProcess::MergedChannels);
0408     m_proc->setReadChannel(QProcess::StandardOutput);
0409     m_output.clear();
0410 
0411     connect(m_proc, &KProcess::readyReadStandardOutput,
0412             this, &TexDocDialog::slotProcessOutput);
0413     connect(m_proc, &KProcess::readyReadStandardError,
0414             this, &TexDocDialog::slotProcessOutput);
0415     connect(m_proc, static_cast<void (QProcess::*)(int,QProcess::ExitStatus)>(&QProcess::finished),
0416             this, &TexDocDialog::slotProcessExited);
0417 
0418     KILE_DEBUG_MAIN << "=== TexDocDialog::runShellScript() ====================" << Qt::endl;
0419     KILE_DEBUG_MAIN << "   execute: " << command << Qt::endl;
0420     m_proc->start();
0421 }
0422 
0423 void TexDocDialog::slotProcessOutput()
0424 {
0425     m_output += m_proc->readAll();
0426 }
0427 
0428 void TexDocDialog::slotProcessExited(int exitCode, QProcess::ExitStatus exitStatus)
0429 {
0430     Q_UNUSED(exitCode);
0431 
0432     if (exitStatus == QProcess::NormalExit) {
0433         //showFile(m_filename);
0434         emit(processFinished());
0435     }
0436     else {
0437         KMessageBox::error(this, i18n("<center>") + i18n("Could not determine the search paths of TexLive/teTeX or file 'texdoctk.dat'.<br/>"
0438                                                          "Unfortunately, Kile cannot show any useful information.") + i18n("</center>"), i18n("TexDoc Dialog"));
0439     }
0440 }
0441 
0442 ////////////////////// process slots, when finished //////////////////////
0443 
0444 void TexDocDialog::slotInitToc()
0445 {
0446     disconnect(this, &TexDocDialog::processFinished, this, &TexDocDialog::slotInitToc);
0447 
0448     QStringList results = m_output.split('\n', Qt::KeepEmptyParts);
0449     if (results.count() < 3) {
0450         KMessageBox::error(this, i18n("Could not determine the installation path of your TeX distribution or find the file 'texdoctk.dat'.<br/>"
0451                                       "Hence, we cannot provide you with an overview of the installed TeX documentation."));
0452         return;
0453     }
0454 
0455     m_texdoctkPath = results[0];
0456     m_texmfdocPath = results[1];
0457     m_texmfPath = results[2];
0458 
0459     KILE_DEBUG_MAIN << "\ttexdoctk path: " << m_texdoctkPath << Qt::endl;
0460     KILE_DEBUG_MAIN << "\ttexmfdoc path: " << m_texmfdocPath << Qt::endl;
0461     KILE_DEBUG_MAIN << "\ttexmf path: " << m_texmfPath << Qt::endl;
0462 
0463     if(m_texdoctkPath.indexOf('\n', -1) > -1) {
0464         m_texdoctkPath.truncate(m_texdoctkPath.length() - 1);
0465     }
0466 
0467     // read data and initialize listview
0468     readToc();
0469     slotResetSearch();
0470 }
0471 
0472 void TexDocDialog::slotShowFile()
0473 {
0474     disconnect(this, &TexDocDialog::processFinished, this, &TexDocDialog::slotShowFile);
0475     showFile(m_filename);
0476 }
0477 
0478 ////////////////////// Icon/Mime //////////////////////
0479 
0480 QString TexDocDialog::getMimeType(const QString &filename)
0481 {
0482     QFileInfo fi(filename);
0483     QString basename = fi.baseName().toLower();
0484     QString ext = fi.suffix().toLower();
0485 
0486     QString mimetype;
0487     if (ext == "txt" || ext == "faq" || ext == "sty" || basename == "readme" || basename == "00readme") {
0488         mimetype = "text/plain";
0489     }
0490     else {
0491         QUrl mimeurl;
0492         mimeurl.setPath(filename);
0493         QMimeDatabase db;
0494         QMimeType pMime = db.mimeTypeForUrl(mimeurl);
0495         mimetype = pMime.name();
0496     }
0497 
0498     KILE_DEBUG_MAIN << "\tmime = "  << mimetype << " " << Qt::endl;
0499     return mimetype;
0500 }
0501 
0502 QString TexDocDialog::getIconName(const QString &filename)
0503 {
0504     QFileInfo fi(filename);
0505     QString basename = fi.baseName().toLower();
0506     QString ext = fi.suffix().toLower();
0507 
0508     QString icon;
0509     if (ext == "application-x-bzdvi" ) { // FIXME exchange as soon as a real dvi icon is available
0510         icon = ext;
0511     }
0512     else if( ext == "htm" || ext == "html" ) {
0513         icon = "text-html";
0514     }
0515     else if(ext == "pdf" ) {
0516         icon = "application-pdf";
0517     }
0518     else if( ext == "txt") {
0519         icon = "text-plain";
0520     }
0521     else if(ext == "ps") {
0522         icon = "application-postscript";
0523     }
0524     else if(ext == "sty") {
0525         icon = "text-x-tex";
0526     }
0527     else if(ext == "faq" || basename == "readme" || basename == "00readme") {
0528         icon = "text-x-readme";
0529     }
0530     else {
0531         icon = "text-plain";
0532     }
0533     return icon;
0534 }
0535 
0536 }