#include "./picturepreviewselection.h" #include "../application/settings.h" #include "../misc/utility.h" #include "ui_picturepreviewselection.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 #include #include #include using namespace std; using namespace Media; using namespace ConversionUtilities; namespace QtGui { void PicturePreviewScene::mousePressEvent(QGraphicsSceneMouseEvent *mouseEvent) { emit displayRequested(); mouseEvent->accept(); } void PicturePreview::mousePressEvent(QMouseEvent *mouseEvent) { emit displayRequested(); mouseEvent->accept(); } /*! * \brief Constructs a new PicturePreviewSelection for the specified \a tag and \a field. */ PicturePreviewSelection::PicturePreviewSelection(Tag *tag, KnownField field, QWidget *parent) : QWidget(parent), m_ui(new Ui::PicturePreviewSelection), m_scene(nullptr), m_textItem(nullptr), m_pixmapItem(nullptr), m_rectItem(nullptr), m_tag(tag), m_field(field), m_currentTypeIndex(0) { m_ui->setupUi(this); m_ui->coverButtonsWidget->setHidden(Settings::values().editor.hideCoverButtons); connect(m_ui->addButton, &QPushButton::clicked, this, static_cast(&PicturePreviewSelection::addOfSelectedType)); connect(m_ui->removeButton, &QPushButton::clicked, this, &PicturePreviewSelection::removeSelected); connect(m_ui->extractButton, &QPushButton::clicked, this, &PicturePreviewSelection::extractSelected); connect(m_ui->displayButton, &QPushButton::clicked, this, &PicturePreviewSelection::displaySelected); connect(m_ui->restoreButton, &QPushButton::clicked, std::bind(&PicturePreviewSelection::setup, this, PreviousValueHandling::Clear)); connect(m_ui->previewGraphicsView, &QGraphicsView::customContextMenuRequested, this, &PicturePreviewSelection::showContextMenu); setup(); setAcceptDrops(true); } /*! * \brief Destroys the instance. */ PicturePreviewSelection::~PicturePreviewSelection() {} /*! * \brief Sets the \a value of the current tag field manually using the given \a previousValueHandling. */ void PicturePreviewSelection::setValue(const TagValue &value, PreviousValueHandling previousValueHandling) { assert(m_currentTypeIndex < m_values.size()); TagValue ¤tValue = m_values[m_currentTypeIndex]; if(previousValueHandling == PreviousValueHandling::Clear || !value.isEmpty()) { if(previousValueHandling != PreviousValueHandling::Keep || currentValue.isEmpty()) { currentValue = value; // TODO: move(value); emit pictureChanged(); } } updatePreview(m_currentTypeIndex); } /*! * \brief Defines the predicate to get relevant fields. */ template bool fieldPredicate(int i, const std::pair &pair) { return pair.second.isTypeInfoAssigned() ? (pair.second.typeInfo() == static_cast(i)) : (i == 0); } /*! * \brief Fetches the ID3v2 cover values from the specified \a tag. * \param tag Specifies a tag to get the values from. * \param field Specifies the field. * \param values Specifies a list to push the fetched values in. * \param valueCount Specifies the number of cover types. * \param previousValueHandling Specifies the "previous value handling policy". */ template int fetchId3v2CoverValues(const TagType *tag, KnownField field, QList &values, const int valueCount, const PreviousValueHandling previousValueHandling) { values.reserve(valueCount); int first = -1; const auto &fields = tag->fields(); auto range = fields.equal_range(tag->fieldId(field)); for(int i = 0; i < valueCount; ++i) { if(i >= values.size()) { values << TagValue(); } auto pair = find_if(range.first, range.second, std::bind(fieldPredicate, i, placeholders::_1)); if(pair != range.second) { const TagValue &value = pair->second.value(); if((previousValueHandling == PreviousValueHandling::Clear || !value.isEmpty()) && (previousValueHandling != PreviousValueHandling::Keep || values[i].isEmpty())) { values[i] = value; } } else if(previousValueHandling == PreviousValueHandling::Clear) { values[i].clearDataAndMetadata(); } if(first < 0 && !values[i].isEmpty()) { first = i; } } values.erase(values.begin() + valueCount, values.end()); return first; } /*! * \brief Sets the widget up. */ void PicturePreviewSelection::setup(PreviousValueHandling previousValueHandling) { if(previousValueHandling == PreviousValueHandling::Auto) { previousValueHandling = PreviousValueHandling::Update; } m_currentTypeIndex = 0; if(m_tag) { if(m_field == KnownField::Cover && (m_tag->type() == TagType::Id3v2Tag || m_tag->type() == TagType::VorbisComment)) { m_ui->switchTypeComboBox->setHidden(false); m_ui->switchTypeLabel->setHidden(false); if(!m_ui->switchTypeComboBox->count()) { m_ui->switchTypeComboBox->addItems(QStringList() << tr("Other") << tr("32x32 File icon") << tr("Other file icon") << tr("Cover (front)") << tr("Cover (back)") << tr("Leaflet page") << tr("Media (e. g. label side of CD)") << tr("Lead artist/performer/soloist") << tr("Artist/performer") << tr("Conductor") << tr("Band/Orchestra") << tr("Composer") << tr("Lyricist/text writer") << tr("Recording Location") << tr("During recording") << tr("During performance") << tr("Movie/video screen capture") << tr("A bright coloured fish") << tr("Illustration") << tr("Band/artist logotype") << tr("Publisher/Studio logotype") ); } int first; switch(m_tag->type()) { case TagType::Id3v2Tag: first = fetchId3v2CoverValues(static_cast(m_tag), m_field, m_values, m_ui->switchTypeComboBox->count(), previousValueHandling); break; case TagType::VorbisComment: case TagType::OggVorbisComment: first = fetchId3v2CoverValues(static_cast(m_tag), m_field, m_values, m_ui->switchTypeComboBox->count(), previousValueHandling); break; default: first = 0; } if(first >= 0) { m_ui->switchTypeComboBox->setCurrentIndex(first); } m_currentTypeIndex = m_ui->switchTypeComboBox->currentIndex(); connect(m_ui->switchTypeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &PicturePreviewSelection::typeSwitched); } else { m_ui->switchTypeComboBox->setHidden(true); m_ui->switchTypeLabel->setHidden(true); m_currentTypeIndex = 0; const TagValue &value = m_tag->value(m_field); if(previousValueHandling == PreviousValueHandling::Clear || !value.isEmpty()) { if(m_values.size()) { if(previousValueHandling != PreviousValueHandling::Keep || m_values[0].isEmpty()) { m_values[0] = value; } } else { m_values << value; } } if(m_values.size()) { m_values.erase(m_values.begin() + 1, m_values.end()); } else { m_values << TagValue(); } } bool hideDesc = !m_tag->supportsDescription(m_field); m_ui->descriptionLineEdit->setHidden(hideDesc); m_ui->descriptionLabel->setHidden(hideDesc); updatePreview(m_currentTypeIndex); updateDescription(m_currentTypeIndex); setEnabled(true); } else { m_currentTypeIndex = 0; setEnabled(false); } } /*! * \brief Pushes the ID3v2 cover values to the specified \a tag. * \param tag Specifies a tag to push the values to. * \param field Specifies the field. * \param values Specifies the values to be pushed. */ template void pushId3v2CoverValues(TagType *tag, KnownField field, const QList &values) { auto &fields = tag->fields(); const auto id = tag->fieldId(field); const auto range = fields.equal_range(id); const auto first = range.first; // iterate through all tag values for(int index = 0, valueCount = values.size(); index < valueCount; ++index) { // check whether there is already a tag value with the current index/type auto pair = find_if(first, range.second, std::bind(fieldPredicate, index, placeholders::_1)); if(pair != range.second) { // there is already a tag value with the current index/type // -> update this value pair->second.setValue(values[index]); // check whether there are more values with the current index/type assigned while((pair = find_if(++pair, range.second, std::bind(fieldPredicate, index, placeholders::_1))) != range.second) { // -> remove these values as we only support one value of a type in the same tag pair->second.setValue(TagValue()); } } else if(!values[index].isEmpty()) { typename TagType::fieldType field(id, values[index]); field.setTypeInfo(static_cast(index)); fields.insert(std::make_pair(id, field)); } } } /*! * \brief Applies the selection to the tag. */ void PicturePreviewSelection::apply() { if(m_tag) { if(m_field == KnownField::Cover && (m_tag->type() == TagType::Id3v2Tag || m_tag->type() == TagType::VorbisComment)) { switch(m_tag->type()) { case TagType::Id3v2Tag: pushId3v2CoverValues(static_cast(m_tag), m_field, m_values); break; case TagType::VorbisComment: pushId3v2CoverValues(static_cast(m_tag), m_field, m_values); break; default: ; } } else { if(m_values.size()) { m_tag->setValue(m_field, m_values.first()); } else { m_tag->setValue(m_field, TagValue()); } } } } /*! * \brief Clears all selected pictures (without applying the change to the tag). */ void PicturePreviewSelection::clear() { for(int i = 0, count = m_values.count(); i < count; ++i) { m_values[i].clearDataAndMetadata(); } updatePreview(m_currentTypeIndex); updateDescription(m_currentTypeIndex); } /*! * \brief Asks the user to add a picture of the selected type. */ void PicturePreviewSelection::addOfSelectedType() { assert(m_currentTypeIndex < m_values.size()); QString path = QFileDialog::getOpenFileName(this, tr("Select a picture to add as cover")); if(!path.isEmpty()) { addOfSelectedType(path); } } /*! * \brief Adds the picture from the specified \a path of the selected type. */ void PicturePreviewSelection::addOfSelectedType(const QString &path) { assert(m_currentTypeIndex < m_values.size()); TagValue &selectedCover = m_values[m_currentTypeIndex]; try { MediaFileInfo fileInfo(toNativeFileName(path).constData()); fileInfo.open(true); fileInfo.parseContainerFormat(); auto mimeType = QString::fromUtf8(fileInfo.mimeType()); bool ok; mimeType = QInputDialog::getText(this, tr("Enter/confirm mime type"), tr("Confirm or enter the mime type of the selected file."), QLineEdit::Normal, mimeType, &ok); if(ok) { if((fileInfo.size() < 10485760) || (QMessageBox::warning(this, QApplication::applicationName(), tr("The selected file is very large (for a cover). Do you want to continue?"), QMessageBox::Yes, QMessageBox::No) == QMessageBox::No)) { auto buff = make_unique(fileInfo.size()); fileInfo.stream().seekg(0); fileInfo.stream().read(buff.get(), fileInfo.size()); selectedCover.assignData(std::move(buff), fileInfo.size(), TagDataType::Picture); selectedCover.setMimeType(mimeType.toUtf8().constData()); emit pictureChanged(); } } } catch (const Media::Failure &) { QMessageBox::critical(this, QApplication::applicationName(), tr("Unable to parse specified cover file.")); } catch(...) { ::IoUtilities::catchIoFailure(); QMessageBox::critical(this, QApplication::applicationName(), tr("An IO error occured when parsing the specified cover file.")); } updatePreview(m_currentTypeIndex); } /*! * \brief Removes the selected picture. */ void PicturePreviewSelection::removeSelected() { if(m_currentTypeIndex < m_values.size()) { if(m_values[m_currentTypeIndex].isEmpty()) { QMessageBox::information(this, QApplication::applicationName(), tr("There is no cover to remove.")); } else { m_values[m_currentTypeIndex].clearData(); updatePreview(m_currentTypeIndex); emit pictureChanged(); } } else { throw logic_error("Invalid type selected (no corresponding value assigned)."); } } /*! * \brief Extracts the selected picture. */ void PicturePreviewSelection::extractSelected() { assert(m_currentTypeIndex < m_values.size()); TagValue &value = m_values[m_currentTypeIndex]; if(value.isEmpty()) { QMessageBox::information(this, QApplication::applicationName(), tr("There is no image attached to be extracted.")); } else { const auto path = QFileDialog::getSaveFileName(this, tr("Where do you want to save the cover?")); if(!path.isEmpty()) { QFile file(path); if(file.open(QIODevice::WriteOnly)) { if(file.write(value.dataPointer(), value.dataSize()) > 0) { QMessageBox::information(this, QApplication::applicationName(), tr("The cover has extracted.")); } else { QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to write to output file.")); } file.close(); } else { QMessageBox::warning(this, QApplication::applicationName(), tr("Unable to open output file.")); } } } } /*! * \brief Displays the selected picture in a modal dialog. */ void PicturePreviewSelection::displaySelected() { assert(m_currentTypeIndex < m_values.size()); TagValue &value = m_values[m_currentTypeIndex]; if(!value.isEmpty()) { QImage img; if(value.mimeType() == "-->") { QFile file(Utility::stringToQString(value.toString(), value.dataEncoding())); if(file.open(QFile::ReadOnly)) { img = QImage::fromData(file.readAll()); } else { QMessageBox::warning(this, QApplication::applicationName(), tr("The attached image can't be found.")); return; } } else { img = QImage::fromData(reinterpret_cast(value.dataPointer()), value.dataSize()); } if(img.isNull()) { QMessageBox::warning(this, QApplication::applicationName(), tr("The attached image can't be displayed.")); } else { QDialog dlg; dlg.setWindowFlags(Qt::Tool); dlg.setWindowTitle(tr("Cover - %1").arg(QApplication::applicationName())); QBoxLayout layout(QBoxLayout::Up); layout.setMargin(0); QGraphicsView view(&dlg); QGraphicsScene scene; layout.addWidget(&view); scene.addItem(new QGraphicsPixmapItem(QPixmap::fromImage(img))); view.setScene(&scene); view.show(); dlg.setLayout(&layout); dlg.exec(); } } else { QMessageBox::warning(this, QApplication::applicationName(), tr("There is no image attached.")); } } /*! * \brief Asks the user to alter the MIME-type of the selected cover. */ void PicturePreviewSelection::changeMimeTypeOfSelected() { assert(m_currentTypeIndex < m_values.size()); TagValue &selectedCover = m_values[m_currentTypeIndex]; auto mimeType = QString::fromUtf8(selectedCover.mimeType().data()); bool ok; mimeType = QInputDialog::getText(this, tr("Enter/confirm mime type"), tr("Confirm or enter the mime type of the selected file."), QLineEdit::Normal, mimeType, &ok); if(ok) { selectedCover.setMimeType(mimeType.toUtf8().data()); } } /*! * \brief Sets whether cover buttons are hidden. */ void PicturePreviewSelection::setCoverButtonsHidden(bool hideCoverButtons) { m_ui->coverButtonsWidget->setHidden(hideCoverButtons); } void PicturePreviewSelection::changeEvent(QEvent *event) { switch(event->type()) { case QEvent::EnabledChange: if(m_rectItem) { m_rectItem->setVisible(!isEnabled()); } break; default: ; } } void PicturePreviewSelection::resizeEvent(QResizeEvent *) { if(m_pixmapItem && !m_pixmap.isNull()) { m_pixmapItem->setPixmap(m_pixmap.scaled(m_ui->previewGraphicsView->size(), Qt::KeepAspectRatio)); } } void PicturePreviewSelection::dragEnterEvent(QDragEnterEvent *event) { const auto *mimeData = event->mimeData(); if(mimeData->hasUrls()) { for(const auto &url : mimeData->urls()) { if(url.scheme() == QLatin1String("file")) { event->accept(); return; } } } event->ignore(); } void PicturePreviewSelection::dropEvent(QDropEvent *event) { const auto *mimeData = event->mimeData(); if(mimeData->hasUrls()) { for(const auto &url : mimeData->urls()) { if(url.scheme() == QLatin1String("file")) { #ifdef Q_OS_WIN32 // remove leading slash QString path = url.path(); int index = 0; for(const auto &c : path) { if(c == QChar('/')) { ++index; } else { break; } } if(index) { path = path.mid(index); } addOfSelectedType(path); #else addOfSelectedType(url.path()); #endif event->accept(); return; } } } event->ignore(); } void PicturePreviewSelection::mouseDoubleClickEvent(QMouseEvent *event) { displaySelected(); event->accept(); } bool PicturePreviewSelection::eventFilter(QObject *watched, QEvent *event) { if(watched == m_scene && event->type() == QEvent::MouseButtonDblClick) { displaySelected(); return true; } return QWidget::eventFilter(watched, event); } /*! * \brief Handles selected type being switched. */ void PicturePreviewSelection::typeSwitched(int index) { assert(m_currentTypeIndex < m_values.size()); int lastIndex = m_currentTypeIndex; if(index < 0 || index >= m_values.size()) { throw logic_error("current type index is invalid"); } else { m_currentTypeIndex = index; } updateDescription(lastIndex, index); updatePreview(index); } /*! * \brief Shows the description of type with the specified \a newIndex. */ void PicturePreviewSelection::updateDescription(int newIndex) { // show description of selected type m_ui->descriptionLineEdit->setText(Utility::stringToQString(m_values[newIndex].description(), m_values[newIndex].descriptionEncoding())); } /*! * \brief Shows the description of type with the specified \a newIndex and saves the description of the type with the specified \a lastIndex before. */ void PicturePreviewSelection::updateDescription(int lastIndex, int newIndex) { TagTextEncoding enc; if(m_tag) { TagTextEncoding preferredEncoding = Settings::values().tagPocessing.preferredEncoding; enc = m_tag->canEncodingBeUsed(preferredEncoding) ? preferredEncoding : m_tag->proposedTextEncoding(); } else { enc = m_values[lastIndex].descriptionEncoding(); } // save entered description m_values[lastIndex].setDescription(Utility::qstringToString(m_ui->descriptionLineEdit->text(), enc), enc); // show description of selected type updateDescription(newIndex); } /*! * \brief Updates the preview to show the type with the specified \a index. */ void PicturePreviewSelection::updatePreview(int index) { if(!m_scene) { m_scene = new PicturePreviewScene(m_ui->previewGraphicsView); m_ui->previewGraphicsView->setScene(m_scene); connect(m_scene, &PicturePreviewScene::displayRequested, this, &PicturePreviewSelection::displaySelected); //m_scene->installEventFilter(this); } if(!m_textItem) { m_textItem = new QGraphicsTextItem; m_textItem->setTextWidth(m_ui->previewGraphicsView->width()); m_scene->addItem(m_textItem); } if(!m_pixmapItem) { m_pixmapItem = new QGraphicsPixmapItem; m_scene->addItem(m_pixmapItem); } if(!m_rectItem) { m_rectItem = new QGraphicsRectItem; m_rectItem->setPen(Qt::NoPen); m_rectItem->setBrush(QBrush(QColor(120, 120, 120, 120))); m_rectItem->setVisible(!isEnabled()); m_scene->addItem(m_rectItem); } TagValue &value = m_values[index]; if(value.isEmpty()) { m_textItem->setVisible(true); m_textItem->setPlainText(tr("No image (of the selected type) attached.")); m_pixmapItem->setVisible(false); m_ui->addButton->setText(tr("Add")); } else { QImage img; if(value.mimeType() == "-->") { QFile file(Utility::stringToQString(value.toString(), value.dataEncoding())); if(file.open(QFile::ReadOnly)) img = QImage::fromData(file.readAll()); else { m_textItem->setPlainText(tr("The attached image can't be found.")); m_textItem->setVisible(true); m_pixmapItem->setVisible(false); return; } } else { img = QImage::fromData(reinterpret_cast(value.dataPointer()), value.dataSize()); } if(img.isNull()) { m_pixmap = QPixmap(); m_textItem->setPlainText(tr("Unable to display attached image.")); m_textItem->setVisible(true); m_pixmapItem->setVisible(false); } else { m_textItem->setVisible(false); m_pixmap = QPixmap::fromImage(img); m_pixmapItem->setPixmap(m_pixmap.scaled(m_ui->previewGraphicsView->size(), Qt::KeepAspectRatio)); m_pixmapItem->setVisible(true); } m_ui->addButton->setText(tr("Change")); } m_rectItem->setRect(0, 0, m_ui->previewGraphicsView->width(), m_ui->previewGraphicsView->height()); } void PicturePreviewSelection::showContextMenu() { QMenu menu; QAction *addAction = menu.addAction(m_ui->addButton->text()); addAction->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); connect(addAction, &QAction::triggered, this, static_cast(&PicturePreviewSelection::addOfSelectedType)); if(m_ui->extractButton->isEnabled()) { QAction *mimeAction = menu.addAction(tr("Change MIME-type")); mimeAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); connect(mimeAction, &QAction::triggered, this, &PicturePreviewSelection::changeMimeTypeOfSelected); } menu.addSeparator(); if(m_ui->removeButton->isEnabled()) { QAction *removeAction = menu.addAction(m_ui->removeButton->text()); removeAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-delete"))); connect(removeAction, &QAction::triggered, this, &PicturePreviewSelection::removeSelected); } if(m_ui->restoreButton->isEnabled()) { QAction *restoreAction = menu.addAction(m_ui->restoreButton->text()); restoreAction->setIcon(QIcon::fromTheme(QStringLiteral("document-revert"))); connect(restoreAction, &QAction::triggered, std::bind(&PicturePreviewSelection::setup, this, PreviousValueHandling::Clear)); } menu.addSeparator(); if(m_ui->extractButton->isEnabled()) { QAction *extractAction = menu.addAction(m_ui->extractButton->text()); extractAction->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); connect(extractAction, &QAction::triggered, this, &PicturePreviewSelection::extractSelected); } if(m_ui->displayButton->isEnabled()) { QAction *displayAction = menu.addAction(m_ui->displayButton->text()); displayAction->setIcon(QIcon::fromTheme(QStringLiteral("image-x-generic"))); connect(displayAction, &QAction::triggered, this, &PicturePreviewSelection::displaySelected); } menu.exec(QCursor::pos()); } }