File indexing completed on 2025-01-12 04:25:50
0001 /* 0002 SPDX-FileCopyrightText: 2017 Nicolas Carion 0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 #include "assetlistwidget.hpp" 0007 #include "assets/assetlist/model/assetfilter.hpp" 0008 #include "assets/assetlist/model/assettreemodel.hpp" 0009 #include "assets/assetlist/view/asseticonprovider.hpp" 0010 #include "mltconnection.h" 0011 0012 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0013 #include <KNS3/Entry> 0014 #else 0015 #include <KNSCore/Entry> 0016 #endif 0017 0018 #include <KNSWidgets/Action> 0019 #include <QAction> 0020 #include <QFontDatabase> 0021 #include <QKeyEvent> 0022 #include <QLineEdit> 0023 #include <QMenu> 0024 #include <QSplitter> 0025 #include <QStandardPaths> 0026 #include <QStyledItemDelegate> 0027 #include <QTextBrowser> 0028 #include <QTextDocument> 0029 #include <QToolBar> 0030 #include <QVBoxLayout> 0031 0032 class AssetDelegate : public QStyledItemDelegate 0033 { 0034 public: 0035 using QStyledItemDelegate::QStyledItemDelegate; 0036 0037 protected: 0038 void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const override 0039 { 0040 int favorite = index.data(AssetTreeModel::FavoriteRole).toInt(); 0041 QStyledItemDelegate::initStyleOption(option, index); 0042 if (favorite > 0) { 0043 QFont font(option->font); 0044 /*modify fonts*/ 0045 font.setBold(true); 0046 option->font = font; 0047 option->fontMetrics = QFontMetrics(font); 0048 } 0049 } 0050 }; 0051 0052 AssetListWidget::AssetListWidget(bool isEffect, QWidget *parent) 0053 : QWidget(parent) 0054 , m_isEffect(isEffect) 0055 { 0056 m_lay = new QVBoxLayout(this); 0057 m_lay->setContentsMargins(0, 0, 0, 0); 0058 m_lay->setSpacing(0); 0059 m_contextMenu = new QMenu(this); 0060 QAction *addFavorite = new QAction(i18n("Add to favorites"), this); 0061 addFavorite->setData(QStringLiteral("favorite")); 0062 connect(addFavorite, &QAction::triggered, this, [this]() { setFavorite(m_effectsTree->currentIndex(), !isFavorite(m_effectsTree->currentIndex())); }); 0063 m_contextMenu->addAction(addFavorite); 0064 if (m_isEffect) { 0065 // Delete effect 0066 QAction *customAction = new QAction(i18n("Delete custom effect"), this); 0067 customAction->setData(QStringLiteral("custom")); 0068 connect(customAction, &QAction::triggered, this, [this]() { deleteCustomEffect(m_effectsTree->currentIndex()); }); 0069 m_contextMenu->addAction(customAction); 0070 // reload effect 0071 customAction = new QAction(i18n("Reload custom effect"), this); 0072 customAction->setData(QStringLiteral("custom")); 0073 connect(customAction, &QAction::triggered, this, [this]() { reloadCustomEffectIx(m_effectsTree->currentIndex()); }); 0074 m_contextMenu->addAction(customAction); 0075 // Edit effect 0076 customAction = new QAction(i18n("Edit Info…"), this); 0077 customAction->setData(QStringLiteral("custom")); 0078 connect(customAction, &QAction::triggered, this, [this]() { editCustomAsset(m_effectsTree->currentIndex()); }); 0079 m_contextMenu->addAction(customAction); 0080 // Edit effect 0081 customAction = new QAction(i18n("Export XML…"), this); 0082 customAction->setData(QStringLiteral("custom")); 0083 connect(customAction, &QAction::triggered, this, [this]() { exportCustomEffect(m_effectsTree->currentIndex()); }); 0084 m_contextMenu->addAction(customAction); 0085 } 0086 // Create toolbar for buttons 0087 m_toolbar = new QToolBar(this); 0088 int iconSize = style()->pixelMetric(QStyle::PM_SmallIconSize); 0089 m_toolbar->setIconSize(QSize(iconSize, iconSize)); 0090 m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); 0091 QAction *allEffects = new QAction(this); 0092 allEffects->setIcon(QIcon::fromTheme(QStringLiteral("show-all-effects"))); 0093 allEffects->setToolTip(m_isEffect ? i18n("Main effects") : i18n("Main compositions")); 0094 connect(allEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral()); }); 0095 m_toolbar->addAction(allEffects); 0096 if (m_isEffect) { 0097 QAction *videoEffects = new QAction(this); 0098 videoEffects->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-video"))); 0099 videoEffects->setToolTip(i18n("Show all video effects")); 0100 connect(videoEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("video")); }); 0101 m_toolbar->addAction(videoEffects); 0102 QAction *audioEffects = new QAction(this); 0103 audioEffects->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-show-audio"))); 0104 audioEffects->setToolTip(i18n("Show all audio effects")); 0105 connect(audioEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("audio")); }); 0106 m_toolbar->addAction(audioEffects); 0107 QAction *customEffects = new QAction(this); 0108 customEffects->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-custom-effect"))); 0109 customEffects->setToolTip(i18n("Show all custom effects")); 0110 connect(customEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("custom")); }); 0111 m_toolbar->addAction(customEffects); 0112 } else { 0113 QAction *transOnly = new QAction(this); 0114 transOnly->setIcon(QIcon::fromTheme(QStringLiteral("transform-move-horizontal"))); 0115 transOnly->setToolTip(i18n("Show transitions only")); 0116 connect(transOnly, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("transition")); }); 0117 m_toolbar->addAction(transOnly); 0118 } 0119 QAction *favEffects = new QAction(this); 0120 favEffects->setIcon(QIcon::fromTheme(QStringLiteral("favorite"))); 0121 favEffects->setToolTip(i18n("Show favorite items")); 0122 connect(favEffects, &QAction::triggered, this, [this]() { setFilterType(QStringLiteral("favorites")); }); 0123 m_toolbar->addAction(favEffects); 0124 m_lay->addWidget(m_toolbar); 0125 if (m_isEffect) { 0126 KNSWidgets::Action *downloadAction = new KNSWidgets::Action(i18n("Download New Effects..."), QStringLiteral(":data/kdenlive_effects.knsrc"), this); 0127 m_toolbar->addAction(downloadAction); 0128 connect(downloadAction, &KNSWidgets::Action::dialogFinished, this, [&](const QList<KNSCore::Entry> &changedEntries) { 0129 if (changedEntries.count() > 0) { 0130 for (auto &ent : changedEntries) { 0131 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0132 if (ent.status() == KNS3::Entry::Status::Deleted) { 0133 #else 0134 if (ent.status() == KNSCore::Entry::Status::Deleted) { 0135 #endif 0136 reloadTemplates(); 0137 } else { 0138 QStringList files = ent.installedFiles(); 0139 for (auto &f : files) { 0140 reloadCustomEffect(f); 0141 } 0142 } 0143 } 0144 if (!m_searchLine->text().isEmpty()) { 0145 setFilterName(m_searchLine->text()); 0146 } 0147 } 0148 }); 0149 } else { 0150 KNSWidgets::Action *downloadAction = new KNSWidgets::Action(i18n("Download New Wipes..."), QStringLiteral(":data/kdenlive_wipes.knsrc"), this); 0151 m_toolbar->addAction(downloadAction); 0152 connect(downloadAction, &KNSWidgets::Action::dialogFinished, this, [&](const QList<KNSCore::Entry> &changedEntries) { 0153 if (changedEntries.count() > 0) { 0154 MltConnection::refreshLumas(); 0155 } 0156 }); 0157 } 0158 QWidget *empty = new QWidget(this); 0159 empty->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); 0160 m_toolbar->addWidget(empty); 0161 QAction *showInfo = new QAction(this); 0162 showInfo->setCheckable(true); 0163 showInfo->setIcon(QIcon::fromTheme(QStringLiteral("help-about"))); 0164 showInfo->setToolTip(m_isEffect ? i18n("Show/hide description of the effects") : i18n("Show/hide description of the compositions")); 0165 m_toolbar->addAction(showInfo); 0166 m_lay->addWidget(m_toolbar); 0167 0168 // Search line 0169 m_searchLine = new QLineEdit(this); 0170 m_searchLine->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); 0171 // m_searchLine->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); 0172 m_searchLine->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); 0173 m_searchLine->setClearButtonEnabled(true); 0174 m_searchLine->setPlaceholderText(i18n("Search…")); 0175 m_searchLine->setFocusPolicy(Qt::ClickFocus); 0176 m_searchLine->installEventFilter(this); 0177 connect(m_searchLine, &QLineEdit::textChanged, this, [this](const QString &str) { setFilterName(str); }); 0178 m_lay->addWidget(m_searchLine); 0179 0180 setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); 0181 setAcceptDrops(true); 0182 m_effectsTree = new QTreeView(this); 0183 m_effectsTree->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); 0184 m_effectsTree->setHeaderHidden(true); 0185 m_effectsTree->setAlternatingRowColors(true); 0186 m_effectsTree->setRootIsDecorated(true); 0187 m_effectsTree->setDragEnabled(true); 0188 m_effectsTree->setDragDropMode(QAbstractItemView::DragDrop); 0189 m_effectsTree->setItemDelegate(new AssetDelegate); 0190 connect(m_effectsTree, &QTreeView::doubleClicked, this, &AssetListWidget::activate); 0191 m_effectsTree->installEventFilter(this); 0192 m_effectsTree->setContextMenuPolicy(Qt::CustomContextMenu); 0193 connect(m_effectsTree, &QTreeView::customContextMenuRequested, this, &AssetListWidget::onCustomContextMenu); 0194 auto *viewSplitter = new QSplitter(Qt::Vertical, this); 0195 viewSplitter->addWidget(m_effectsTree); 0196 QTextBrowser *textEdit = new QTextBrowser(this); 0197 textEdit->setReadOnly(true); 0198 textEdit->setAcceptRichText(true); 0199 textEdit->setOpenExternalLinks(true); 0200 m_infoDocument = new QTextDocument(this); 0201 textEdit->setDocument(m_infoDocument); 0202 viewSplitter->addWidget(textEdit); 0203 m_lay->addWidget(viewSplitter); 0204 viewSplitter->setStretchFactor(0, 4); 0205 viewSplitter->setStretchFactor(1, 2); 0206 viewSplitter->setSizes({50, 0}); 0207 connect(showInfo, &QAction::triggered, this, [showInfo, viewSplitter]() { 0208 if (showInfo->isChecked()) { 0209 viewSplitter->setSizes({50, 20}); 0210 } else { 0211 viewSplitter->setSizes({50, 0}); 0212 } 0213 }); 0214 } 0215 0216 AssetListWidget::~AssetListWidget() {} 0217 0218 bool AssetListWidget::eventFilter(QObject *watched, QEvent *event) 0219 { 0220 // To avoid shortcut conflicts between the media browser and main app, we dis/enable actions when we gain/lose focus 0221 switch (event->type()) { 0222 case QEvent::ShortcutOverride: 0223 if (static_cast<QKeyEvent *>(event)->key() == Qt::Key_Escape) { 0224 setFilterName(QString()); 0225 m_effectsTree->setFocus(); 0226 } 0227 break; 0228 case QEvent::KeyPress: { 0229 const auto key = static_cast<QKeyEvent *>(event)->key(); 0230 if (key == Qt::Key_Return || key == Qt::Key_Enter) { 0231 activate(m_effectsTree->currentIndex()); 0232 setFilterName(QString()); 0233 event->accept(); 0234 return true; 0235 } else if (watched == m_searchLine && (key == Qt::Key_Down || key == Qt::Key_Up)) { 0236 QModelIndex current = m_effectsTree->currentIndex(); 0237 QModelIndex ix; 0238 if (key == Qt::Key_Down) { 0239 ix = current.siblingAtRow(current.row() + 1); 0240 if (!ix.isValid()) { 0241 ix = current.parent().siblingAtRow(current.parent().row() + 1); 0242 if (ix.isValid()) { 0243 ix = m_proxyModel->index(0, 0, ix); 0244 } 0245 } 0246 } else { 0247 if (current.row() > 0) { 0248 ix = current.siblingAtRow(current.row() - 1); 0249 } else { 0250 ix = current.parent().siblingAtRow(current.parent().row() - 1); 0251 if (ix.isValid()) { 0252 QModelIndex child = m_proxyModel->index(0, 0, ix); 0253 int row = 1; 0254 while (child.isValid()) { 0255 ix = child; 0256 child = child.siblingAtRow(row); 0257 row++; 0258 } 0259 } 0260 } 0261 } 0262 if (ix.isValid()) { 0263 m_effectsTree->setCurrentIndex(ix); 0264 } 0265 } 0266 break; 0267 } 0268 default: 0269 break; 0270 } 0271 return QObject::eventFilter(watched, event); 0272 } 0273 0274 QString AssetListWidget::getName(const QModelIndex &index) const 0275 { 0276 return m_model->getName(m_proxyModel->mapToSource(index)); 0277 } 0278 0279 bool AssetListWidget::isFavorite(const QModelIndex &index) const 0280 { 0281 return m_model->isFavorite(m_proxyModel->mapToSource(index)); 0282 } 0283 0284 void AssetListWidget::setFavorite(const QModelIndex &index, bool favorite) 0285 { 0286 m_model->setFavorite(m_proxyModel->mapToSource(index), favorite, isEffect()); 0287 Q_EMIT m_proxyModel->dataChanged(index, index, QVector<int>()); 0288 m_proxyModel->reloadFilterOnFavorite(); 0289 Q_EMIT reloadFavorites(); 0290 } 0291 0292 void AssetListWidget::deleteCustomEffect(const QModelIndex &index) 0293 { 0294 m_model->deleteEffect(m_proxyModel->mapToSource(index)); 0295 } 0296 0297 QString AssetListWidget::getDescription(const QModelIndex &index) const 0298 { 0299 return m_model->getDescription(isEffect(), m_proxyModel->mapToSource(index)); 0300 } 0301 0302 void AssetListWidget::setFilterName(const QString &pattern) 0303 { 0304 QItemSelectionModel *sel = m_effectsTree->selectionModel(); 0305 QModelIndex current = m_proxyModel->getModelIndex(sel->currentIndex()); 0306 m_proxyModel->setFilterName(!pattern.isEmpty(), pattern); 0307 if (!pattern.isEmpty()) { 0308 QVariantList mapped = m_proxyModel->getCategories(); 0309 for (auto &ix : mapped) { 0310 m_effectsTree->setExpanded(ix.toModelIndex(), true); 0311 } 0312 QModelIndex ix = m_proxyModel->firstVisibleItem(m_proxyModel->mapFromSource(current)); 0313 sel->setCurrentIndex(ix, QItemSelectionModel::ClearAndSelect); 0314 m_effectsTree->scrollTo(ix); 0315 } else { 0316 m_effectsTree->scrollTo(m_proxyModel->mapFromSource(current)); 0317 } 0318 } 0319 0320 QVariantMap AssetListWidget::getMimeData(const QString &assetId) const 0321 { 0322 QVariantMap mimeData; 0323 mimeData.insert(getMimeType(assetId), assetId); 0324 if (isAudio(assetId)) { 0325 mimeData.insert(QStringLiteral("type"), QStringLiteral("audio")); 0326 } 0327 return mimeData; 0328 } 0329 0330 void AssetListWidget::activate(const QModelIndex &ix) 0331 { 0332 if (!ix.isValid()) { 0333 return; 0334 } 0335 const QString assetId = m_model->data(m_proxyModel->mapToSource(ix), AssetTreeModel::IdRole).toString(); 0336 if (assetId != QLatin1String("root")) { 0337 Q_EMIT activateAsset(getMimeData(assetId)); 0338 } else { 0339 m_effectsTree->setExpanded(ix, !m_effectsTree->isExpanded(ix)); 0340 } 0341 } 0342 0343 bool AssetListWidget::showDescription() const 0344 { 0345 return KdenliveSettings::showeffectinfo(); 0346 } 0347 0348 void AssetListWidget::setShowDescription(bool show) 0349 { 0350 KdenliveSettings::setShoweffectinfo(show); 0351 Q_EMIT showDescriptionChanged(); 0352 } 0353 0354 void AssetListWidget::onCustomContextMenu(const QPoint &pos) 0355 { 0356 QModelIndex index = m_effectsTree->indexAt(pos); 0357 if (index.isValid()) { 0358 const QModelIndex sourceIndex = m_proxyModel->mapToSource(index); 0359 const QString assetId = m_model->data(sourceIndex, AssetTreeModel::IdRole).toString(); 0360 if (assetId != QStringLiteral("root")) { 0361 QList<QAction *> actions = m_contextMenu->actions(); 0362 bool isFavorite = m_model->data(sourceIndex, AssetTreeModel::FavoriteRole).toBool(); 0363 auto itemType = m_model->data(sourceIndex, AssetTreeModel::TypeRole).value<AssetListType::AssetType>(); 0364 bool isCustom = itemType == AssetListType::AssetType::Custom || itemType == AssetListType::AssetType::CustomAudio; 0365 for (auto &ac : actions) { 0366 if (ac->data().toString() == QLatin1String("custom")) { 0367 ac->setEnabled(isCustom); 0368 } else if (ac->data().toString() == QLatin1String("favorite")) { 0369 ac->setText(isFavorite ? i18n("Remove from favorites") : i18n("Add to favorites")); 0370 } 0371 } 0372 m_contextMenu->exec(m_effectsTree->viewport()->mapToGlobal(pos)); 0373 } 0374 } 0375 } 0376 0377 void AssetListWidget::updateAssetInfo(const QModelIndex ¤t, const QModelIndex &) 0378 { 0379 if (current.isValid()) { 0380 QString description = getDescription(current); 0381 const QString id = m_model->data(m_proxyModel->mapToSource(current), AssetTreeModel::IdRole).toString(); 0382 if (id.isEmpty() || id == QStringLiteral("root")) { 0383 m_infoDocument->clear(); 0384 return; 0385 } 0386 auto type = m_model->data(m_proxyModel->mapToSource(current), AssetTreeModel::TypeRole).value<AssetListType::AssetType>(); 0387 // Add link to our documentation 0388 const QString link = buildLink(id, type); 0389 if (!description.isEmpty()) { 0390 description.append(QString("<br/><a title=\"%1\" href=\"%2\">🔗 %3</a>").arg(i18nc("@info:tooltip", "Online documentation"), link, id)); 0391 } else { 0392 description = QString("<a title=\"%1\" href=\"%2\">🔗 %3</a>").arg(i18nc("@info:tooltip", "Online documentation"), link, id); 0393 } 0394 m_infoDocument->setHtml(description); 0395 } else { 0396 m_infoDocument->clear(); 0397 } 0398 } 0399 0400 const QString AssetListWidget::buildLink(const QString id, AssetListType::AssetType type) const 0401 { 0402 QString prefix; 0403 if (type == AssetListType::AssetType::Audio) { 0404 prefix = QStringLiteral("effect_link"); 0405 } else if (type == AssetListType::AssetType::Video) { 0406 prefix = QStringLiteral("effect_link"); 0407 } else if (type == AssetListType::AssetType::AudioComposition || type == AssetListType::AssetType::AudioTransition) { 0408 prefix = QStringLiteral("transition_link"); 0409 } else if (type == AssetListType::AssetType::VideoShortComposition || type == AssetListType::AssetType::VideoComposition || 0410 type == AssetListType::AssetType::VideoTransition) { 0411 prefix = QStringLiteral("transition_link"); 0412 } else { 0413 prefix = QStringLiteral("other"); 0414 } 0415 return QStringLiteral("https://docs.kdenlive.org/%1/%2").arg(prefix).arg(id); 0416 }