File indexing completed on 2024-04-28 05:08:26

0001 /***************************************************************************
0002     Copyright (C) 2005-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 <config.h>
0026 #include "reportdialog.h"
0027 #include "translators/htmlexporter.h"
0028 #include "images/imagefactory.h"
0029 #include "tellico_kernel.h"
0030 #include "collection.h"
0031 #include "document.h"
0032 #include "entry.h"
0033 #include "controller.h"
0034 #include "tellico_debug.h"
0035 #include "gui/combobox.h"
0036 #include "utils/cursorsaver.h"
0037 #include "utils/datafileregistry.h"
0038 #include "utils/tellico_utils.h"
0039 #include "config/tellico_config.h"
0040 #ifdef HAVE_QCHARTS
0041 #include "charts/chartmanager.h"
0042 #include "charts/chartreport.h"
0043 #endif
0044 
0045 #include <KLocalizedString>
0046 #include <KStandardGuiItem>
0047 #include <KWindowConfig>
0048 #include <KConfigGroup>
0049 
0050 #include <QFile>
0051 #include <QLabel>
0052 #include <QFileInfo>
0053 #include <QTimer>
0054 #include <QTextStream>
0055 #include <QHBoxLayout>
0056 #include <QVBoxLayout>
0057 #include <QPushButton>
0058 #include <QFileDialog>
0059 #include <QDialogButtonBox>
0060 #include <QStackedWidget>
0061 #include <QScrollArea>
0062 #include <QPainter>
0063 #include <QPrinter>
0064 #include <QPrintDialog>
0065 #include <QTemporaryFile>
0066 
0067 #ifdef USE_KHTML
0068 #include <KHTMLPart>
0069 #include <KHTMLView>
0070 #else
0071 #include <QWebEngineView>
0072 #include <QWebEnginePage>
0073 #include <QWebEngineSettings>
0074 #endif
0075 
0076 namespace {
0077   static const int REPORT_MIN_WIDTH = 600;
0078   static const int REPORT_MIN_HEIGHT = 420;
0079   static const char* dialogOptionsString = "Report Dialog Options";
0080   static const int INDEX_HTML = 0;
0081   static const int INDEX_CHART = 1;
0082   static const int ALL_ENTRIES = -1;
0083 }
0084 
0085 using Tellico::ReportDialog;
0086 
0087 // default button is going to be used as a print button, so it's separated
0088 ReportDialog::ReportDialog(QWidget* parent_)
0089     : QDialog(parent_), m_exporter(nullptr), m_tempFile(nullptr) {
0090   setModal(false);
0091   setWindowTitle(i18n("Collection Report"));
0092 
0093   QWidget* mainWidget = new QWidget(this);
0094   QVBoxLayout* mainLayout = new QVBoxLayout();
0095   setLayout(mainLayout);
0096   mainLayout->addWidget(mainWidget);
0097 
0098   QVBoxLayout* topLayout = new QVBoxLayout(mainWidget);
0099 
0100   QBoxLayout* hlay = new QHBoxLayout();
0101   topLayout->addLayout(hlay);
0102   QLabel* l = new QLabel(i18n("&Report template:"), mainWidget);
0103   hlay->addWidget(l);
0104 
0105   QStringList files = Tellico::locateAllFiles(QStringLiteral("tellico/report-templates/*.xsl"));
0106   QMap<QString, QVariant> templates; // gets sorted by title
0107   foreach(const QString& file, files) {
0108     QFileInfo fi(file);
0109     const QString lfile = fi.fileName();
0110     // the Group Summary report template doesn't work with QWebView
0111 #ifndef USE_KHTML
0112     if(lfile == QStringLiteral("Group_Summary.xsl")) {
0113       continue;
0114     }
0115 #endif
0116     QString name = lfile.section(QLatin1Char('.'), 0, -2);
0117     name.replace(QLatin1Char('_'), QLatin1Char(' '));
0118     QString title = i18nc((name + QLatin1String(" XSL Template")).toUtf8().constData(), name.toUtf8().constData());
0119     templates.insert(title, lfile);
0120   }
0121 #ifdef HAVE_QCHARTS
0122   // add the chart reports
0123   foreach(const auto& report, ChartManager::self()->allReports()) {
0124     templates.insert(report->title(), report->uuid());
0125   }
0126 #endif
0127   // special case for concatenating all entry templates
0128   templates.insert(i18n("One Entry Per Page"), ALL_ENTRIES);
0129 
0130   m_templateCombo = new GUI::ComboBox(mainWidget);
0131   for(auto it = templates.constBegin(); it != templates.constEnd(); ++it) {
0132     const bool isChart = static_cast<QMetaType::Type>(it.value().type()) == QMetaType::QUuid;
0133     m_templateCombo->addItem(QIcon::fromTheme(isChart ? QStringLiteral("kchart") : QStringLiteral("text-rdf")),
0134                              it.key(), it.value());
0135   }
0136   hlay->addWidget(m_templateCombo);
0137   l->setBuddy(m_templateCombo);
0138 
0139   QPushButton* pb1 = new QPushButton(mainWidget);
0140   KGuiItem::assign(pb1, KGuiItem(i18n("&Generate"), QStringLiteral("application-x-executable")));
0141   hlay->addWidget(pb1);
0142   connect(pb1, &QAbstractButton::clicked, this, &ReportDialog::slotGenerate);
0143 
0144   hlay->addStretch();
0145 
0146   QPushButton* pb2 = new QPushButton(mainWidget);
0147   KGuiItem::assign(pb2, KStandardGuiItem::saveAs());
0148   hlay->addWidget(pb2);
0149   connect(pb2, &QAbstractButton::clicked, this, &ReportDialog::slotSaveAs);
0150 
0151   QPushButton* pb3 = new QPushButton(mainWidget);
0152   KGuiItem::assign(pb3, KStandardGuiItem::print());
0153   hlay->addWidget(pb3);
0154   connect(pb3, &QAbstractButton::clicked, this, &ReportDialog::slotPrint);
0155 
0156   QColor color = palette().color(QPalette::Link);
0157   QString text = QString::fromLatin1("<html><style>p{font-family:sans-serif;font-weight:bold;width:50%;"
0158                                      "margin:20% auto auto auto;text-align:center;"
0159                                      "color:%1;}</style><body><p>").arg(color.name())
0160                + i18n("Select a report template and click <em>Generate</em>.") + QLatin1Char(' ')
0161                + i18n("Some reports may take several seconds to generate for large collections.")
0162                + QLatin1String("</p></body></html>");
0163 
0164   m_reportView = new QStackedWidget(mainWidget);
0165   topLayout->addWidget(m_reportView);
0166 
0167 #ifdef USE_KHTML
0168   m_HTMLPart = new KHTMLPart(m_reportView);
0169   m_HTMLPart->setJScriptEnabled(true);
0170   m_HTMLPart->setJavaEnabled(false);
0171   m_HTMLPart->setMetaRefreshEnabled(false);
0172   m_HTMLPart->setPluginsEnabled(false);
0173 
0174   m_HTMLPart->begin();
0175   m_HTMLPart->write(text);
0176   m_HTMLPart->end();
0177   m_reportView->insertWidget(INDEX_HTML, m_HTMLPart->view());
0178 #else
0179   m_webView = new QWebEngineView(m_reportView);
0180   connect(m_webView, &QWebEngineView::loadFinished, this, [](bool b) {
0181     if(!b) myDebug() << "ReportDialog - failed to load view";
0182   });
0183   QWebEngineSettings* settings = m_webView->page()->settings();
0184   settings->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
0185   settings->setAttribute(QWebEngineSettings::PluginsEnabled, false);
0186   settings->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true);
0187   settings->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, true);
0188 
0189   m_webView->setHtml(text);
0190   m_reportView->insertWidget(INDEX_HTML, m_webView);
0191 #endif
0192 
0193   QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
0194   mainLayout->addWidget(buttonBox);
0195   connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
0196   connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0197   buttonBox->button(QDialogButtonBox::Close)->setDefault(true);
0198 
0199   setMinimumWidth(qMax(minimumWidth(), REPORT_MIN_WIDTH));
0200   setMinimumHeight(qMax(minimumHeight(), REPORT_MIN_HEIGHT));
0201 
0202   QTimer::singleShot(0, this, &ReportDialog::slotUpdateSize);
0203 }
0204 
0205 ReportDialog::~ReportDialog() {
0206   delete m_exporter;
0207   m_exporter = nullptr;
0208   delete m_tempFile;
0209   m_tempFile = nullptr;
0210 
0211   KConfigGroup config(KSharedConfig::openConfig(), QLatin1String(dialogOptionsString));
0212   KWindowConfig::saveWindowSize(windowHandle(), config);
0213 }
0214 
0215 void ReportDialog::slotGenerate() {
0216   GUI::CursorSaver cs(Qt::WaitCursor);
0217 
0218   QVariant curData = m_templateCombo->currentData();
0219   if(static_cast<QMetaType::Type>(curData.type()) == QMetaType::QUuid) {
0220     generateChart();
0221     m_reportView->setCurrentIndex(INDEX_CHART);
0222   } else if(curData == ALL_ENTRIES) {
0223     generateAllEntries();
0224     m_reportView->setCurrentIndex(INDEX_HTML);
0225   } else {
0226     generateHtml();
0227     m_reportView->setCurrentIndex(INDEX_HTML);
0228   }
0229 }
0230 
0231 void ReportDialog::generateChart() {
0232 #ifdef HAVE_QCHARTS
0233   const QUuid uuid = m_templateCombo->currentData().toUuid();
0234   auto oldWidget = m_reportView->widget(INDEX_CHART);
0235   auto newWidget = ChartManager::self()->report(uuid)->createWidget();
0236   if(newWidget) {
0237     m_reportView->insertWidget(INDEX_CHART, newWidget);
0238   }
0239   if(oldWidget) {
0240     m_reportView->removeWidget(oldWidget);
0241     delete oldWidget;
0242   }
0243 #endif
0244 }
0245 
0246 void ReportDialog::generateHtml() {
0247   QString fileName = QLatin1String("report-templates/") + m_templateCombo->currentData().toString();
0248   QString xsltFile = DataFileRegistry::self()->locate(fileName);
0249   if(xsltFile.isEmpty()) {
0250     myWarning() << "can't locate " << m_templateCombo->currentData().toString();
0251     return;
0252   }
0253   // if it's the same XSL file, no need to reload the XSLTHandler, just refresh
0254   if(xsltFile == m_xsltFile) {
0255     slotRefresh();
0256     return;
0257   }
0258 
0259   m_xsltFile = xsltFile;
0260 
0261   delete m_exporter;
0262   m_exporter = new Export::HTMLExporter(Data::Document::self()->collection());
0263   m_exporter->setXSLTFile(m_xsltFile);
0264   m_exporter->setPrintHeaders(false); // the templates should take care of this themselves
0265   m_exporter->setPrintGrouped(true); // allow templates to take advantage of added DOM
0266 
0267   slotRefresh();
0268 }
0269 
0270 void ReportDialog::generateAllEntries() {
0271   auto coll = Data::Document::self()->collection();
0272   QString entryXSLTFile = Config::templateName(coll);
0273   QString xsltFile = DataFileRegistry::self()->locate(QLatin1String("entry-templates/") +
0274                                                       entryXSLTFile + QLatin1String(".xsl"));
0275   if(xsltFile.isEmpty()) {
0276     myWarning() << "can't locate " << entryXSLTFile << ".xsl";
0277     return;
0278   }
0279   m_xsltFile = xsltFile;
0280 
0281   delete m_exporter;
0282   m_exporter = new Export::HTMLExporter(coll);
0283   m_exporter->setXSLTFile(m_xsltFile);
0284   m_exporter->setPrintHeaders(false); // the templates should take care of this themselves
0285   m_exporter->setPrintGrouped(true); // allow templates to take advantage of added DOM
0286   m_exporter->setGroupBy(Controller::self()->expandedGroupBy());
0287   m_exporter->setSortTitles(Controller::self()->sortTitles());
0288   m_exporter->setColumns(Controller::self()->visibleColumns());
0289   long options = Export::ExportUTF8 | Export::ExportComplete | Export::ExportImages;
0290   if(Config::autoFormat()) {
0291     options |= Export::ExportFormatted;
0292   }
0293   m_exporter->setOptions(options);
0294 
0295   if(Controller::self()->visibleEntries().isEmpty()) {
0296     slotRefresh();
0297     return;
0298   }
0299 
0300   // do some surgery on the HTML since we've got <html> elements in every page
0301   static const QRegularExpression bodyRx(QLatin1String("<body[^>]*>"));
0302   m_exporter->setEntries(Controller::self()->visibleEntries());
0303   QString html = m_exporter->text();
0304   auto bodyMatch = bodyRx.match(html);
0305   Q_ASSERT(bodyMatch.hasMatch());
0306   const QString htmlStart = html.left(bodyMatch.capturedStart() + bodyMatch.capturedLength());
0307   const QString htmlEnd = html.mid(html.lastIndexOf(QLatin1String("</body")));
0308   const QString htmlBetween = QStringLiteral("<p style=\"page-break-after: always;\">&nbsp;</p>"
0309                                              "<p style=\"page-break-before: always;\">&nbsp;</p>");
0310 
0311   // estimate how much space in the string to reserve
0312   const auto estimatedSize = Controller::self()->visibleEntries().size() * html.size();
0313   html = htmlStart;
0314   html.reserve(1.1*estimatedSize);
0315   foreach(Data::EntryPtr entry, Controller::self()->visibleEntries()) {
0316     m_exporter->setEntries(Data::EntryList() << entry);
0317     QString fullText = m_exporter->text();
0318     // extract the body portion
0319     auto bodyMatch = bodyRx.match(fullText);
0320     if(bodyMatch.hasMatch()) {
0321       const auto bodyEnd = fullText.lastIndexOf(QLatin1String("</body"));
0322       const auto bodyLength = bodyEnd - bodyMatch.capturedEnd();
0323       html += fullText.midRef(bodyMatch.capturedEnd(), bodyLength) + htmlBetween;
0324     }
0325   }
0326   html += htmlEnd;
0327   m_exporter->setCustomHtml(html);
0328   showText(html, QUrl::fromLocalFile(m_xsltFile));
0329 }
0330 
0331 void ReportDialog::slotRefresh() {
0332   if(!m_exporter) {
0333     myWarning() << "no exporter";
0334     return;
0335   }
0336 
0337   m_exporter->setGroupBy(Controller::self()->expandedGroupBy());
0338   m_exporter->setSortTitles(Controller::self()->sortTitles());
0339   m_exporter->setColumns(Controller::self()->visibleColumns());
0340   // only print visible entries
0341   m_exporter->setEntries(Controller::self()->visibleEntries());
0342 
0343   long options = Export::ExportUTF8 | Export::ExportComplete | Export::ExportImages;
0344   if(Config::autoFormat()) {
0345     options |= Export::ExportFormatted;
0346   }
0347   m_exporter->setOptions(options);
0348   // by setting the xslt file as the URL, any images referenced in the xslt "theme" can be found
0349   // by simply using a relative path in the xslt file
0350   showText(m_exporter->text(), QUrl::fromLocalFile(m_xsltFile));
0351 }
0352 
0353 void ReportDialog::showText(const QString& text_, const QUrl& url_) {
0354 #ifdef USE_KHTML
0355   m_HTMLPart->begin(url_);
0356   m_HTMLPart->write(text_);
0357   m_HTMLPart->end();
0358 #else
0359   // limit is 2 MB after percent encoding, etc., so give some padding
0360   if(text_.size() > 1200000) {
0361     delete m_tempFile;
0362     m_tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/tellicoreport_XXXXXX") + QLatin1String(".html"));
0363     m_tempFile->open();
0364     QTextStream stream(m_tempFile);
0365     stream.setCodec("UTF-8");
0366     stream << text_;
0367     m_webView->load(QUrl::fromLocalFile(m_tempFile->fileName()));
0368   } else {
0369     m_webView->setHtml(text_, url_);
0370   }
0371 #endif
0372 #if 0
0373   myDebug() << "Remove debug from reportdialog.cpp";
0374   QFile f(QLatin1String("/tmp/test.html"));
0375   if(f.open(QIODevice::WriteOnly)) {
0376     QTextStream t(&f);
0377     t << text_;
0378   }
0379   f.close();
0380 #endif
0381 }
0382 
0383 // actually the print button
0384 void ReportDialog::slotPrint() {
0385   if(m_reportView->currentIndex() == INDEX_CHART) {
0386     QPrinter printer;
0387     printer.setResolution(600);
0388     QPointer<QPrintDialog> dialog = new QPrintDialog(&printer, this);
0389     if(dialog->exec() == QDialog::Accepted) {
0390       QWidget* widget = m_reportView->currentWidget();
0391       // there might be a widget inside a scroll area
0392       if(QScrollArea* scrollArea = qobject_cast<QScrollArea*>(widget)) {
0393         widget = scrollArea->widget();
0394       }
0395       QPainter painter;
0396       painter.begin(&printer);
0397       auto const paintRect = printer.pageLayout().paintRectPixels(printer.resolution());
0398       const double xscale = paintRect.width() / double(widget->width());
0399       const double yscale = paintRect.height() / double(widget->height());
0400       const double scale = 0.95*qMin(xscale, yscale);
0401       auto const paperRect = printer.pageLayout().fullRectPixels(printer.resolution());
0402       painter.translate(paperRect.center());
0403       painter.scale(scale, scale);
0404       painter.translate(-widget->width()/2, -widget->height()/2);
0405       widget->render(&painter);
0406     }
0407   } else {
0408 #ifdef USE_KHTML
0409     m_HTMLPart->view()->print();
0410 #else
0411     QPrinter printer;
0412     printer.setResolution(300);
0413     QPointer<QPrintDialog> dialog = new QPrintDialog(&printer, this);
0414     if(dialog->exec() == QDialog::Accepted) {
0415       QEventLoop loop;
0416       GUI::CursorSaver cs(Qt::WaitCursor);
0417       m_webView->page()->print(&printer, [&](bool) { loop.quit(); });
0418       loop.exec();
0419     }
0420 #endif
0421   }
0422 }
0423 
0424 void ReportDialog::slotSaveAs() {
0425   if(m_reportView->currentIndex() == INDEX_CHART) {
0426     QString filter = i18n("PNG Files") + QLatin1String(" (*.png)")
0427                   + QLatin1String(";;")
0428                   + i18n("All Files") + QLatin1String(" (*)");
0429     QUrl u = QFileDialog::getSaveFileUrl(this, QString(), QUrl(), filter);
0430     if(!u.isEmpty() && u.isValid()) {
0431       QWidget* widget = m_reportView->currentWidget();
0432       // there might be a widget inside a scroll area
0433       if(QScrollArea* scrollArea = qobject_cast<QScrollArea*>(widget)) {
0434         widget = scrollArea->widget();
0435       }
0436       QPixmap pixmap(widget->size());
0437       widget->render(&pixmap);
0438       pixmap.save(u.toLocalFile());
0439     }
0440   } else if(m_exporter) {
0441     QString filter = i18n("HTML Files") + QLatin1String(" (*.html)")
0442                   + QLatin1String(";;")
0443                   + i18n("All Files") + QLatin1String(" (*)");
0444     QUrl u = QFileDialog::getSaveFileUrl(this, QString(), QUrl(), filter);
0445     if(!u.isEmpty() && u.isValid()) {
0446       KConfigGroup config(KSharedConfig::openConfig(), "ExportOptions");
0447       bool encode = config.readEntry("EncodeUTF8", true);
0448       long oldOpt = m_exporter->options();
0449 
0450       // turn utf8 off
0451       long options = oldOpt & ~Export::ExportUTF8;
0452       // now turn it on if true
0453       if(encode) {
0454         options |= Export::ExportUTF8;
0455       }
0456 
0457       QUrl oldURL = m_exporter->url();
0458       m_exporter->setOptions(options);
0459       m_exporter->setURL(u);
0460 
0461       m_exporter->exec();
0462 
0463       m_exporter->setURL(oldURL);
0464       m_exporter->setOptions(oldOpt);
0465     }
0466   }
0467 }
0468 
0469 void ReportDialog::slotUpdateSize() {
0470   KConfigGroup config(KSharedConfig::openConfig(), QLatin1String(dialogOptionsString));
0471   KWindowConfig::restoreWindowSize(windowHandle(), config);
0472 }