/////////////////////////////////////////////////////////////////////////////// // MediaInfoXP // Copyright (C) 2004-2013 LoRd_MuldeR <MuldeR2@GMX.de> // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // // http://www.gnu.org/licenses/gpl-2.0.txt /////////////////////////////////////////////////////////////////////////////// #include "MainWindow.h" //UIC includes #include "../tmp/Common/uic/Dialog.h" //Qt includes #include <QMessageBox> #include <QFileDialog> #include <QStyleFactory> #include <QTimer> #include <QResource> #include <QProcess> #include <QCloseEvent> #include <QScrollBar> #include <QDesktopServices> #include <QClipboard> //Win32 #define WIN32_LEAN_AND_MEAN #include <Windows.h> //Internal #include "Config.h" #include "Utils.h" //Macros #define SET_FONT_BOLD(WIDGET,BOLD) { QFont _font = WIDGET->font(); _font.setBold(BOLD); WIDGET->setFont(_font); } #define SET_TEXT_COLOR(WIDGET,COLOR) { QPalette _palette = WIDGET->palette(); _palette.setColor(QPalette::WindowText, (COLOR)); _palette.setColor(QPalette::Text, (COLOR)); WIDGET->setPalette(_palette); } //Text const char *STATUS_BLNK = ">> You can drop any type of media files here <<"; const char *STATUS_WORK = "Analyzing file(s), this may take a moment or two..."; //Links const char *LINK_MULDER = "http://muldersoft.com/"; const char *LINK_MEDIAINFO = "http://mediainfo.sourceforge.net/en"; const char *LINK_DISCUSS = "http://forum.doom9.org/showthread.php?t=96516"; //HTML characters static QList<QPair<const QString, const QString>> HTML_ESCAPE(void) { QList<QPair<const QString, const QString>> htmlEscape; htmlEscape << QPair<const QString, const QString>("<", "<"); htmlEscape << QPair<const QString, const QString>(">", ">"); htmlEscape << QPair<const QString, const QString>("&", "&"); return htmlEscape; } //////////////////////////////////////////////////////////// // Constructor //////////////////////////////////////////////////////////// CMainWindow::CMainWindow(const QString &tempFolder, QWidget *parent) : QMainWindow(parent), m_tempFolder(tempFolder), m_firstShow(true), m_htmlEscape(HTML_ESCAPE()), ui(new Ui::MainWindow) { //Init UI qApp->setStyle(QStyleFactory::create("plastique")); ui->setupUi(this); setMinimumSize(this->size()); //Setup links ui->actionLink_MuldeR->setData(QVariant(QString::fromLatin1(LINK_MULDER))); ui->actionLink_MediaInfo->setData(QVariant(QString::fromLatin1(LINK_MEDIAINFO))); ui->actionLink_Discuss->setData(QVariant(QString::fromLatin1(LINK_DISCUSS))); //Setup connections connect(ui->analyzeButton, SIGNAL(clicked()), this, SLOT(analyzeButtonClicked())); connect(ui->actionOpen, SIGNAL(triggered()), this, SLOT(analyzeButtonClicked())); connect(ui->actionSave, SIGNAL(triggered()), this, SLOT(saveButtonClicked())); connect(ui->actionCopyToClipboard, SIGNAL(triggered()), this, SLOT(copyToClipboardButtonClicked())); connect(ui->actionClear, SIGNAL(triggered()), this, SLOT(clearButtonClicked())); connect(ui->actionLink_MuldeR, SIGNAL(triggered()), this, SLOT(linkTriggered())); connect(ui->actionLink_MediaInfo, SIGNAL(triggered()), this, SLOT(linkTriggered())); connect(ui->actionLink_Discuss, SIGNAL(triggered()), this, SLOT(linkTriggered())); connect(ui->actionAbout, SIGNAL(triggered()), this, SLOT(showAboutScreen())); ui->versionLabel->installEventFilter(this); //Context menu ui->textBrowser->setContextMenuPolicy(Qt::ActionsContextMenu); ui->textBrowser->insertActions(0, ui->menuFile->actions()); //Create label m_floatingLabel = new QLabel(ui->textBrowser); m_floatingLabel->setText(QString::fromLatin1(STATUS_BLNK)); m_floatingLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); m_floatingLabel->show(); SET_TEXT_COLOR(m_floatingLabel, Qt::darkGray); SET_FONT_BOLD(m_floatingLabel, true); m_floatingLabel->setContextMenuPolicy(Qt::ActionsContextMenu); m_floatingLabel->insertActions(0, ui->textBrowser->actions()); //Clear m_mediaInfoPath.clear(); m_mediaInfoHandle = INVALID_HANDLE_VALUE; m_process = NULL; } //////////////////////////////////////////////////////////// // Destructor //////////////////////////////////////////////////////////// CMainWindow::~CMainWindow(void) { if(m_mediaInfoHandle != INVALID_HANDLE_VALUE) { CloseHandle(m_mediaInfoHandle); m_mediaInfoHandle = INVALID_HANDLE_VALUE; } MIXP_DELETE_OBJ(m_process); MIXP_DELETE_OBJ(m_floatingLabel); } //////////////////////////////////////////////////////////// // EVENTS //////////////////////////////////////////////////////////// void CMainWindow::showEvent(QShowEvent *event) { QMainWindow::showEvent(event); resize(this->minimumSize()); //Init test ui->versionLabel->setText(QString("v%1 / v%2 (%3)").arg(QString().sprintf("%u.%02u", mixp_versionMajor, mixp_versionMinor), QString().sprintf("%u.%u.%02u", mixp_miVersionMajor, mixp_miVersionMinor, mixp_miVersionPatch), mixp_get_build_date().toString(Qt::ISODate))); ui->updateLabel->setText(tr("This version is more than six month old and probably outdated. Please check <a href=\"%1\">%1</a> for updates!").arg(LINK_MULDER)); //Show update hint? ui->updateLabel->setVisible(mixp_get_build_date().addMonths(6) < mixp_get_current_date()); //Force resize event resizeEvent(NULL); QTimer::singleShot(0, this, SLOT(updateSize())); //Enable drag & drop support setAcceptDrops(true); if(m_firstShow) { const QStringList arguments = qApp->arguments(); for(QStringList::ConstIterator iter = arguments.constBegin(); iter != arguments.constEnd(); iter++) { if(QString::compare(*iter, "--open", Qt::CaseInsensitive) == 0) { if(++iter != arguments.constEnd()) { QFileInfo currentFile = QFileInfo(*iter); if(currentFile.exists() && currentFile.isFile()) { m_pendingFiles << currentFile.canonicalFilePath(); } continue; } break; } } if(!m_pendingFiles.empty()) { QTimer::singleShot(0, this, SLOT(analyzeFiles())); } m_firstShow = false; } } void CMainWindow::closeEvent(QCloseEvent *event) { if(m_process) { if(m_process->state() != QProcess::NotRunning) { event->ignore(); } } } void CMainWindow::resizeEvent(QResizeEvent *event) { if(event) { QMainWindow::resizeEvent(event); } updateSize(); } void CMainWindow::dragEnterEvent(QDragEnterEvent *event) { bool accept[2] = {false, false}; foreach(const QString &fmt, event->mimeData()->formats()) { accept[0] = accept[0] || fmt.contains("text/uri-list", Qt::CaseInsensitive); accept[1] = accept[1] || fmt.contains("FileNameW", Qt::CaseInsensitive); } if(accept[0] && accept[1]) { event->acceptProposedAction(); } } void CMainWindow::dropEvent(QDropEvent *event) { if(m_process && (m_process->state() != QProcess::NotRunning)) { qWarning("Process is still running!\n"); return; } m_pendingFiles.clear(); QList<QUrl> urls = event->mimeData()->urls(); while(!urls.isEmpty()) { QUrl currentUrl = urls.takeFirst(); QFileInfo file(currentUrl.toLocalFile()); if(file.exists() && file.isFile()) { m_pendingFiles << file.canonicalFilePath(); } } if(!m_pendingFiles.isEmpty()) { QTimer::singleShot(0, this, SLOT(analyzeFiles())); } } void CMainWindow::keyPressEvent(QKeyEvent *e) { if(e->key() == Qt::Key_Escape) { if(m_process && (m_process->state() != QProcess::NotRunning)) { MessageBeep(MB_ICONERROR); qWarning("Escape pressed, terminated process!"); m_process->kill(); } } } bool CMainWindow::eventFilter(QObject *o, QEvent *e) { if((o == ui->versionLabel) && (e->type() == QEvent::MouseButtonPress)) { QTimer::singleShot(0, this, SLOT(showAboutScreen())); return true; } return QMainWindow::eventFilter(o, e); } //////////////////////////////////////////////////////////// // SLOTS //////////////////////////////////////////////////////////// void CMainWindow::analyzeFiles(void) { //Any files pending? if(m_pendingFiles.isEmpty()) { qDebug("No pending files, nothing to do!\n"); return; } //Create process, if not done yet if(!m_process) { m_process = new QProcess(); m_process->setProcessChannelMode(QProcess::MergedChannels); m_process->setReadChannel(QProcess::StandardOutput); connect(m_process, SIGNAL(readyReadStandardError()), this, SLOT(outputAvailable())); connect(m_process, SIGNAL(readyReadStandardOutput()), this, SLOT(outputAvailable())); connect(m_process, SIGNAL(finished(int,QProcess::ExitStatus)), this, SLOT(processFinished())); connect(m_process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(processFinished())); } //Still running? if(m_process->state() != QProcess::NotRunning) { qWarning("Process is still running!\n"); return; } //Clear data ui->textBrowser->clear(); m_outputLines.clear(); //Disable buttons ui->analyzeButton->setEnabled(false); ui->exitButton->setEnabled(false); ui->actionClear->setEnabled(false); ui->actionCopyToClipboard->setEnabled(false); ui->actionSave->setEnabled(false); ui->actionOpen->setEnabled(false); //Show banner m_floatingLabel->show(); m_floatingLabel->setText(QString::fromLatin1(STATUS_WORK)); m_floatingLabel->setCursor(Qt::WaitCursor); //Trigger GUI update QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); //Give it a go! QTimer::singleShot(0, this, SLOT(analyzeNextFile())); } void CMainWindow::analyzeNextFile(void) { if(m_pendingFiles.isEmpty()) { qWarning("Oups, no pending files!"); return; } //Lookup MediaInfo path const QString mediaInfoPath = getMediaInfoPath(); if(mediaInfoPath.isEmpty()) { ui->textBrowser->setHtml(QString("<pre>%1</pre>").arg(tr("Oups, failed to extract MediaInfo binary!"))); QMessageBox::critical(this, tr("Failure"), tr("Error: Failed to extract MediaInfo binary!"), QMessageBox::Ok); m_floatingLabel->hide(); ui->actionOpen->setEnabled(true); ui->analyzeButton->setEnabled(true); ui->exitButton->setEnabled(true); return; } const QString filePath = m_pendingFiles.takeFirst(); //Start analyziation qDebug("Analyzing media file:\n%s\n", filePath.toUtf8().constData()); m_process->start(mediaInfoPath, QStringList() << QDir::toNativeSeparators(filePath)); //Wait for process to start if(!m_process->waitForStarted()) { qWarning("Process failed to start:\n%s\n", m_process->errorString().toLatin1().constData()); ui->textBrowser->setHtml(QString("<pre>%1</pre>").arg(tr("Oups, failed to create MediaInfo process!"))); QMessageBox::critical(this, tr("Failure"), tr("Error: Failed to create MediaInfo process!"), QMessageBox::Ok); m_floatingLabel->hide(); ui->actionOpen->setEnabled(true); ui->analyzeButton->setEnabled(true); ui->exitButton->setEnabled(true); return; } qDebug("Process started successfully (PID: %u)", m_process->pid()->dwProcessId); } void CMainWindow::analyzeButtonClicked(void) { if(m_process && (m_process->state() != QProcess::NotRunning)) { qWarning("Process is still running!\n"); return; } const QStringList selectedFiles = QFileDialog::getOpenFileNames(this, tr("Select file to analyze..."), QString(), tr("All supported media files (*.*)")); if(!selectedFiles.isEmpty()) { m_pendingFiles.clear(); m_pendingFiles << selectedFiles; analyzeFiles(); } } void CMainWindow::saveButtonClicked(void) { if(m_process && (m_process->state() != QProcess::NotRunning)) { qWarning("Process is still running!\n"); return; } const QString selectedFile = QFileDialog::getSaveFileName(this, tr("Select file to save..."), QString(), tr("Plain Text (*.txt)")); if(!selectedFile.isEmpty()) { QFile file(selectedFile); if(file.open(QIODevice::WriteOnly | QIODevice::WriteOnly)) { file.write(m_outputLines.join("\r\n").toUtf8()); file.close(); MessageBeep(MB_ICONINFORMATION); } else { QMessageBox::critical(this, tr("Failure"), tr("Error: Failed to open the file writing!"), QMessageBox::Ok); } } } void CMainWindow::copyToClipboardButtonClicked(void) { if(m_process && (m_process->state() != QProcess::NotRunning)) { qWarning("Process is still running!\n"); return; } if(QClipboard *clipboard = QApplication::clipboard()) { clipboard->setText(m_outputLines.join("\n")); MessageBeep(MB_ICONINFORMATION); } } void CMainWindow::clearButtonClicked(void) { if(m_process && (m_process->state() != QProcess::NotRunning)) { qWarning("Process is still running!\n"); return; } //Clear data and re-show banner ui->textBrowser->clear(); m_floatingLabel->setText(QString::fromLatin1(STATUS_BLNK)); m_floatingLabel->setCursor(Qt::ArrowCursor); m_floatingLabel->show(); //Disable actions ui->actionClear->setEnabled(false); ui->actionCopyToClipboard->setEnabled(false); ui->actionSave->setEnabled(false); } void CMainWindow::outputAvailable(void) { if(m_process) { //Update lines while(m_process->canReadLine()) { QString line = QString::fromUtf8(m_process->readLine()).trimmed(); if(!(line.isEmpty() && m_outputLines.empty())) { m_outputLines << line; } } } } void CMainWindow::processFinished(void) { //Fetch any remaining data outputAvailable(); //Remove trailing blank lines while((!m_outputLines.isEmpty()) && m_outputLines.last().trimmed().isEmpty()) { m_outputLines.takeLast(); } //Failed? if(m_outputLines.empty()) { m_outputLines << tr("Oups, apparently MediaInfo encountered a problem!"); } //Append one last linebreak m_outputLines << ""; //Scroll up ui->textBrowser->verticalScrollBar()->setValue(0); ui->textBrowser->horizontalScrollBar()->setValue(0); //Check exit code const int exitCode = m_process->exitCode(); qDebug("Process has finished (Code: %d)\n", exitCode); //More files left? if(!m_pendingFiles.isEmpty() && (exitCode == 0)) { if(!m_outputLines.empty()) { m_outputLines << QString().fill('-', 104) << ""; } QTimer::singleShot(0, this, SLOT(analyzeNextFile())); return; } //Hide banner if(m_floatingLabel->isVisible()) m_floatingLabel->hide(); //Convert to HTML QStringList htmlData(m_outputLines); escapeHtmlChars(htmlData); //Highlight headers htmlData.replaceInStrings(QRegExp("^(-+)$"), "<font color=\"darkgray \">\\1</font>"); //Separator lines htmlData.replaceInStrings(QRegExp("^([^:<>]+):(.+)$"), "<font color=\"darkblue\">\\1:</font>\\2"); //Info lines htmlData.replaceInStrings(QRegExp("^([^:<>]+)$"), "<b><font color=\"darkred\">\\1</font></b>"); //Heading lines //Update document ui->textBrowser->setHtml(QString("<pre>%1</pre>").arg(htmlData.join("<br>"))); //Enable actions if(!m_outputLines.empty()) { ui->actionClear->setEnabled(true); ui->actionCopyToClipboard->setEnabled(true); ui->actionSave->setEnabled(true); } ui->actionOpen->setEnabled(true); ui->analyzeButton->setEnabled(true); ui->exitButton->setEnabled(true); } void CMainWindow::linkTriggered(void) { QObject *obj = QObject::sender(); if(QAction *action = dynamic_cast<QAction*>(obj)) { QDesktopServices::openUrl(QUrl(action->data().toString())); } } void CMainWindow::showAboutScreen(void) { if(m_process && (m_process->state() != QProcess::NotRunning)) { qWarning("Process is still running!\n"); return; } const QDate buildDate = mixp_get_build_date(); const QDate curntDate = mixp_get_current_date(); QString text; text += QString().sprintf("<nobr><tt><b>MediaInfoXP v%u.%02u - Simple GUI for MediaInfo</b><br>", mixp_versionMajor, mixp_versionMinor); text += QString().sprintf("Copyright (c) 2004-%04d LoRd_MuldeR <mulder2@gmx.de>. Some rights reserved.<br>", qMax(buildDate.year(),curntDate.year())); text += QString().sprintf("Built on %s at %s, using Qt Framework v%s.<br><br>", buildDate.toString(Qt::ISODate).toLatin1().constData(), mixp_buildTime, qVersion()); text += QString().sprintf("This program is free software: you can redistribute it and/or modify<br>"); text += QString().sprintf("it under the terms of the GNU General Public License <http://www.gnu.org/>.<br>"); text += QString().sprintf("Note that this program is distributed with ABSOLUTELY NO WARRANTY.<br><br>"); text += QString().sprintf("Please check the web-site at <a href=\"%s\">%s</a> for updates !!!<br>", LINK_MULDER, LINK_MULDER); text += QString().sprintf("<hr><br>"); text += QString().sprintf("This application is powered by MediaInfo v%u.%u.%02u<br>", mixp_miVersionMajor, mixp_miVersionMinor, mixp_miVersionPatch); text += QString().sprintf("Free and OpenSource tool for displaying technical information about media files.<br>"); text += QString().sprintf("Copyright (c) 2002-%s MediaArea.net SARL. All rights reserved.<br><br>", &mixp_buildDate[7]); text += QString().sprintf("Redistribution and use is permitted according to the (2-Clause) BSD License.<br>"); text += QString().sprintf("Please see <a href=\"%s\">%s</a> for more information.<br></tt></nobr>", LINK_MEDIAINFO, LINK_MEDIAINFO); QMessageBox aboutBox(this); aboutBox.setIconPixmap(QIcon(":/res/logo.png").pixmap(64,64)); aboutBox.setWindowTitle(tr("About...")); aboutBox.setText(text.replace("-", "−")); if(QPushButton *btn = aboutBox.addButton(tr("About Qt"), QMessageBox::NoRole)) { btn->setIcon(QIcon(":/res/ico_qt.png")); btn->setMinimumWidth(120); } if(QPushButton *btn = aboutBox.addButton(tr("Discard"), QMessageBox::NoRole)) { btn->setIcon(QIcon(":/res/ico_discard.png")); btn->setMinimumWidth(104); aboutBox.setEscapeButton(btn); } forever { const int ret = aboutBox.exec(); if(ret == 0) { QMessageBox::aboutQt(this); continue; } break; } } void CMainWindow::updateSize(void) { if(const QWidget *const viewPort = ui->textBrowser->viewport()) { m_floatingLabel->setGeometry(viewPort->x(), viewPort->y(), viewPort->width(), viewPort->height()); } } //////////////////////////////////////////////////////////// // PRIVATE FUNCTIONS //////////////////////////////////////////////////////////// #define VALIDATE_MEDIAINFO(HANDLE) do \ { \ if(HANDLE != INVALID_HANDLE_VALUE) \ { \ QByteArray buffer(mediaInfoRes.size(), '\0'); DWORD bytesRead = 0; \ if(GetFileSize(HANDLE, NULL) == mediaInfoRes.size()) \ { \ SetFilePointer(HANDLE, 0L, NULL, FILE_BEGIN); \ ReadFile(HANDLE, buffer.data(), mediaInfoRes.size(), &bytesRead, NULL); \ } \ if(memcmp(buffer.constData(), mediaInfoRes.data(), mediaInfoRes.size()) != 0) \ { \ qWarning("MediaInfo binary failed to validate!"); \ m_mediaInfoPath.clear(); \ } \ } \ } \ while(0) QString CMainWindow::getMediaInfoPath(void) { QResource mediaInfoRes(":/res/MediaInfo.i386.exe"); if(!mediaInfoRes.isValid()) { qFatal("MediaInfo resource could not be initialized!"); return QString(); } //Already existsing? if(!m_mediaInfoPath.isEmpty()) { QFileInfo mediaInfoNfo(m_mediaInfoPath); if(!(mediaInfoNfo.exists() && mediaInfoNfo.isFile())) { qWarning("MediaInfo binary does NOT seem to exist any longer!\n"); m_mediaInfoPath.clear(); } } //Validate file content VALIDATE_MEDIAINFO(m_mediaInfoHandle); //Extract MediaInfo if(m_mediaInfoPath.isEmpty()) { qDebug("MediaInfo binary not existing yet, going to extract now...\n"); if(m_mediaInfoHandle != INVALID_HANDLE_VALUE) { CloseHandle(m_mediaInfoHandle); m_mediaInfoHandle = INVALID_HANDLE_VALUE; } QString path = QString("%1/MediaInfo.exe").arg(m_tempFolder); QFile mediaInfoFile(path); if(mediaInfoFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { if(mediaInfoFile.write(reinterpret_cast<const char*>(mediaInfoRes.data()), mediaInfoRes.size()) == mediaInfoRes.size()) { m_mediaInfoPath = path; qDebug("MediaInfo path is:\n%s\n", m_mediaInfoPath.toUtf8().constData()); } else { qWarning("Failed to write data to MediaInfo binary file!\n"); } mediaInfoFile.close(); } else { qWarning("Failed to open MediaInfo binary for writing!\n"); } } //Open file for reading if((!m_mediaInfoPath.isEmpty()) && (m_mediaInfoHandle == INVALID_HANDLE_VALUE)) { m_mediaInfoHandle = CreateFileW(QWCHAR(QDir::toNativeSeparators(m_mediaInfoPath)), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, 0); if(m_mediaInfoHandle == INVALID_HANDLE_VALUE) { qWarning("Failed to open the MediaInfo binary for reading!\n"); m_mediaInfoPath.clear(); } } //Validate file content VALIDATE_MEDIAINFO(m_mediaInfoHandle); return m_mediaInfoPath; } void CMainWindow::escapeHtmlChars(QStringList &strings) { QList<QPair<const QString, const QString>>::ConstIterator iter; for(iter = m_htmlEscape.constBegin(); iter != m_htmlEscape.constEnd(); iter++) { strings.replaceInStrings((*iter).first, (*iter).second); } }