/** * @RDE_HEADER@ * Main thp viewer widget * $Id$ * $HeadURL$ * $Author$ */ #include "thp_viewer.h" #include "ui_thp_viewer.h" #include "thp_plugin_provider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace NGL { /* * ThpViewer base class */ ThpViewer::ThpViewer(const QUrl &url, QWidget *parent) : QWidget(parent) , ui(new Ui::thpViewer) , m_scaleFactor(1.0) , m_homeLink(url) , m_homePath(QFileInfo(m_homeLink.toLocalFile()).absolutePath()) , m_trainingName(QString()) , m_logger(NGL::NGLLoggerBootstrap::getNGLRootLogger().getNGLLogger("ThpViewer")) { ui->setupUi(this); ui->audioWidget->setAV(true); ui->videoWidget->setAV(false); /* TODO if (!conffile.isEmpty()) { QSettings thpsettings(conffile, QSettings::IniFormat, this); // needed? const char timeZone = thpsettings.value(QStringLiteral("Time/Timezone"), QStringLiteral("Z")).toString().at(0).toLatin1(); DTG::setTimezone(timeZone, true); } */ QString basedir(url.toLocalFile()); basedir = basedir.left(basedir.lastIndexOf(QLatin1Char('/'))); DEBUG_LOG(m_logger) << "url: " << url.toString() << " basedir: " << basedir; QFont f = ui->mainLabel->font(); f.setBold(true); f.setPointSizeF(f.pointSizeF() * 1.2); ui->mainLabel->setFont(f); connect(ui->closeButton, &QToolButton::clicked, this, &ThpViewer::close); connect(ui->backButton, &QToolButton::clicked, this, &ThpViewer::onBack); connect(ui->homeButton, &QToolButton::clicked, this, &ThpViewer::onHome); connect(ui->helpButton, &QToolButton::clicked, this, &ThpViewer::onHelp); connect(ui->printButton, &QToolButton::clicked, this, &ThpViewer::onPrint); connect(ui->fullScreenButton, &QToolButton::clicked, this, &ThpViewer::onFullScreen); connect(ui->htmlWidget, &THP::HtmlWidget::linkClicked, this, &ThpViewer::onLinkClicked); connect(ui->htmlWidget, &THP::HtmlWidget::urlChanged, this, &ThpViewer::onUrlChanged); connect(ui->htmlWidget, &THP::HtmlWidget::titleChanged, this, &ThpViewer::onTitleChanged); ui->backButton->setEnabled(false); ui->helpButton->setVisible(false); // Help disabled for now #ifndef Q_WS_WIN setWindowIcon(QIcon(QStringLiteral(":icons/16x16/rde.ico"))); #endif setWindowTitle(tr("Combat Training Center")); } ThpViewer::~ThpViewer() { delete ui; } bool ThpViewer::init() { QFile file(m_homePath + QStringLiteral("/data.json")); int exerciseId = 0; if (file.exists() && file.open(QIODevice::ReadOnly)) { const auto obj = QJsonDocument::fromJson(file.readAll()).object(); exerciseId = obj.value(QLatin1String("exerciseId")).toInt(); } else { QFile file2(m_homePath + QStringLiteral("/../data.json")); if (file2.exists() && file2.open(QIODevice::ReadOnly)) { const auto obj = QJsonDocument::fromJson(file2.readAll()).object(); exerciseId = obj.value(QLatin1String("exerciseId")).toInt(); } } DEBUG_LOG(m_logger) << "Use EXERCISE" << exerciseId; if (!ui->situationWidget->init(m_homePath, exerciseId)) { ERROR_LOG(m_logger) << "Could not init situation widget - abort"; return false; } if (!ui->buildingWidget->init(ui->situationWidget->replayHandler(), ui->situationWidget->pluginProvider()->buildingPluginProvider(), m_homePath)) { ERROR_LOG(m_logger) << "Could not init situation widget - abort"; return false; } onZoomChanged(); onLinkClicked(m_homeLink); return true; } void ThpViewer::center() { QRect rect = QGuiApplication::primaryScreen()->availableGeometry(); // 80 % int width = rect.width() * 4 / 5; int height = rect.height() * 4 / 5; int x = rect.width() / 2 - width / 2; int y = rect.height() / 2 - height / 2; resize(width, height); move(x, y); } void ThpViewer::closeEvent(QCloseEvent *event) { // explicit delete those two widgets here, otherwise we may get in trouble // on windows when the app is closed with the 'X' button from the window frame delete ui->buildingWidget; ui->buildingWidget = nullptr; delete ui->situationWidget; ui->situationWidget = nullptr; QWidget::closeEvent(event); } void ThpViewer::onNewThp(const QString &index) { DEBUG_LOG(m_logger) << "onNewThp: " << index; if (index.isEmpty()) return; m_homeLink = QUrl::fromLocalFile(index); m_homePath = QFileInfo(m_homeLink.toLocalFile()).absolutePath(); DEBUG_LOG(m_logger) << "home: " << m_homeLink.toString(); onLinkClicked(m_homeLink); ui->backButton->setEnabled(false); this->raise(); } void ThpViewer::onTitleChanged(const QString &title) { if (title.endsWith(QLatin1String("html")) || title.endsWith(QLatin1String("html#!/"))) return; if (m_trainingName.isEmpty()) m_trainingName = title; ui->mainLabel->setText(m_trainingName == title ? m_trainingName : QStringLiteral("%1 - %2").arg(m_trainingName, title)); } void ThpViewer::keyPressEvent(QKeyEvent *e) { switch (e->key()) { case Qt::Key_Backspace: onBack(); break; case Qt::Key_Plus: if (ui->stackedWidget->isVisible() && ui->stackedWidget->currentWidget() == ui->situationWidget) return; if (m_scaleFactor < 10) { m_scaleFactor *= 1.5; onZoomChanged(); } break; case Qt::Key_M: { if (QApplication::queryKeyboardModifiers() == Qt::ControlModifier) onLinkClicked(QUrl::fromLocalFile(QDir::currentPath() + QStringLiteral("/") + QInputDialog::getText(this, {}, QStringLiteral("href_local")))); break; } case Qt::Key_Minus: if (ui->stackedWidget->isVisible() && ui->stackedWidget->currentWidget() == ui->situationWidget) return; if (m_scaleFactor > 0.05) { m_scaleFactor /= 1.5; onZoomChanged(); } break; case Qt::Key_F1: onHelp(); break; case Qt::Key_Escape: if (isFullScreen()) onFullScreen(); break; default: QWidget::keyPressEvent(e); break; } } void ThpViewer::onHelp() { QFileInfo f(QStringLiteral("%1/../DATA/help/%2/help.html").arg(QCoreApplication::applicationDirPath(), QString::fromLatin1(NGL::translationLocale()))); if (f.exists()) onLinkClicked(QUrl::fromLocalFile(f.absoluteFilePath())); // TODO Pfad anpassen! else { f.setFile(qApp->applicationDirPath() + QStringLiteral("/../DATA/help/en_US/help.html")); // TODO Pfad anpassen! if (f.exists()) onLinkClicked(QUrl::fromLocalFile(f.absoluteFilePath())); } } void ThpViewer::onZoomChanged() { ui->pdfWidget->setZoomFactor(m_scaleFactor); ui->htmlWidget->setZoomFactor(m_scaleFactor); // double zoom = ( int ) ( m_scaleFactor * 100. + 0.5 ); // ui->statusbar->showMessage ( tr ( "zoom: %1 %" ).arg ( zoom ) ); } void ThpViewer::onLinkClicked(const QUrl &url) { DEBUG_LOG(m_logger) << "url: " << url.toString(); const QString path = url.path(); const QString lpath = path.toLower(); const QStringRef ext = lpath.splitRef(QLatin1Char('.')).last(); const QStringRef ext2Chars = ext.left(2); ui->audioWidget->stop(); ui->videoWidget->stop(); // Video-only if (lpath.endsWith(QLatin1String("video.pls")) || ext == QLatin1String("mpg") || ext == QLatin1String("avi") || ext == QLatin1String("ogg")) { ui->videoWidget->play(url); ui->stackedWidget->hide(); ui->videoWidget->show(); ui->audioWidget->hide(); ui->backButton->setEnabled(true); // Audio-only } else if (lpath.endsWith(QLatin1String("audio.pls")) || ext == QLatin1String("mp3")) { ui->audioWidget->play(url); // Ermitteln der Groesse des AudioWidgets QSize sizeAudio = ui->audioWidget->sizeHint(); QSize sizeStack = ui->stackedWidget->sizeHint(); sizeStack -= sizeAudio; QList l; l << sizeStack.height() << sizeAudio.height(); ui->splitter_1->setSizes(l); ui->stackedWidget->show(); // hier können wir html mit anzeigen ui->videoWidget->hide(); ui->audioWidget->show(); ui->backButton->setEnabled(true); // PDF-only } else if (ext == QLatin1String("pdf")) { if (ui->pdfWidget->showPDF(url)) { ui->stackedWidget->setCurrentWidget(ui->pdfWidget); ui->stackedWidget->show(); ui->pdfWidget->resizePdfToWidth(); ui->videoWidget->hide(); ui->audioWidget->hide(); ui->backButton->setEnabled(true); } else { QMessageBox::warning(this, tr("Opening PDF file"), tr("Can not open PDF file %1 - missing or corrupt file!").arg(url.toString())); } } else if (lpath.endsWith(QLatin1String("sitmap.json"), Qt::CaseInsensitive)) { // 2D oder 3D ? { NOTICE_LOG(m_logger) << LOGFUNCTION << " lpath = '" << lpath << "'"; // peek into /sitreplay.json: QFileInfo fi(path); const QString basePath = fi.absolutePath(); const QString replayConfigFile = basePath + QDir::separator() + QStringLiteral("sitreplay.json"); // aka replayConfigPath NOTICE_LOG(m_logger) << LOGFUNCTION << " replayConfigFile = '" << replayConfigFile << "'"; } // Lage-only QString localFile = url.toLocalFile(); QString basePath = localFile; int idx = basePath.lastIndexOf(QLatin1Char('/')); idx = basePath.lastIndexOf(QLatin1Char('/'), idx - 1); if (idx >= 0) basePath.chop(basePath.size() - idx - 1); ui->situationWidget->showSituation(url.toLocalFile(), basePath); ui->stackedWidget->setCurrentWidget(ui->situationWidget); ui->stackedWidget->show(); ui->videoWidget->hide(); ui->audioWidget->hide(); ui->backButton->setEnabled(true); } else if (lpath.endsWith(QLatin1String("buildingViewer.json"), Qt::CaseInsensitive)) { // BuildingViewer-only QString localFile = url.toLocalFile(); QString basePath = localFile; int idx = basePath.lastIndexOf(QLatin1Char('/')); idx = basePath.lastIndexOf(QLatin1Char('/'), idx - 1); if (idx >= 0) basePath.chop(basePath.size() - idx - 1); ui->buildingWidget->showSituation(url.toLocalFile(), basePath); ui->stackedWidget->setCurrentWidget(ui->buildingWidget); ui->stackedWidget->show(); ui->videoWidget->hide(); ui->audioWidget->hide(); ui->backButton->setEnabled(true); } else if (ext == QLatin1String("html") && lpath.midRef(lpath.lastIndexOf(QLatin1Char('/')) + 1).startsWith(QLatin1String("combined_"))) { // Kombiniert QHash urls = getCombinedPlaybackDetails(url); if (urls.isEmpty()) return; if (urls.contains(QLatin1String("audio"))) { ui->audioWidget->show(); ui->audioWidget->play(urls[QLatin1String("audio")]); } if (urls.contains(QLatin1String("video"))) { ui->videoWidget->show(); ui->videoWidget->play(urls[QLatin1String("video")]); } // Einer von dreien: if (urls.contains(QLatin1String("pdf"))) { ui->stackedWidget->setCurrentWidget(ui->pdfWidget); ui->pdfWidget->showPDF(urls[QLatin1String("pdf")]); ui->stackedWidget->show(); } else if (urls.contains(QLatin1String("html"))) { ui->stackedWidget->setCurrentWidget(ui->htmlWidget); ui->htmlWidget->showHTML(urls[QLatin1String("html")]); ui->stackedWidget->show(); } else if (urls.contains(QLatin1String("jpg"))) { ui->stackedWidget->setCurrentWidget(ui->htmlWidget); ui->htmlWidget->showHTML(urls[QLatin1String("jpg")]); ui->stackedWidget->show(); } else if (urls.contains(QLatin1String("png"))) { ui->stackedWidget->setCurrentWidget(ui->htmlWidget); ui->htmlWidget->showHTML(urls[QLatin1String("png")]); ui->stackedWidget->show(); } else ui->stackedWidget->hide(); ui->backButton->setEnabled(true); } else if (ext2Chars == QLatin1String("do") || ext2Chars == QLatin1String("xl") || ext2Chars == QLatin1String("pp")) QDesktopServices::openUrl(url); else { DEBUG_LOG(m_logger) << "Trying to open url: " << url.toString(); ui->htmlWidget->showHTML(url); ui->stackedWidget->setCurrentWidget(ui->htmlWidget); ui->stackedWidget->show(); ui->videoWidget->hide(); ui->audioWidget->hide(); ui->backButton->setEnabled(url != m_homeLink); } } /******************************************************************************************** * Parsed eine HTML-Dateinach allen Links, die sich direkt unterhalb des BODY-Tags befinden * und liefert die entsprechenden Link-URLs zurück, wenn diese das Tag ID besitzen. * * Von der aufrufenden Funktion derzeit ausgewertete Werte für ID sind: * html, audio, video, lage, pdf (alles klein geschrieben!) */ QHash ThpViewer::getCombinedPlaybackDetails(const QUrl &url) const { QHash hash; QFile file(url.toLocalFile()); QDomDocument doc(QStringLiteral("RSS")); if (!file.open(QIODevice::ReadOnly)) return hash; QString errorStr; int errorLine, errorColumn; if (!doc.setContent(&file, true, &errorStr, &errorLine, &errorColumn)) { file.close(); return hash; } QDomElement domel = doc.documentElement(); domel = domel.firstChildElement(QStringLiteral("BODY")); domel = domel.firstChildElement(QStringLiteral("A")); while (!domel.isNull()) { if (!domel.attribute(QStringLiteral("id")).isEmpty()) { QUrl linkurl(domel.attribute(QStringLiteral("HREF"))); if (linkurl.isRelative()) linkurl = url.resolved(linkurl); DEBUG_LOG(m_logger) << "attr: " << domel.attribute(QStringLiteral("id")).toLower() << " url: " << linkurl.toString(); hash[domel.attribute(QStringLiteral("id")).toLower()] = linkurl; } domel = domel.nextSiblingElement(QStringLiteral("A")); } file.close(); return hash; } void ThpViewer::onPrint() { if (ui->stackedWidget->isVisible()) { if (ui->stackedWidget->currentWidget() == ui->situationWidget) { QImage image = ui->situationWidget->grabFrameBuffer(); QPrinter printer; QPrintDialog *dialog = new QPrintDialog(&printer, this); dialog->setWindowTitle(tr("Print Document")); if (dialog->exec() != QDialog::Accepted) return; QPainter painter; painter.begin(&printer); QRectF rect = painter.viewport(); double mmH = printer.logicalDpiY() / 25.4; // 25.4 = mm/inch double mmW = printer.logicalDpiX() / 25.4; painter.setFont(QFont(QStringLiteral("Arial"), 30)); QString title = tr("Combat Training Center"); QRectF textBox = painter.boundingRect(QRectF(), Qt::AlignLeft & Qt::TextSingleLine, title); textBox.setWidth(painter.viewport().width()); painter.drawText(textBox, Qt::AlignCenter & Qt::TextSingleLine, title); QPointF underImage; if (printer.orientation() == QPrinter::Portrait) { const QPointF textBlockBegin = QPointF(0 * mmW, 25 * mmH); // Baseline 25mm unter Seitenbeginn // Noch mal schauen, welches (auch übersetzt!) der breiteste Bezeichner (also 1. Spalte) ist! painter.setFont(QFont(QStringLiteral("Arial"), 14, QFont::Bold, false)); textBox = painter.boundingRect(QRect(), Qt::AlignLeft & Qt::TextSingleLine, tr("Center point:\nExercise:\nTime:")); // Textbox hat 3 Zeilen, also ist der Faktor pro Zeile 0.33 = 1/3 // Spalte 1 painter.drawText(textBlockBegin, tr("Exercise:")); painter.drawText(textBlockBegin + QPointF(0, 0.33 * textBox.height()), tr("Time:")); painter.drawText(textBlockBegin + QPointF(0, 0.66 * textBox.height()), tr("Center point:")); // Zwischen Spalte 1 und Spalte 2 noch 3mm Platz lassen // Spalte 2 painter.setFont(QFont(QStringLiteral("Arial"), 14, QFont::Normal, false)); painter.drawText(textBlockBegin + QPointF(textBox.width() + 3 * mmW, 0), m_trainingName); painter.drawText(textBlockBegin + QPointF(textBox.width() + 3 * mmW, 0.33 * textBox.height()), ui->situationWidget->currentDTGTime()); painter.drawText(textBlockBegin + QPointF(textBox.width() + 3 * mmW, 0.66 * textBox.height()), ui->situationWidget->centerPointString()); // Das Bild soll formatfüllend angezeigt werden, das Rechteck muss 45mm "flacher" sein, da es erst ab 45mm gedruckt werden soll. QSizeF size = image.size(); rect.adjust(0, 0, 0, -45 * mmH); size.scale(rect.size(), Qt::KeepAspectRatio); // Und ab 45mm von oben drucken: QRectF imgrect = QRectF(QPointF(0 * mmW, 45 * mmH), size); painter.drawImage(imgrect, image); // Untere linke Ecke des Bildes underImage = imgrect.bottomLeft(); } else // Landscape { // Noch mal schauen, welches (auch übersetzt!) der breiteste Bezeichner oder variable Text ist! painter.setFont(QFont(QStringLiteral("Arial"), 14, QFont::Bold, false)); textBox = painter.boundingRect(QRect(), Qt::AlignLeft, tr("Center point:\n%1\n\nTime:\n%2\n\nExercise:\n%3") .arg(m_trainingName, ui->situationWidget->currentDTGTime(), ui->situationWidget->centerPointString())); // TextBlock fängt an: Notwendige Breite von rechts und 15 mm + 1 Zeilenhöhe (=1/8 der Textboxhöhe) von oben. // Die Zeilenhöhe ist notwendig weil drawText(QPoint, QString) den QPoint auf die Baseline setzt! QPointF textBlockBegin = QPointF(painter.viewport().width() - textBox.width(), 15 * mmH + 0.125 * textBox.height()); // Die Textbox hat 8 Zeilen, also ist der Faktor pro Zeile 1/8 = 0.125 // Übung painter.setFont(QFont(QStringLiteral("Arial"), 14, QFont::Bold, false)); painter.drawText(textBlockBegin, tr("Exercise:")); painter.setFont(QFont(QStringLiteral("Arial"), 14, QFont::Normal, false)); painter.drawText(textBlockBegin + QPointF(0, 0.125 * textBox.height()), m_trainingName); painter.setFont(QFont(QStringLiteral("Arial"), 14, QFont::Bold, false)); painter.drawText(textBlockBegin + QPointF(0, 0.375 * textBox.height()), tr("Time:")); painter.setFont(QFont(QStringLiteral("Arial"), 14, QFont::Normal, false)); painter.drawText(textBlockBegin + QPointF(0, 0.500 * textBox.height()), ui->situationWidget->currentDTGTime()); painter.setFont(QFont(QStringLiteral("Arial"), 14, QFont::Bold, false)); painter.drawText(textBlockBegin + QPointF(0, 0.750 * textBox.height()), tr("Center point:")); painter.setFont(QFont(QStringLiteral("Arial"), 14, QFont::Normal, false)); painter.drawText(textBlockBegin + QPointF(0, 0.875 * textBox.height()), ui->situationWidget->centerPointString()); // Das Bild soll formatfüllend angezeigt werden, das Rechteck muss 15mm "flacher" sein, da es erst ab 15mm gedruckt werden soll. // Ausserdem müssen in der Breite die Textboxbreite und 3mm Luft dazwischen abgezogen werden. QSizeF size = image.size(); rect.adjust(0, 0, -textBox.width() - 3 * mmW, -15 * mmH); size.scale(rect.size(), Qt::KeepAspectRatio); // Und ab 15mm von oben drucken: QRectF imgrect = QRectF(QPointF(0 * mmW, 15 * mmH), size); painter.drawImage(imgrect, image); // Untere linke Ecke des Bildes underImage = imgrect.bottomLeft(); } painter.setFont(QFont(QStringLiteral("Arial"), 12)); // String für Breite und Höhe erstellen const QRectF extent = ui->situationWidget->extentMgrs(); QString areasize = tr("Width: %1m, Height: %2m").arg(extent.width()).arg(extent.height()); // Die Größe herausfinden textBox = painter.boundingRect(QRect(), Qt::AlignLeft & Qt::TextSingleLine, areasize); // Einen Dot vergrößern (siehe QRect: Die Rahmenpixel unten und rechts sind nicht Teil der Rechteckgröße) textBox.adjust(0, 0, 1, 1); // An die richtige Stelle bugsieren textBox.moveBottomLeft(underImage); // Und mit weiß und 50% Deckkraft vollmalen. painter.fillRect(textBox, QColor(255, 255, 255, 128)); //Dann den Text drüberlegen painter.drawText(underImage, areasize); painter.end(); } else if (ui->stackedWidget->currentWidget() == ui->pdfWidget) { QPrinter printer; QPrintDialog *dialog = new QPrintDialog(&printer, this); dialog->setWindowTitle(tr("Print Document")); if (dialog->exec() != QDialog::Accepted) return; QVector images = ui->pdfWidget->getPrintableImages(printer.resolution()); int lastItem = images.size() - 1; QPainter painter; painter.begin(&printer); for (int i = 0; i < images.size(); ++i) { QRect rect = painter.viewport(); QSize size = images.at(i).size(); size.scale(rect.size(), Qt::KeepAspectRatio); painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); painter.setWindow(images.at(i).rect()); painter.drawImage(0, 0, images.at(i)); if (i != lastItem) printer.newPage(); } painter.end(); } else if (ui->stackedWidget->currentWidget() == ui->htmlWidget) { QPrinter printer; QPrintDialog *dialog = new QPrintDialog(&printer, this); dialog->setWindowTitle(tr("Print Document")); if (dialog->exec() != QDialog::Accepted) return; ui->htmlWidget->print(&printer); } else // Audio/Video-Widget { } } else // nur HTML sichtbar... { QPrinter printer; QPrintDialog *dialog = new QPrintDialog(&printer, this); dialog->setWindowTitle(tr("Print Document")); if (dialog->exec() != QDialog::Accepted) return; ui->htmlWidget->print(&printer); } } void ThpViewer::onBack() { ui->audioWidget->stop(); ui->videoWidget->stop(); ui->situationWidget->stop(); ui->audioWidget->hide(); ui->videoWidget->hide(); if (ui->stackedWidget->currentWidget() == ui->htmlWidget && ui->stackedWidget->isVisible()) ui->htmlWidget->back(); ui->stackedWidget->show(); ui->stackedWidget->setCurrentWidget(ui->htmlWidget); onUrlChanged(ui->htmlWidget->url()); } void ThpViewer::onHome() { ui->htmlWidget->show(); onLinkClicked(m_homeLink); } void ThpViewer::onFullScreen() { setWindowState(windowState() ^ Qt::WindowFullScreen); } void ThpViewer::onUrlChanged(const QUrl &url) { ui->backButton->setEnabled(url != m_homeLink); } } // namespace NGL