File indexing completed on 2024-05-12 17:18:50

0001 /*
0002  * SPDX-FileCopyrightText: 2009 Peter Penz <peter.penz19@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "informationpanelcontent.h"
0008 
0009 #include <KConfigGroup>
0010 #include <KIO/PreviewJob>
0011 #include <KIconEffect>
0012 #include <KIconLoader>
0013 #include <KJobWidgets>
0014 #include <KLocalizedString>
0015 #include <KSeparator>
0016 #include <KSharedConfig>
0017 #include <KStringHandler>
0018 #include <QPainterPath>
0019 
0020 #include <QIcon>
0021 #include <QStyle>
0022 #include <QTextDocument>
0023 
0024 #include <Baloo/FileMetaDataWidget>
0025 
0026 #include <phonon/BackendCapabilities>
0027 #include <phonon/MediaObject>
0028 
0029 #include <QDialogButtonBox>
0030 #include <QGesture>
0031 #include <QLabel>
0032 #include <QLinearGradient>
0033 #include <QPainter>
0034 #include <QPolygon>
0035 #include <QScrollArea>
0036 #include <QScroller>
0037 #include <QTextLayout>
0038 #include <QTimer>
0039 #include <QVBoxLayout>
0040 
0041 #include "dolphin_informationpanelsettings.h"
0042 #include "phononwidget.h"
0043 #include "pixmapviewer.h"
0044 
0045 const int PLAY_ARROW_SIZE = 24;
0046 const int PLAY_ARROW_BORDER_SIZE = 2;
0047 
0048 InformationPanelContent::InformationPanelContent(QWidget *parent)
0049     : QWidget(parent)
0050     , m_item()
0051     , m_previewJob(nullptr)
0052     , m_outdatedPreviewTimer(nullptr)
0053     , m_preview(nullptr)
0054     , m_phononWidget(nullptr)
0055     , m_nameLabel(nullptr)
0056     , m_metaDataWidget(nullptr)
0057     , m_metaDataArea(nullptr)
0058     , m_isVideo(false)
0059 {
0060     parent->installEventFilter(this);
0061 
0062     // Initialize timer for disabling an outdated preview with a small
0063     // delay. This prevents flickering if the new preview can be generated
0064     // within a very small timeframe.
0065     m_outdatedPreviewTimer = new QTimer(this);
0066     m_outdatedPreviewTimer->setInterval(100);
0067     m_outdatedPreviewTimer->setSingleShot(true);
0068     connect(m_outdatedPreviewTimer, &QTimer::timeout, this, &InformationPanelContent::markOutdatedPreview);
0069 
0070     QVBoxLayout *layout = new QVBoxLayout(this);
0071 
0072     // preview
0073     const int minPreviewWidth = KIconLoader::SizeEnormous + KIconLoader::SizeMedium;
0074 
0075     m_preview = new PixmapViewer(parent);
0076     m_preview->setMinimumWidth(minPreviewWidth);
0077     m_preview->setMinimumHeight(KIconLoader::SizeEnormous);
0078 
0079     m_phononWidget = new PhononWidget(parent);
0080     m_phononWidget->hide();
0081     m_phononWidget->setMinimumWidth(minPreviewWidth);
0082     m_phononWidget->setAutoPlay(InformationPanelSettings::previewsAutoPlay());
0083     connect(m_phononWidget, &PhononWidget::hasVideoChanged, this, &InformationPanelContent::slotHasVideoChanged);
0084 
0085     // name
0086     m_nameLabel = new QLabel(parent);
0087     QFont font = m_nameLabel->font();
0088     font.setBold(true);
0089     m_nameLabel->setFont(font);
0090     m_nameLabel->setTextFormat(Qt::PlainText);
0091     m_nameLabel->setAlignment(Qt::AlignHCenter);
0092     m_nameLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Fixed);
0093     m_nameLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
0094 
0095     const bool previewsShown = InformationPanelSettings::previewsShown();
0096     m_preview->setVisible(previewsShown);
0097 
0098     m_metaDataWidget = new Baloo::FileMetaDataWidget(parent);
0099     m_metaDataWidget->setDateFormat(static_cast<Baloo::DateFormats>(InformationPanelSettings::dateFormat()));
0100     connect(m_metaDataWidget, &Baloo::FileMetaDataWidget::urlActivated, this, &InformationPanelContent::urlActivated);
0101     m_metaDataWidget->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0102     m_metaDataWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
0103 
0104     // Configuration
0105     m_configureLabel = new QLabel(i18nc("@label::textbox", "Select which data should be shown:"), this);
0106     m_configureLabel->setWordWrap(true);
0107     m_configureLabel->setVisible(false);
0108 
0109     m_configureButtons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel);
0110     m_configureButtons->setVisible(false);
0111     connect(m_configureButtons, &QDialogButtonBox::accepted, this, [this]() {
0112         m_metaDataWidget->setConfigurationMode(Baloo::ConfigurationMode::Accept);
0113         m_configureButtons->setVisible(false);
0114         m_configureLabel->setVisible(false);
0115         Q_EMIT configurationFinished();
0116     });
0117     connect(m_configureButtons, &QDialogButtonBox::rejected, this, [this]() {
0118         m_metaDataWidget->setConfigurationMode(Baloo::ConfigurationMode::Cancel);
0119         m_configureButtons->setVisible(false);
0120         m_configureLabel->setVisible(false);
0121         Q_EMIT configurationFinished();
0122     });
0123 
0124     m_metaDataArea = new QScrollArea(parent);
0125     m_metaDataArea->setWidget(m_metaDataWidget);
0126     m_metaDataArea->setWidgetResizable(true);
0127     m_metaDataArea->setFrameShape(QFrame::NoFrame);
0128 
0129     QWidget *viewport = m_metaDataArea->viewport();
0130     QScroller::grabGesture(viewport, QScroller::TouchGesture);
0131     viewport->installEventFilter(this);
0132 
0133     layout->addWidget(m_preview);
0134     layout->addWidget(m_phononWidget);
0135     layout->addWidget(m_nameLabel);
0136     layout->addWidget(new KSeparator());
0137     layout->addWidget(m_configureLabel);
0138     layout->addWidget(m_metaDataArea);
0139     layout->addWidget(m_configureButtons);
0140 
0141     grabGesture(Qt::TapAndHoldGesture);
0142 }
0143 
0144 InformationPanelContent::~InformationPanelContent()
0145 {
0146     InformationPanelSettings::self()->save();
0147 }
0148 
0149 void InformationPanelContent::showItem(const KFileItem &item)
0150 {
0151     // compares item entries, comparing items only compares urls
0152     if (m_item.entry() != item.entry()) {
0153         m_item = item;
0154         m_preview->stopAnimatedImage();
0155         refreshMetaData();
0156     }
0157 
0158     refreshPreview();
0159 }
0160 
0161 void InformationPanelContent::refreshPixmapView()
0162 {
0163     // If there is a preview job, kill it to prevent that we have jobs for
0164     // multiple items running, and thus a race condition (bug 250787).
0165     if (m_previewJob) {
0166         m_previewJob->kill();
0167     }
0168 
0169     // try to get a preview pixmap from the item...
0170 
0171     // Mark the currently shown preview as outdated. This is done
0172     // with a small delay to prevent a flickering when the next preview
0173     // can be shown within a short timeframe.
0174     m_outdatedPreviewTimer->start();
0175 
0176     const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings");
0177     const QStringList plugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins());
0178     m_previewJob = new KIO::PreviewJob(KFileItemList() << m_item, QSize(m_preview->width(), m_preview->height()), &plugins);
0179     m_previewJob->setScaleType(KIO::PreviewJob::Unscaled);
0180     m_previewJob->setIgnoreMaximumSize(m_item.isLocalFile() && !m_item.isSlow());
0181     m_previewJob->setDevicePixelRatio(devicePixelRatioF());
0182     if (m_previewJob->uiDelegate()) {
0183         KJobWidgets::setWindow(m_previewJob, this);
0184     }
0185 
0186     connect(m_previewJob.data(), &KIO::PreviewJob::gotPreview, this, &InformationPanelContent::showPreview);
0187     connect(m_previewJob.data(), &KIO::PreviewJob::failed, this, &InformationPanelContent::showIcon);
0188 }
0189 
0190 void InformationPanelContent::refreshPreview()
0191 {
0192     // If there is a preview job, kill it to prevent that we have jobs for
0193     // multiple items running, and thus a race condition (bug 250787).
0194     if (m_previewJob) {
0195         m_previewJob->kill();
0196     }
0197 
0198     m_preview->setCursor(Qt::ArrowCursor);
0199     setNameLabelText(m_item.text());
0200     if (InformationPanelSettings::previewsShown()) {
0201         const QUrl itemUrl = m_item.url();
0202         const bool isSearchUrl = itemUrl.scheme().contains(QLatin1String("search")) && m_item.localPath().isEmpty();
0203         if (isSearchUrl) {
0204             m_preview->show();
0205             m_phononWidget->hide();
0206 
0207             // in the case of a search-URL the URL is not readable for humans
0208             // (at least not useful to show in the Information Panel)
0209             m_preview->setPixmap(QIcon::fromTheme(QStringLiteral("baloo")).pixmap(m_preview->height(), m_preview->width()));
0210         } else {
0211             refreshPixmapView();
0212 
0213             const QString mimeType = m_item.mimetype();
0214             const bool isAnimatedImage = m_preview->isAnimatedMimeType(mimeType);
0215             m_isVideo = !isAnimatedImage && mimeType.startsWith(QLatin1String("video/"));
0216             bool usePhonon = m_isVideo || mimeType.startsWith(QLatin1String("audio/"));
0217 
0218             if (usePhonon) {
0219                 // change the cursor of the preview
0220                 m_preview->setCursor(Qt::PointingHandCursor);
0221                 m_preview->installEventFilter(m_phononWidget);
0222                 m_phononWidget->show();
0223 
0224                 // if the video is playing, has been paused or stopped
0225                 // we don't need to update the preview/phonon widget states
0226                 // unless the previewed file has changed,
0227                 // or the setting previewshown has changed
0228                 if ((m_phononWidget->state() != Phonon::State::PlayingState && m_phononWidget->state() != Phonon::State::PausedState
0229                      && m_phononWidget->state() != Phonon::State::StoppedState)
0230                     || m_item.targetUrl() != m_phononWidget->url() || (!m_preview->isVisible() && !m_phononWidget->isVisible())) {
0231                     if (InformationPanelSettings::previewsAutoPlay() && m_isVideo) {
0232                         // hides the preview now to avoid flickering when the autoplay video starts
0233                         m_preview->hide();
0234                     } else {
0235                         // the video won't play before the preview is displayed
0236                         m_preview->show();
0237                     }
0238 
0239                     m_phononWidget->setUrl(m_item.targetUrl(), m_isVideo ? PhononWidget::MediaKind::Video : PhononWidget::MediaKind::Audio);
0240                     adjustWidgetSizes(parentWidget()->width());
0241                 }
0242             } else {
0243                 if (isAnimatedImage) {
0244                     m_preview->setAnimatedImageFileName(itemUrl.toLocalFile());
0245                 }
0246                 // When we don't need it, hide the phonon widget first to avoid flickering
0247                 m_phononWidget->hide();
0248                 m_preview->show();
0249                 m_preview->removeEventFilter(m_phononWidget);
0250                 m_phononWidget->clearUrl();
0251             }
0252         }
0253     } else {
0254         m_preview->stopAnimatedImage();
0255         m_preview->hide();
0256         m_phononWidget->hide();
0257     }
0258 }
0259 
0260 void InformationPanelContent::configureShownProperties()
0261 {
0262     m_configureLabel->setVisible(true);
0263     m_configureButtons->setVisible(true);
0264     m_metaDataWidget->setConfigurationMode(Baloo::ConfigurationMode::ReStart);
0265 }
0266 
0267 void InformationPanelContent::refreshMetaData()
0268 {
0269     m_metaDataWidget->setDateFormat(static_cast<Baloo::DateFormats>(InformationPanelSettings::dateFormat()));
0270     m_metaDataWidget->show();
0271     m_metaDataWidget->setItems(KFileItemList() << m_item);
0272 }
0273 
0274 void InformationPanelContent::showItems(const KFileItemList &items)
0275 {
0276     // If there is a preview job, kill it to prevent that we have jobs for
0277     // multiple items running, and thus a race condition (bug 250787).
0278     if (m_previewJob) {
0279         m_previewJob->kill();
0280     }
0281 
0282     m_preview->stopAnimatedImage();
0283 
0284     m_preview->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-information")).pixmap(m_preview->height(), m_preview->width()));
0285     setNameLabelText(i18ncp("@label", "%1 item selected", "%1 items selected", items.count()));
0286 
0287     m_metaDataWidget->setItems(items);
0288 
0289     m_phononWidget->hide();
0290 
0291     m_item = KFileItem();
0292 }
0293 
0294 bool InformationPanelContent::eventFilter(QObject *obj, QEvent *event)
0295 {
0296     switch (event->type()) {
0297     case QEvent::Resize: {
0298         QResizeEvent *resizeEvent = static_cast<QResizeEvent *>(event);
0299         if (obj == m_metaDataArea->viewport()) {
0300             // The size of the meta text area has changed. Adjust the fixed
0301             // width in a way that no horizontal scrollbar needs to be shown.
0302             m_metaDataWidget->setFixedWidth(resizeEvent->size().width());
0303         } else if (obj == parent()) {
0304             adjustWidgetSizes(resizeEvent->size().width());
0305         }
0306         break;
0307     }
0308 
0309     case QEvent::Polish:
0310         adjustWidgetSizes(parentWidget()->width());
0311         break;
0312 
0313     case QEvent::FontChange:
0314         m_metaDataWidget->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0315         break;
0316 
0317     default:
0318         break;
0319     }
0320 
0321     return QWidget::eventFilter(obj, event);
0322 }
0323 
0324 bool InformationPanelContent::event(QEvent *event)
0325 {
0326     if (event->type() == QEvent::Gesture) {
0327         gestureEvent(static_cast<QGestureEvent *>(event));
0328         return true;
0329     }
0330     return QWidget::event(event);
0331 }
0332 
0333 bool InformationPanelContent::gestureEvent(QGestureEvent *event)
0334 {
0335     if (!underMouse()) {
0336         return false;
0337     }
0338 
0339     QTapAndHoldGesture *tap = static_cast<QTapAndHoldGesture *>(event->gesture(Qt::TapAndHoldGesture));
0340 
0341     if (tap) {
0342         if (tap->state() == Qt::GestureFinished) {
0343             Q_EMIT contextMenuRequested(tap->position().toPoint());
0344         }
0345         event->accept();
0346         return true;
0347     }
0348     return false;
0349 }
0350 
0351 void InformationPanelContent::showIcon(const KFileItem &item)
0352 {
0353     m_outdatedPreviewTimer->stop();
0354     QPixmap pixmap = QIcon::fromTheme(item.iconName()).pixmap(m_preview->size(), devicePixelRatioF());
0355     KIconLoader::global()->drawOverlays(item.overlays(), pixmap, KIconLoader::Desktop);
0356     m_preview->setPixmap(pixmap);
0357 }
0358 
0359 void InformationPanelContent::showPreview(const KFileItem &item, const QPixmap &pixmap)
0360 {
0361     m_outdatedPreviewTimer->stop();
0362 
0363     QPixmap p = pixmap;
0364     KIconLoader::global()->drawOverlays(item.overlays(), p, KIconLoader::Desktop);
0365 
0366     if (m_isVideo) {
0367         // adds a play arrow overlay
0368 
0369         auto maxDim = qMax(p.width(), p.height());
0370         auto arrowSize = qMax(PLAY_ARROW_SIZE, maxDim / 8);
0371 
0372         // compute relative pixel positions
0373         const int zeroX = static_cast<int>((p.width() / 2 - arrowSize / 2) / pixmap.devicePixelRatio());
0374         const int zeroY = static_cast<int>((p.height() / 2 - arrowSize / 2) / pixmap.devicePixelRatio());
0375 
0376         QPolygon arrow;
0377         arrow << QPoint(zeroX, zeroY);
0378         arrow << QPoint(zeroX, zeroY + arrowSize);
0379         arrow << QPoint(zeroX + arrowSize, zeroY + arrowSize / 2);
0380 
0381         QPainterPath path;
0382         path.addPolygon(arrow);
0383 
0384         QLinearGradient gradient(QPointF(zeroX, zeroY + arrowSize / 2), QPointF(zeroX + arrowSize, zeroY + arrowSize / 2));
0385 
0386         QColor whiteColor = Qt::white;
0387         QColor blackColor = Qt::black;
0388         gradient.setColorAt(0, whiteColor);
0389         gradient.setColorAt(1, blackColor);
0390 
0391         QBrush brush(gradient);
0392 
0393         QPainter painter(&p);
0394 
0395         QPen pen(blackColor, PLAY_ARROW_BORDER_SIZE, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
0396         painter.setPen(pen);
0397 
0398         painter.setRenderHint(QPainter::Antialiasing);
0399         painter.drawPolygon(arrow);
0400         painter.fillPath(path, brush);
0401     }
0402 
0403     m_preview->setPixmap(p);
0404 }
0405 
0406 void InformationPanelContent::markOutdatedPreview()
0407 {
0408     if (m_item.isDir()) {
0409         // directory preview can be long
0410         // but since we always have icons to display
0411         // use it until the preview is done
0412         showIcon(m_item);
0413     } else {
0414         KIconEffect *iconEffect = KIconLoader::global()->iconEffect();
0415         QPixmap disabledPixmap = iconEffect->apply(m_preview->pixmap(), KIconLoader::Desktop, KIconLoader::DisabledState);
0416         m_preview->setPixmap(disabledPixmap);
0417     }
0418 }
0419 
0420 KFileItemList InformationPanelContent::items()
0421 {
0422     return m_metaDataWidget->items();
0423 }
0424 
0425 void InformationPanelContent::slotHasVideoChanged(bool hasVideo)
0426 {
0427     m_preview->setVisible(InformationPanelSettings::previewsShown() && !hasVideo);
0428     if (m_preview->isVisible() && m_preview->size().width() != m_preview->pixmap().size().width()) {
0429         // in case the information panel has been resized when the preview was not displayed
0430         // we need to refresh its content
0431         refreshPixmapView();
0432     }
0433 }
0434 
0435 void InformationPanelContent::setPreviewAutoPlay(bool autoPlay)
0436 {
0437     m_phononWidget->setAutoPlay(autoPlay);
0438 }
0439 
0440 void InformationPanelContent::setNameLabelText(const QString &text)
0441 {
0442     QTextOption textOption;
0443     textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere);
0444 
0445     const QString processedText = Qt::mightBeRichText(text) ? text : KStringHandler::preProcessWrap(text);
0446 
0447     QTextLayout textLayout(processedText);
0448     textLayout.setFont(m_nameLabel->font());
0449     textLayout.setTextOption(textOption);
0450 
0451     QString wrappedText;
0452     wrappedText.reserve(processedText.length());
0453 
0454     // wrap the text to fit into the width of m_nameLabel
0455     textLayout.beginLayout();
0456     QTextLine line = textLayout.createLine();
0457     while (line.isValid()) {
0458         line.setLineWidth(m_nameLabel->width());
0459         wrappedText += QStringView(processedText).mid(line.textStart(), line.textLength());
0460 
0461         line = textLayout.createLine();
0462         if (line.isValid()) {
0463             wrappedText += QChar::LineSeparator;
0464         }
0465     }
0466     textLayout.endLayout();
0467 
0468     m_nameLabel->setText(wrappedText);
0469 }
0470 
0471 void InformationPanelContent::adjustWidgetSizes(int width)
0472 {
0473     // If the text inside the name label or the info label cannot
0474     // get wrapped, then the maximum width of the label is increased
0475     // so that the width of the information panel gets increased.
0476     // To prevent this, the maximum width is adjusted to
0477     // the current width of the panel.
0478     const int maxWidth = width - style()->layoutSpacing(QSizePolicy::DefaultType, QSizePolicy::DefaultType, Qt::Horizontal) * 4;
0479     m_nameLabel->setMaximumWidth(maxWidth);
0480 
0481     // The metadata widget also contains a text widget which may return
0482     // a large preferred width.
0483     m_metaDataWidget->setMaximumWidth(maxWidth);
0484 
0485     // try to increase the preview as large as possible
0486     m_preview->setSizeHint(QSize(maxWidth, maxWidth));
0487 
0488     if (m_phononWidget->isVisible()) {
0489         // assure that the size of the video player is the same as the preview size
0490         m_phononWidget->setVideoSize(QSize(maxWidth, maxWidth));
0491     }
0492 }
0493 
0494 #include "moc_informationpanelcontent.cpp"