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;\"> </p>" 0309 "<p style=\"page-break-before: always;\"> </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 }