File indexing completed on 2024-04-28 16:32:04

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 }
0083 
0084 using Tellico::ReportDialog;
0085 
0086 // default button is going to be used as a print button, so it's separated
0087 ReportDialog::ReportDialog(QWidget* parent_)
0088     : QDialog(parent_), m_exporter(nullptr), m_tempFile(nullptr) {
0089   setModal(false);
0090   setWindowTitle(i18n("Collection Report"));
0091 
0092   QWidget* mainWidget = new QWidget(this);
0093   QVBoxLayout* mainLayout = new QVBoxLayout();
0094   setLayout(mainLayout);
0095   mainLayout->addWidget(mainWidget);
0096 
0097   QVBoxLayout* topLayout = new QVBoxLayout(mainWidget);
0098 
0099   QBoxLayout* hlay = new QHBoxLayout();
0100   topLayout->addLayout(hlay);
0101   QLabel* l = new QLabel(i18n("&Report template:"), mainWidget);
0102   hlay->addWidget(l);
0103 
0104   QStringList files = Tellico::locateAllFiles(QStringLiteral("tellico/report-templates/*.xsl"));
0105   QMap<QString, QVariant> templates; // gets sorted by title
0106   foreach(const QString& file, files) {
0107     QFileInfo fi(file);
0108     const QString lfile = fi.fileName();
0109     // the Group Summary report template doesn't work with QWebView
0110 #ifndef USE_KHTML
0111     if(lfile == QStringLiteral("Group_Summary.xsl")) {
0112       continue;
0113     }
0114 #endif
0115     QString name = lfile.section(QLatin1Char('.'), 0, -2);
0116     name.replace(QLatin1Char('_'), QLatin1Char(' '));
0117     QString title = i18nc((name + QLatin1String(" XSL Template")).toUtf8().constData(), name.toUtf8().constData());
0118     templates.insert(title, lfile);
0119   }
0120 #ifdef HAVE_QCHARTS
0121   // add the chart reports
0122   foreach(const auto& report, ChartManager::self()->allReports()) {
0123     templates.insert(report->title(), report->uuid());
0124   }
0125 #endif
0126 
0127   m_templateCombo = new GUI::ComboBox(mainWidget);
0128   for(auto it = templates.constBegin(); it != templates.constEnd(); ++it) {
0129     const bool isChart = static_cast<QMetaType::Type>(it.value().type()) == QMetaType::QUuid;
0130     m_templateCombo->addItem(QIcon::fromTheme(isChart ? QStringLiteral("kchart") : QStringLiteral("text-rdf")),
0131                              it.key(), it.value());
0132   }
0133   hlay->addWidget(m_templateCombo);
0134   l->setBuddy(m_templateCombo);
0135 
0136   QPushButton* pb1 = new QPushButton(mainWidget);
0137   KGuiItem::assign(pb1, KGuiItem(i18n("&Generate"), QStringLiteral("application-x-executable")));
0138   hlay->addWidget(pb1);
0139   connect(pb1, &QAbstractButton::clicked, this, &ReportDialog::slotGenerate);
0140 
0141   hlay->addStretch();
0142 
0143   QPushButton* pb2 = new QPushButton(mainWidget);
0144   KGuiItem::assign(pb2, KStandardGuiItem::saveAs());
0145   hlay->addWidget(pb2);
0146   connect(pb2, &QAbstractButton::clicked, this, &ReportDialog::slotSaveAs);
0147 
0148   QPushButton* pb3 = new QPushButton(mainWidget);
0149   KGuiItem::assign(pb3, KStandardGuiItem::print());
0150   hlay->addWidget(pb3);
0151   connect(pb3, &QAbstractButton::clicked, this, &ReportDialog::slotPrint);
0152 
0153   QColor color = palette().color(QPalette::Link);
0154   QString text = QString::fromLatin1("<html><style>p{font-family:sans-serif;font-weight:bold;width:50%;"
0155                                      "margin:20% auto auto auto;text-align:center;"
0156                                      "color:%1;}</style><body><p>").arg(color.name())
0157                + i18n("Select a report template and click <em>Generate</em>.") + QLatin1Char(' ')
0158                + i18n("Some reports may take several seconds to generate for large collections.")
0159                + QLatin1String("</p></body></html>");
0160 
0161   m_reportView = new QStackedWidget(mainWidget);
0162   topLayout->addWidget(m_reportView);
0163 
0164 #ifdef USE_KHTML
0165   m_HTMLPart = new KHTMLPart(m_reportView);
0166   m_HTMLPart->setJScriptEnabled(true);
0167   m_HTMLPart->setJavaEnabled(false);
0168   m_HTMLPart->setMetaRefreshEnabled(false);
0169   m_HTMLPart->setPluginsEnabled(false);
0170 
0171   m_HTMLPart->begin();
0172   m_HTMLPart->write(text);
0173   m_HTMLPart->end();
0174   m_reportView->insertWidget(INDEX_HTML, m_HTMLPart->view());
0175 #else
0176   m_webView = new QWebEngineView(m_reportView);
0177   connect(m_webView, &QWebEngineView::loadFinished, this, [](bool b) {
0178     if(!b) myDebug() << "ReportDialog - failed to load view";
0179   });
0180   QWebEngineSettings* settings = m_webView->page()->settings();
0181   settings->setAttribute(QWebEngineSettings::JavascriptEnabled, true);
0182   settings->setAttribute(QWebEngineSettings::PluginsEnabled, false);
0183   settings->setAttribute(QWebEngineSettings::LocalContentCanAccessRemoteUrls, true);
0184   settings->setAttribute(QWebEngineSettings::LocalContentCanAccessFileUrls, true);
0185 
0186   m_webView->setHtml(text);
0187   m_reportView->insertWidget(INDEX_HTML, m_webView);
0188 #endif
0189 
0190   QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close);
0191   mainLayout->addWidget(buttonBox);
0192   connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
0193   connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0194   buttonBox->button(QDialogButtonBox::Close)->setDefault(true);
0195 
0196   setMinimumWidth(qMax(minimumWidth(), REPORT_MIN_WIDTH));
0197   setMinimumHeight(qMax(minimumHeight(), REPORT_MIN_HEIGHT));
0198 
0199   QTimer::singleShot(0, this, &ReportDialog::slotUpdateSize);
0200 }
0201 
0202 ReportDialog::~ReportDialog() {
0203   delete m_exporter;
0204   m_exporter = nullptr;
0205   delete m_tempFile;
0206   m_tempFile = nullptr;
0207 
0208   KConfigGroup config(KSharedConfig::openConfig(), QLatin1String(dialogOptionsString));
0209   KWindowConfig::saveWindowSize(windowHandle(), config);
0210 }
0211 
0212 void ReportDialog::slotGenerate() {
0213   GUI::CursorSaver cs(Qt::WaitCursor);
0214 
0215   QVariant curData = m_templateCombo->currentData();
0216   if(static_cast<QMetaType::Type>(curData.type()) == QMetaType::QUuid) {
0217     generateChart();
0218     m_reportView->setCurrentIndex(INDEX_CHART);
0219   } else {
0220     generateHtml();
0221     m_reportView->setCurrentIndex(INDEX_HTML);
0222   }
0223 }
0224 
0225 void ReportDialog::generateChart() {
0226 #ifdef HAVE_QCHARTS
0227   const QUuid uuid = m_templateCombo->currentData().toUuid();
0228   auto oldWidget = m_reportView->widget(INDEX_CHART);
0229   auto newWidget = ChartManager::self()->report(uuid)->createWidget();
0230   if(newWidget) {
0231     m_reportView->insertWidget(INDEX_CHART, newWidget);
0232   }
0233   if(oldWidget) {
0234     m_reportView->removeWidget(oldWidget);
0235     delete oldWidget;
0236   }
0237 #endif
0238 }
0239 
0240 void ReportDialog::generateHtml() {
0241   QString fileName = QLatin1String("report-templates/") + m_templateCombo->currentData().toString();
0242   QString xsltFile = DataFileRegistry::self()->locate(fileName);
0243   if(xsltFile.isEmpty()) {
0244     myWarning() << "can't locate " << m_templateCombo->currentData().toString();
0245     return;
0246   }
0247   // if it's the same XSL file, no need to reload the XSLTHandler, just refresh
0248   if(xsltFile == m_xsltFile) {
0249     slotRefresh();
0250     return;
0251   }
0252 
0253   m_xsltFile = xsltFile;
0254 
0255   delete m_exporter;
0256   m_exporter = new Export::HTMLExporter(Data::Document::self()->collection());
0257   m_exporter->setXSLTFile(m_xsltFile);
0258   m_exporter->setPrintHeaders(false); // the templates should take care of this themselves
0259   m_exporter->setPrintGrouped(true); // allow templates to take advantage of added DOM
0260 
0261   slotRefresh();
0262 }
0263 
0264 void ReportDialog::slotRefresh() {
0265   if(!m_exporter) {
0266     myWarning() << "no exporter";
0267     return;
0268   }
0269 
0270   m_exporter->setGroupBy(Controller::self()->expandedGroupBy());
0271   m_exporter->setSortTitles(Controller::self()->sortTitles());
0272   m_exporter->setColumns(Controller::self()->visibleColumns());
0273   // only print visible entries
0274   m_exporter->setEntries(Controller::self()->visibleEntries());
0275 
0276   long options = Export::ExportUTF8 | Export::ExportComplete | Export::ExportImages;
0277   if(Config::autoFormat()) {
0278     options |= Export::ExportFormatted;
0279   }
0280   m_exporter->setOptions(options);
0281 
0282   // by setting the xslt file as the URL, any images referenced in the xslt "theme" can be found
0283   // by simply using a relative path in the xslt file
0284   QUrl u = QUrl::fromLocalFile(m_xsltFile);
0285 #ifdef USE_KHTML
0286   m_HTMLPart->begin(u);
0287   m_HTMLPart->write(m_exporter->text());
0288   m_HTMLPart->end();
0289 #else
0290   const auto exporterText = m_exporter->text();
0291   // limit is 2 MB after percent encoding, etc., so give some padding
0292   if(exporterText.size() > 1200000) {
0293     delete m_tempFile;
0294     m_tempFile = new QTemporaryFile(QDir::tempPath() + QLatin1String("/tellicoreport_XXXXXX") + QLatin1String(".html"));
0295     m_tempFile->open();
0296     QTextStream stream(m_tempFile);
0297     stream.setCodec("UTF-8");
0298     stream << exporterText;
0299     m_webView->load(QUrl::fromLocalFile(m_tempFile->fileName()));
0300   } else {
0301     m_webView->setHtml(exporterText, u);
0302   }
0303 #endif
0304 #if 0
0305   myDebug() << "Remove debug from reportdialog.cpp";
0306   QFile f(QLatin1String("/tmp/test.html"));
0307   if(f.open(QIODevice::WriteOnly)) {
0308     QTextStream t(&f);
0309     t << m_exporter->text();
0310   }
0311   f.close();
0312 #endif
0313 }
0314 
0315 // actually the print button
0316 void ReportDialog::slotPrint() {
0317   if(m_reportView->currentIndex() == INDEX_CHART) {
0318     QPrinter printer;
0319     printer.setResolution(600);
0320     QPointer<QPrintDialog> dialog = new QPrintDialog(&printer, this);
0321     if(dialog->exec() == QDialog::Accepted) {
0322       QWidget* widget = m_reportView->currentWidget();
0323       // there might be a widget inside a scroll area
0324       if(QScrollArea* scrollArea = qobject_cast<QScrollArea*>(widget)) {
0325         widget = scrollArea->widget();
0326       }
0327       QPainter painter;
0328       painter.begin(&printer);
0329       auto const paintRect = printer.pageLayout().paintRectPixels(printer.resolution());
0330       const double xscale = paintRect.width() / double(widget->width());
0331       const double yscale = paintRect.height() / double(widget->height());
0332       const double scale = 0.95*qMin(xscale, yscale);
0333       auto const paperRect = printer.pageLayout().fullRectPixels(printer.resolution());
0334       painter.translate(paperRect.center());
0335       painter.scale(scale, scale);
0336       painter.translate(-widget->width()/2, -widget->height()/2);
0337       widget->render(&painter);
0338     }
0339   } else {
0340 #ifdef USE_KHTML
0341     m_HTMLPart->view()->print();
0342 #else
0343     QPrinter printer;
0344     printer.setResolution(300);
0345     QPointer<QPrintDialog> dialog = new QPrintDialog(&printer, this);
0346     if(dialog->exec() == QDialog::Accepted) {
0347       QEventLoop loop;
0348       GUI::CursorSaver cs(Qt::WaitCursor);
0349       m_webView->page()->print(&printer, [&](bool) { loop.quit(); });
0350       loop.exec();
0351     }
0352 #endif
0353   }
0354 }
0355 
0356 void ReportDialog::slotSaveAs() {
0357   if(m_reportView->currentIndex() == INDEX_CHART) {
0358     QString filter = i18n("PNG Files") + QLatin1String(" (*.png)")
0359                   + QLatin1String(";;")
0360                   + i18n("All Files") + QLatin1String(" (*)");
0361     QUrl u = QFileDialog::getSaveFileUrl(this, QString(), QUrl(), filter);
0362     if(!u.isEmpty() && u.isValid()) {
0363       QWidget* widget = m_reportView->currentWidget();
0364       // there might be a widget inside a scroll area
0365       if(QScrollArea* scrollArea = qobject_cast<QScrollArea*>(widget)) {
0366         widget = scrollArea->widget();
0367       }
0368       QPixmap pixmap(widget->size());
0369       widget->render(&pixmap);
0370       pixmap.save(u.toLocalFile());
0371     }
0372   } else if(m_exporter) {
0373     QString filter = i18n("HTML Files") + QLatin1String(" (*.html)")
0374                   + QLatin1String(";;")
0375                   + i18n("All Files") + QLatin1String(" (*)");
0376     QUrl u = QFileDialog::getSaveFileUrl(this, QString(), QUrl(), filter);
0377     if(!u.isEmpty() && u.isValid()) {
0378       KConfigGroup config(KSharedConfig::openConfig(), "ExportOptions");
0379       bool encode = config.readEntry("EncodeUTF8", true);
0380       long oldOpt = m_exporter->options();
0381 
0382       // turn utf8 off
0383       long options = oldOpt & ~Export::ExportUTF8;
0384       // now turn it on if true
0385       if(encode) {
0386         options |= Export::ExportUTF8;
0387       }
0388 
0389       QUrl oldURL = m_exporter->url();
0390       m_exporter->setOptions(options);
0391       m_exporter->setURL(u);
0392 
0393       m_exporter->exec();
0394 
0395       m_exporter->setURL(oldURL);
0396       m_exporter->setOptions(oldOpt);
0397     }
0398   }
0399 }
0400 
0401 void ReportDialog::slotUpdateSize() {
0402   KConfigGroup config(KSharedConfig::openConfig(), QLatin1String(dialogOptionsString));
0403   KWindowConfig::restoreWindowSize(windowHandle(), config);
0404 }