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 }