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