File indexing completed on 2024-04-28 16:31:58

0001 /***************************************************************************
0002     Copyright (C) 2003-2020 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "entryview.h"
0026 #include "entry.h"
0027 #include "field.h"
0028 #include "translators/xslthandler.h"
0029 #include "translators/tellicoxmlexporter.h"
0030 #include "collection.h"
0031 #include "images/imagefactory.h"
0032 #include "images/imageinfo.h"
0033 #include "tellico_kernel.h"
0034 #include "utils/tellico_utils.h"
0035 #include "utils/datafileregistry.h"
0036 #include "core/filehandler.h"
0037 #include "config/tellico_config.h"
0038 #include "gui/drophandler.h"
0039 #include "utils/cursorsaver.h"
0040 #include "document.h"
0041 #include "tellico_debug.h"
0042 
0043 #include <KMessageBox>
0044 #include <KLocalizedString>
0045 #include <KStandardAction>
0046 
0047 #include <QFile>
0048 #include <QTextStream>
0049 #include <QClipboard>
0050 #include <QDomDocument>
0051 #include <QTemporaryFile>
0052 #include <QApplication>
0053 #include <QDesktopServices>
0054 #include <QMenu>
0055 
0056 #ifdef USE_KHTML
0057 #include <dom/dom_element.h>
0058 #else
0059 #include <QWebEnginePage>
0060 #include <QWebEngineSettings>
0061 #include <QPrinter>
0062 #include <QPrinterInfo>
0063 #include <QPrintDialog>
0064 #include <QEventLoop>
0065 #endif
0066 
0067 using Tellico::EntryView;
0068 
0069 #ifdef USE_KHTML
0070 using Tellico::EntryViewWidget;
0071 
0072 EntryViewWidget::EntryViewWidget(EntryView* part, QWidget* parent)
0073     : KHTMLView(part, parent) {}
0074 
0075 // for the life of me, I could not figure out how to call the actual
0076 // KHTMLPartBrowserExtension::copy() slot, so this will have to do
0077 void EntryViewWidget::copy() {
0078   QApplication::clipboard()->setText(part()->selectedText(), QClipboard::Clipboard);
0079 }
0080 
0081 void EntryViewWidget::changeEvent(QEvent* event_) {
0082   // this will delete and reread the default colors, assuming they changed
0083   if(event_->type() == QEvent::PaletteChange ||
0084      event_->type() == QEvent::FontChange ||
0085      event_->type() == QEvent::ApplicationFontChange) {
0086     static_cast<EntryView*>(part())->resetView();
0087   }
0088   KHTMLView::changeEvent(event_);
0089 }
0090 
0091 EntryView::EntryView(QWidget* parent_) : KHTMLPart(new EntryViewWidget(this, parent_), parent_),
0092     m_handler(nullptr), m_tempFile(nullptr), m_useGradientImages(true), m_checkCommonFile(true) {
0093   setJScriptEnabled(false);
0094   setJavaEnabled(false);
0095   setMetaRefreshEnabled(false);
0096   setPluginsEnabled(false);
0097   clear(); // needed for initial layout
0098 
0099   view()->setAcceptDrops(true);
0100   DropHandler* drophandler = new DropHandler(this);
0101   view()->installEventFilter(drophandler);
0102 
0103   connect(browserExtension(), &KParts::BrowserExtension::openUrlRequestDelayed,
0104           this, &EntryView::slotOpenURL);
0105 }
0106 #else
0107 using Tellico::EntryViewPage;
0108 
0109 EntryViewPage::EntryViewPage(QWidget* parent)
0110     : QWebEnginePage(parent) {
0111   settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, false);
0112   settings()->setAttribute(QWebEngineSettings::PluginsEnabled, false);
0113   settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true);
0114   settings()->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, true);
0115 }
0116 
0117 bool EntryViewPage::acceptNavigationRequest(const QUrl& url_, QWebEnginePage::NavigationType type_, bool isMainFrame_) {
0118   Q_UNUSED(isMainFrame_);
0119 
0120   if(url_.scheme() == QLatin1String("tc")) {
0121     // handle this internally
0122     emit signalTellicoAction(url_);
0123     return false;
0124   }
0125 
0126   if(type_ == QWebEnginePage::NavigationTypeLinkClicked) {
0127     // do not load this new url inside the entry view, return false
0128     openExternalLink(url_);
0129     return false;
0130   }
0131 
0132   return true;
0133 }
0134 
0135 // intercept window open commands, including target=_blank
0136 // see https://bugs.kde.org/show_bug.cgi?id=445871
0137 QWebEnginePage* EntryViewPage::createWindow(QWebEnginePage::WebWindowType type_) {
0138   Q_UNUSED(type_);
0139   auto page = new QWebEnginePage(this);
0140   connect(page, &QWebEnginePage::urlChanged, this, [this](const QUrl& u) {
0141     openExternalLink(u);
0142     auto page = static_cast<QWebEnginePage*>(sender());
0143     page->action(QWebEnginePage::Stop)->trigger(); // stop the loading, further is unneccesary
0144     page->deleteLater();
0145   });
0146   return page;
0147 }
0148 
0149 void EntryViewPage::openExternalLink(const QUrl& url_) {
0150   const QUrl finalUrl = Kernel::self()->URL().resolved(url_);
0151   QDesktopServices::openUrl(finalUrl);
0152 }
0153 
0154 EntryView::EntryView(QWidget* parent_) : QWebEngineView(parent_),
0155     m_handler(nullptr), m_tempFile(nullptr), m_useGradientImages(true), m_checkCommonFile(true) {
0156   EntryViewPage* page = new EntryViewPage(this);
0157   setPage(page);
0158   if(m_printer.resolution() < 300) {
0159     m_printer.setResolution(300);
0160   }
0161 
0162   connect(page, &EntryViewPage::signalTellicoAction,
0163           this, &EntryView::signalTellicoAction);
0164 
0165   setAcceptDrops(true);
0166   DropHandler* drophandler = new DropHandler(this);
0167   installEventFilter(drophandler);
0168 
0169   clear(); // needed for initial layout
0170 }
0171 #endif
0172 
0173 EntryView::~EntryView() {
0174   delete m_handler;
0175   m_handler = nullptr;
0176   delete m_tempFile;
0177   m_tempFile = nullptr;
0178 }
0179 
0180 void EntryView::clear() {
0181   m_entry = nullptr;
0182 
0183   // just clear the view
0184 #ifdef USE_KHTML
0185   begin();
0186   if(!m_textToShow.isEmpty()) {
0187     write(m_textToShow);
0188   }
0189   end();
0190   view()->layout(); // I need this because some of the margins and widths may get messed up
0191 #else
0192   setUrl(QUrl());
0193   if(!m_textToShow.isEmpty()) {
0194     // the welcome page references local images, which won't load when passing HTML directly
0195     // so the base Url needs to be set to file://
0196     // see https://bugreports.qt.io/browse/QTBUG-55902#comment-335945
0197     // passing "disable-web-security" to QApplication is another option
0198     page()->setHtml(m_textToShow, QUrl(QStringLiteral("file://")));
0199   }
0200 #endif
0201 }
0202 
0203 void EntryView::showEntries(Tellico::Data::EntryList entries_) {
0204   if(!entries_.isEmpty()) {
0205     showEntry(entries_.first());
0206   }
0207 }
0208 
0209 void EntryView::showEntry(Tellico::Data::EntryPtr entry_) {
0210   if(!entry_) {
0211     clear();
0212     return;
0213   }
0214 
0215   m_textToShow.clear();
0216   if(!m_handler || !m_handler->isValid()) {
0217     setXSLTFile(m_xsltFile);
0218   }
0219   if(!m_handler || !m_handler->isValid()) {
0220     myWarning() << "no xslt handler";
0221     return;
0222   }
0223 
0224   // check if the gradient images need to be written again which might be the case if the collection is different
0225   // and using local directories for storage
0226   if(entry_ && (!m_entry || m_entry->collection() != entry_->collection()) &&
0227      ImageFactory::cacheDir() == ImageFactory::LocalDir) {
0228     // use entry_ instead of m_entry since that's the new entry to show
0229     ImageFactory::createStyleImages(entry_->collection()->type());
0230   }
0231 
0232   m_entry = entry_;
0233 
0234   Export::TellicoXMLExporter exporter(m_entry->collection());
0235   exporter.setEntries(Data::EntryList() << m_entry);
0236   long opt = exporter.options();
0237   // verify images for the view
0238   opt |= Export::ExportVerifyImages;
0239   opt |= Export::ExportComplete;
0240   // use absolute links
0241   opt |= Export::ExportAbsoluteLinks;
0242   // on second thought, don't auto-format everything, just clean it
0243   if(m_entry->collection()->type() == Data::Collection::Bibtex) {
0244     opt |= Export::ExportClean;
0245   }
0246   exporter.setOptions(opt);
0247   QDomDocument dom = exporter.exportXML();
0248 
0249 //  myDebug() << dom.toString();
0250 #if 0
0251   myWarning() << "turn me off!";
0252   QFile f1(QLatin1String("/tmp/test.xml"));
0253   if(f1.open(QIODevice::WriteOnly)) {
0254     QTextStream t(&f1);
0255     t << dom.toString();
0256   }
0257   f1.close();
0258 #endif
0259 
0260   QString html = m_handler->applyStylesheet(dom.toString());
0261   // write out image files
0262   Data::FieldList fields = entry_->collection()->imageFields();
0263   foreach(Data::FieldPtr field, fields) {
0264     QString id = entry_->field(field);
0265     if(id.isEmpty()) {
0266       continue;
0267     }
0268     // only write out image if it's not linked only
0269     if(!ImageFactory::imageInfo(id).linkOnly) {
0270       if(Data::Document::self()->allImagesOnDisk()) {
0271         ImageFactory::writeCachedImage(id, ImageFactory::cacheDir());
0272       } else {
0273         ImageFactory::writeCachedImage(id, ImageFactory::TempDir);
0274       }
0275     }
0276   }
0277 
0278 #if 0
0279   myWarning() << "EntryView::showEntry() - turn me off!";
0280   QFile f2(QLatin1String("/tmp/test.html"));
0281   if(f2.open(QIODevice::WriteOnly)) {
0282     QTextStream t(&f2);
0283     t << html;
0284   }
0285   f2.close();
0286 #endif
0287 
0288 //  myDebug() << html;
0289 #ifdef USE_KHTML
0290   begin(QUrl::fromLocalFile(m_xsltFile));
0291   write(html);
0292   end();
0293   view()->layout(); // I need this because some of the margins and widths may get messed up
0294 #else
0295   // by setting the xslt file as the URL, any images referenced in the xslt "theme" can be found
0296   // by simply using a relative path in the xslt file
0297   page()->setHtml(html, QUrl::fromLocalFile(m_xsltFile));
0298 #endif
0299 }
0300 
0301 void EntryView::showText(const QString& text_) {
0302   m_textToShow = text_;
0303 #ifdef USE_KHTML
0304   begin();
0305   write(text_);
0306   end();
0307 #else
0308   clear(); // shows the default text
0309 #endif
0310 }
0311 
0312 void EntryView::setXSLTFile(const QString& file_) {
0313   if(file_.isEmpty()) {
0314     myWarning() << "empty xslt file";
0315     return;
0316   }
0317   QString oldFile = m_xsltFile;
0318   // if starts with slash, then absolute path
0319   if(file_.at(0) == QLatin1Char('/')) {
0320     m_xsltFile = file_;
0321   } else {
0322     const QString templateDir = QStringLiteral("entry-templates/");
0323     m_xsltFile = DataFileRegistry::self()->locate(templateDir + file_);
0324     if(m_xsltFile.isEmpty()) {
0325       if(!file_.isEmpty()) {
0326         myWarning() << "can't locate" << file_;
0327       }
0328       m_xsltFile = DataFileRegistry::self()->locate(templateDir + QLatin1String("Fancy.xsl"));
0329       if(m_xsltFile.isEmpty()) {
0330         QString str = QStringLiteral("<qt>");
0331         str += i18n("Tellico is unable to locate the default entry stylesheet.");
0332         str += QLatin1Char(' ');
0333         str += i18n("Please check your installation.");
0334         str += QLatin1String("</qt>");
0335 #ifdef USE_KHTML
0336         KMessageBox::error(view(), str);
0337 #else
0338         KMessageBox::error(this, str);
0339 #endif
0340         clear();
0341         return;
0342       }
0343     }
0344   }
0345 
0346   const int type = m_entry ? m_entry->collection()->type() : Kernel::self()->collectionType();
0347 
0348   // we need to know if the colors changed from last time, in case
0349   // we need to do that ugly hack to reload the cache
0350   bool reloadImages = m_useGradientImages;
0351   // if m_useGradientImages is false, then we don't even need to check
0352   // if there's no handler, there there's _no way_ to check
0353   if(m_handler && reloadImages) {
0354     // the only two colors that matter for the gradients are the base color
0355     // and highlight base color
0356     QByteArray oldBase = m_handler->param("bgcolor");
0357     QByteArray oldHigh = m_handler->param("color2");
0358     // remember the string params have apostrophes on either side, so we can start search at pos == 1
0359     reloadImages = oldBase.indexOf(Config::templateBaseColor(type).name().toLatin1(), 1) == -1
0360                 || oldHigh.indexOf(Config::templateHighlightedBaseColor(type).name().toLatin1(), 1) == -1;
0361   }
0362 
0363   if(!m_handler || m_xsltFile != oldFile) {
0364     delete m_handler;
0365     // must read the file name to get proper context
0366     m_handler = new XSLTHandler(QFile::encodeName(m_xsltFile));
0367     if(m_checkCommonFile && !m_handler->isValid()) {
0368       Tellico::checkCommonXSLFile();
0369       m_checkCommonFile = false;
0370       delete m_handler;
0371       m_handler = new XSLTHandler(QFile::encodeName(m_xsltFile));
0372     }
0373     if(!m_handler->isValid()) {
0374       myWarning() << "invalid xslt handler";
0375       clear();
0376       delete m_handler;
0377       m_handler = nullptr;
0378       return;
0379     }
0380   }
0381 
0382   m_handler->addStringParam("font",     Config::templateFont(type).family().toLatin1());
0383   m_handler->addStringParam("fontsize", QByteArray().setNum(Config::templateFont(type).pointSize()));
0384   m_handler->addStringParam("bgcolor",  Config::templateBaseColor(type).name().toLatin1());
0385   m_handler->addStringParam("fgcolor",  Config::templateTextColor(type).name().toLatin1());
0386   m_handler->addStringParam("color1",   Config::templateHighlightedTextColor(type).name().toLatin1());
0387   m_handler->addStringParam("color2",   Config::templateHighlightedBaseColor(type).name().toLatin1());
0388 
0389   if(Data::Document::self()->allImagesOnDisk()) {
0390     m_handler->addStringParam("imgdir", QUrl::fromLocalFile(ImageFactory::imageDir()).toEncoded());
0391   } else {
0392     m_handler->addStringParam("imgdir", QUrl::fromLocalFile(ImageFactory::tempDir()).toEncoded());
0393   }
0394   m_handler->addStringParam("datadir", QUrl::fromLocalFile(Tellico::installationDir()).toEncoded());
0395 
0396   // if we don't have to reload the images, then just show the entry and we're done
0397   if(reloadImages) {
0398     // now, have to recreate images and refresh khtml cache
0399     resetColors();
0400   } else {
0401     showEntry(m_entry);
0402   }
0403 }
0404 
0405 void EntryView::copy() {
0406 #ifndef USE_KHTML
0407   pageAction(QWebEnginePage::Copy)->trigger();
0408 #endif
0409 }
0410 
0411 void EntryView::slotRefresh() {
0412   setXSLTFile(m_xsltFile);
0413   showEntry(m_entry);
0414 #ifdef USE_KHTML
0415   view()->repaint();
0416 #endif
0417 }
0418 
0419 // do some contortions in case the url is relative
0420 // need to interpret it relative to document URL instead of xslt file
0421 // the current node under the mouse would be the text node inside
0422 // the anchor node, so iterate up the parents
0423 void EntryView::slotOpenURL(const QUrl& url_) {
0424 #ifdef USE_KHTML
0425   if(url_.scheme() == QLatin1String("tc")) {
0426     // handle this internally
0427     emit signalTellicoAction(url_);
0428     return;
0429   }
0430 
0431   QUrl u = url_;
0432   for(DOM::Node node = nodeUnderMouse(); !node.isNull(); node = node.parentNode()) {
0433     if(node.nodeType() == DOM::Node::ELEMENT_NODE && static_cast<DOM::Element>(node).tagName() == "a") {
0434       QString href = static_cast<DOM::Element>(node).getAttribute("href").string();
0435       if(!href.isEmpty()) {
0436         // interpret url relative to document url
0437         u = Kernel::self()->URL().resolved(QUrl(href));
0438       }
0439       break;
0440     }
0441   }
0442   // open the url
0443   QDesktopServices::openUrl(u);
0444 #else
0445   Q_UNUSED(url_);
0446 #endif
0447 }
0448 
0449 void EntryView::changeEvent(QEvent* event_) {
0450 #ifdef USE_KHTML
0451   Q_UNUSED(event_);
0452 #else
0453   // this will delete and reread the default colors, assuming they changed
0454   if(event_->type() == QEvent::PaletteChange ||
0455      event_->type() == QEvent::FontChange ||
0456      event_->type() == QEvent::ApplicationFontChange) {
0457     resetView();
0458   }
0459   QWebEngineView::changeEvent(event_);
0460 #endif
0461 }
0462 
0463 void EntryView::slotReloadEntry() {
0464   // this slot should only be connected in setXSLTFile()
0465   // must disconnect the signal first, otherwise, get an infinite loop
0466 #ifdef USE_KHTML
0467   void (EntryView::* completed)() = &EntryView::completed;
0468   disconnect(this, completed, this, &EntryView::slotReloadEntry);
0469   closeUrl(); // this is needed to stop everything, for some reason
0470   view()->setUpdatesEnabled(true);
0471 #else
0472   disconnect(this, &EntryView::loadFinished, this, &EntryView::slotReloadEntry);
0473   setUpdatesEnabled(true);
0474 #endif
0475 
0476   if(m_entry) {
0477     showEntry(m_entry);
0478   } else {
0479     // setXSLTFile() writes some html to clear the image cache
0480     // but we don't want to see that, so just clear everything
0481     clear();
0482   }
0483   delete m_tempFile;
0484   m_tempFile = nullptr;
0485 }
0486 
0487 void EntryView::addXSLTStringParam(const QByteArray& name_, const QByteArray& value_) {
0488   if(!m_handler) {
0489     return;
0490   }
0491   m_handler->addStringParam(name_, value_);
0492 }
0493 
0494 void EntryView::setXSLTOptions(const Tellico::StyleOptions& opt_) {
0495   if(!m_handler) {
0496     return;
0497   }
0498   m_handler->addStringParam("font",     opt_.fontFamily.toLatin1());
0499   m_handler->addStringParam("fontsize", QByteArray().setNum(opt_.fontSize));
0500   m_handler->addStringParam("bgcolor",  opt_.baseColor.name().toLatin1());
0501   m_handler->addStringParam("fgcolor",  opt_.textColor.name().toLatin1());
0502   m_handler->addStringParam("color1",   opt_.highlightedTextColor.name().toLatin1());
0503   m_handler->addStringParam("color2",   opt_.highlightedBaseColor.name().toLatin1());
0504   m_handler->addStringParam("imgdir",   QFile::encodeName(opt_.imgDir));
0505 }
0506 
0507 void EntryView::resetView() {
0508   delete m_handler;
0509   m_handler = nullptr;
0510   // Many of the template style parameters use default values. The only way that
0511   // KConfigSkeleton can be updated is to delete the existing config object, which will then be recreated
0512   delete Config::self();
0513   setXSLTFile(m_xsltFile); // this ends up calling resetColors()
0514 }
0515 
0516 void EntryView::resetColors() {
0517   // recreate gradients
0518   ImageFactory::createStyleImages(m_entry ? m_entry->collection()->type() : Data::Collection::Base);
0519 
0520   QString dir = m_handler ? QFile::decodeName(m_handler->param("imgdir")) : QString();
0521   if(dir.isEmpty()) {
0522     dir = Data::Document::self()->allImagesOnDisk() ? ImageFactory::imageDir() : ImageFactory::tempDir();
0523   } else {
0524     // it's a string param, so it has quotes on both sides
0525     dir = dir.mid(1);
0526     dir.truncate(dir.length()-1);
0527   }
0528 
0529   delete m_tempFile;
0530   m_tempFile = new QTemporaryFile();
0531   if(!m_tempFile->open()) {
0532     myDebug() << "failed to open temp file";
0533     delete m_tempFile;
0534     m_tempFile = nullptr;
0535     return;
0536   }
0537 
0538   // this is a rather bad hack to get around the fact that the image cache is not reloaded when
0539   // the gradient files are changed on disk. Setting the URLArgs for write() calls doesn't seem to
0540   // work. So force a reload with a temp file, then catch the completed signal and repaint
0541   QString s = QStringLiteral("<html><body><img src=\"%1\"><img src=\"%2\"></body></html>")
0542                              .arg(dir + QLatin1String("gradient_bg.png"),
0543                                   dir + QLatin1String("gradient_header.png"));
0544   QTextStream stream(m_tempFile);
0545   stream << s;
0546   stream.flush();
0547 
0548 #ifdef USE_KHTML
0549   KParts::OpenUrlArguments args = arguments();
0550   args.setReload(true); // tell the cache to reload images
0551   setArguments(args);
0552 
0553   view()->setUpdatesEnabled(false);
0554   openUrl(QUrl::fromLocalFile(m_tempFile->fileName()));
0555   void (EntryView::* completed)() = &EntryView::completed;
0556   connect(this, completed, this, &EntryView::slotReloadEntry);
0557 #else
0558   // don't flicker
0559   setUpdatesEnabled(false);
0560   load(QUrl::fromLocalFile(m_tempFile->fileName()));
0561   connect(this, &EntryView::loadFinished, this, &EntryView::slotReloadEntry);
0562 #endif
0563 }
0564 
0565 void EntryView::contextMenuEvent(QContextMenuEvent* event_) {
0566 #ifdef USE_KHTML
0567   Q_UNUSED(event_);
0568 #else
0569   QMenu menu(this);
0570   // can't use the KStandardAction for copy since I don't know what the receiver or trigger target is
0571   QAction* standardCopy = KStandardAction::copy(nullptr, nullptr, &menu);
0572   QAction* pageCopyAction = pageAction(QWebEnginePage::Copy);
0573   pageCopyAction->setIcon(standardCopy->icon());
0574   menu.addAction(pageCopyAction);
0575 
0576   QAction* printAction = KStandardAction::print(this, &EntryView::slotPrint, this);
0577   // remove shortcut since this is specific to the view widget
0578   printAction->setShortcut(QKeySequence());
0579   menu.addAction(printAction);
0580   menu.exec(event_->globalPos());
0581 #endif
0582 }
0583 
0584 void EntryView::slotPrint() {
0585 #ifndef USE_KHTML
0586   QPointer<QPrintDialog> dialog = new QPrintDialog(&m_printer, this);
0587   if(dialog->exec() != QDialog::Accepted) {
0588     return;
0589   }
0590   Tellico::GUI::CursorSaver cs(Qt::WaitCursor);
0591   page()->print(&m_printer, [](bool) {});
0592 #endif
0593 }