File indexing completed on 2024-02-25 05:01:22

0001 /*
0002     SPDX-FileCopyrightText: 2022 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "guideslist.h"
0008 #include "bin/bin.h"
0009 #include "bin/model/markersortmodel.h"
0010 #include "bin/projectclip.h"
0011 #include "core.h"
0012 #include "doc/kdenlivedoc.h"
0013 #include "kdenlive_debug.h"
0014 #include "kdenlivesettings.h"
0015 #include "mainwindow.h"
0016 #include "project/projectmanager.h"
0017 
0018 #include <KLocalizedString>
0019 #include <KMessageBox>
0020 
0021 #include <QButtonGroup>
0022 #include <QCheckBox>
0023 #include <QDialog>
0024 #include <QFileDialog>
0025 #include <QFontDatabase>
0026 #include <QKeyEvent>
0027 #include <QMenu>
0028 #include <QPainter>
0029 
0030 GuideFilterEventEater::GuideFilterEventEater(QObject *parent)
0031     : QObject(parent)
0032 {
0033 }
0034 
0035 bool GuideFilterEventEater::eventFilter(QObject *obj, QEvent *event)
0036 {
0037     switch (event->type()) {
0038     case QEvent::ShortcutOverride:
0039         if (static_cast<QKeyEvent *>(event)->key() == Qt::Key_Escape) {
0040             Q_EMIT clearSearchLine();
0041         }
0042         break;
0043     default:
0044         break;
0045     }
0046     return QObject::eventFilter(obj, event);
0047 }
0048 
0049 class GuidesProxyModel : public QIdentityProxyModel
0050 {
0051 public:
0052     explicit GuidesProxyModel(QObject *parent = nullptr)
0053         : QIdentityProxyModel(parent)
0054     {
0055     }
0056     QVariant data(const QModelIndex &index, int role) const override
0057     {
0058         if (role == Qt::DisplayRole) {
0059             return QString("%1 %2").arg(QIdentityProxyModel::data(index, MarkerListModel::TCRole).toString(),
0060                                         QIdentityProxyModel::data(index, role).toString());
0061         }
0062         return sourceModel()->data(mapToSource(index), role);
0063     }
0064 };
0065 
0066 GuidesList::GuidesList(QWidget *parent)
0067     : QWidget(parent)
0068     , m_markerMode(false)
0069 {
0070     setupUi(this);
0071     setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont));
0072     m_proxy = new GuidesProxyModel(this);
0073     connect(guides_list, &QListView::doubleClicked, this, &GuidesList::editGuide);
0074     connect(guide_delete, &QToolButton::clicked, this, &GuidesList::removeGuide);
0075     connect(guide_add, &QToolButton::clicked, this, &GuidesList::addGuide);
0076     connect(guide_edit, &QToolButton::clicked, this, &GuidesList::editGuides);
0077     connect(filter_line, &QLineEdit::textChanged, this, &GuidesList::filterView);
0078     connect(pCore.get(), &Core::updateDefaultMarkerCategory, this, &GuidesList::refreshDefaultCategory);
0079     QAction *a = KStandardAction::renameFile(this, &GuidesList::editGuides, this);
0080     guides_list->addAction(a);
0081     a->setShortcutContext(Qt::WidgetWithChildrenShortcut);
0082 
0083     //  Settings menu
0084     QMenu *settingsMenu = new QMenu(this);
0085     QAction *importGuides = new QAction(QIcon::fromTheme(QStringLiteral("document-import")), i18n("Import..."), this);
0086     connect(importGuides, &QAction::triggered, this, &GuidesList::importGuides);
0087     settingsMenu->addAction(importGuides);
0088     QAction *exportGuides = new QAction(QIcon::fromTheme(QStringLiteral("document-export")), i18n("Export..."), this);
0089     connect(exportGuides, &QAction::triggered, this, &GuidesList::saveGuides);
0090     settingsMenu->addAction(exportGuides);
0091     QAction *categories = new QAction(QIcon::fromTheme(QStringLiteral("configure")), i18n("Configure Categories"), this);
0092     connect(categories, &QAction::triggered, this, &GuidesList::configureGuides);
0093     settingsMenu->addAction(categories);
0094     guides_settings->setMenu(settingsMenu);
0095 
0096     // Sort menu
0097     m_sortGroup = new QActionGroup(this);
0098     QMenu *sortMenu = new QMenu(this);
0099     QAction *sort1 = new QAction(i18n("Sort by Category"), this);
0100     sort1->setCheckable(true);
0101     QAction *sort2 = new QAction(i18n("Sort by Time"), this);
0102     sort2->setCheckable(true);
0103     QAction *sort3 = new QAction(i18n("Sort by Comment"), this);
0104     sort3->setCheckable(true);
0105     QAction *sortDescending = new QAction(i18n("Descending"), this);
0106     sortDescending->setCheckable(true);
0107 
0108     sort1->setData(0);
0109     sort2->setData(1);
0110     sort3->setData(2);
0111     m_sortGroup->addAction(sort1);
0112     m_sortGroup->addAction(sort2);
0113     m_sortGroup->addAction(sort3);
0114     sortMenu->addAction(sort1);
0115     sortMenu->addAction(sort2);
0116     sortMenu->addAction(sort3);
0117     sortMenu->addSeparator();
0118     sortMenu->addAction(sortDescending);
0119     sort2->setChecked(true);
0120     sort_guides->setMenu(sortMenu);
0121     connect(m_sortGroup, &QActionGroup::triggered, this, &GuidesList::sortView);
0122     connect(sortDescending, &QAction::triggered, this, &GuidesList::changeSortOrder);
0123 
0124     // Filtering
0125     show_categories->enableFilterMode();
0126     show_categories->setAllowAll(true);
0127     show_categories->setOnlyUsed(true);
0128     connect(show_categories, &QToolButton::toggled, this, &GuidesList::switchFilter);
0129     connect(show_categories, &MarkerCategoryButton::categoriesChanged, this, &GuidesList::updateFilter);
0130 
0131     auto *leventEater = new GuideFilterEventEater(this);
0132     filter_line->installEventFilter(leventEater);
0133     connect(leventEater, &GuideFilterEventEater::clearSearchLine, filter_line, &QLineEdit::clear);
0134     connect(filter_line, &QLineEdit::returnPressed, filter_line, &QLineEdit::clear);
0135 
0136     guide_add->setToolTip(i18n("Add new guide."));
0137     guide_add->setWhatsThis(xi18nc("@info:whatsthis", "Add new guide. This will add a guide at the current frame position."));
0138     guide_delete->setToolTip(i18n("Delete guide."));
0139     guide_delete->setWhatsThis(xi18nc("@info:whatsthis", "Delete guide. This will erase all selected guides."));
0140     guide_edit->setToolTip(i18n("Edit selected guide."));
0141     guide_edit->setWhatsThis(xi18nc("@info:whatsthis", "Edit selected guide. Selecting multiple guides allows changing their category."));
0142     show_categories->setToolTip(i18n("Filter guide categories."));
0143     show_categories->setWhatsThis(
0144         xi18nc("@info:whatsthis", "Filter guide categories. This allows you to show or hide selected guide categories in this dialog and in the timeline."));
0145     default_category->setToolTip(i18n("Default guide category."));
0146     default_category->setWhatsThis(xi18nc("@info:whatsthis", "Default guide category. The category used for newly created guides."));
0147 }
0148 
0149 void GuidesList::configureGuides()
0150 {
0151     pCore->window()->slotEditProjectSettings(2);
0152 }
0153 
0154 void GuidesList::importGuides()
0155 {
0156     if (auto markerModel = m_model.lock()) {
0157         QScopedPointer<QFileDialog> fd(
0158             new QFileDialog(this, i18nc("@title:window", "Load Clip Markers"), pCore->projectManager()->current()->projectDataFolder()));
0159         fd->setMimeTypeFilters({QStringLiteral("application/json"), QStringLiteral("text/plain")});
0160         fd->setFileMode(QFileDialog::ExistingFile);
0161         if (fd->exec() != QDialog::Accepted) {
0162             return;
0163         }
0164         QStringList selection = fd->selectedFiles();
0165         QString url;
0166         if (!selection.isEmpty()) {
0167             url = selection.first();
0168         }
0169         if (url.isEmpty()) {
0170             return;
0171         }
0172         QFile file(url);
0173         if (file.size() > 1048576 &&
0174             KMessageBox::warningContinueCancel(this, i18n("Marker file is larger than 1MB, are you sure you want to import ?")) != KMessageBox::Continue) {
0175             // If marker file is larger than 1MB, ask for confirmation
0176             return;
0177         }
0178         if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
0179             KMessageBox::error(this, i18n("Cannot read file %1", QUrl::fromLocalFile(url).fileName()));
0180             return;
0181         }
0182         QString fileContent = QString::fromUtf8(file.readAll());
0183         file.close();
0184         markerModel->importFromFile(fileContent, true);
0185     }
0186 }
0187 
0188 void GuidesList::saveGuides()
0189 {
0190     if (auto markerModel = m_model.lock()) {
0191         markerModel->exportGuidesGui(this, GenTime());
0192     }
0193 }
0194 
0195 void GuidesList::editGuides()
0196 {
0197     QModelIndexList selectedIndexes = guides_list->selectionModel()->selectedIndexes();
0198     if (selectedIndexes.isEmpty()) {
0199         return;
0200     }
0201     if (selectedIndexes.size() == 1) {
0202         editGuide(selectedIndexes.first());
0203         return;
0204     }
0205     QList<GenTime> timeList;
0206     for (auto &ix : selectedIndexes) {
0207         int frame = m_proxy->data(ix, MarkerListModel::FrameRole).toInt();
0208         GenTime pos(frame, pCore->getCurrentFps());
0209         timeList << pos;
0210     }
0211     std::sort(timeList.begin(), timeList.end());
0212     if (auto markerModel = m_model.lock()) {
0213         markerModel->editMultipleMarkersGui(timeList, qApp->activeWindow());
0214     }
0215 }
0216 
0217 void GuidesList::editGuide(const QModelIndex &ix)
0218 {
0219     if (!ix.isValid()) return;
0220     int frame = m_proxy->data(ix, MarkerListModel::FrameRole).toInt();
0221     GenTime pos(frame, pCore->getCurrentFps());
0222     if (auto markerModel = m_model.lock()) {
0223         markerModel->editMarkerGui(pos, qApp->activeWindow(), false, m_clip.get());
0224     }
0225 }
0226 
0227 void GuidesList::selectAll()
0228 {
0229     guides_list->selectAll();
0230 }
0231 
0232 void GuidesList::removeGuide()
0233 {
0234     QModelIndexList selection = guides_list->selectionModel()->selectedIndexes();
0235     if (auto markerModel = m_model.lock()) {
0236         Fun undo = []() { return true; };
0237         Fun redo = []() { return true; };
0238         QList<int> frames;
0239         for (auto &ix : selection) {
0240             frames << m_proxy->data(ix, MarkerListModel::FrameRole).toInt();
0241         }
0242         for (auto &frame : frames) {
0243             GenTime pos(frame, pCore->getCurrentFps());
0244             markerModel->removeMarker(pos, undo, redo);
0245         }
0246         if (!selection.isEmpty()) {
0247             pCore->pushUndo(undo, redo, i18n("Remove guides"));
0248         }
0249     }
0250 }
0251 
0252 void GuidesList::addGuide()
0253 {
0254     int frame = pCore->getMonitorPosition(m_markerMode ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor);
0255     if (frame >= 0) {
0256         GenTime pos(frame, pCore->getCurrentFps());
0257         if (auto markerModel = m_model.lock()) {
0258             markerModel->addMultipleMarkersGui(pos, this, true, m_clip.get());
0259         }
0260     }
0261 }
0262 
0263 void GuidesList::selectionChanged(const QItemSelection &selected, const QItemSelection &)
0264 {
0265     if (selected.indexes().isEmpty()) {
0266         return;
0267     }
0268     const QModelIndex ix = selected.indexes().first();
0269     if (!ix.isValid()) {
0270         return;
0271     }
0272     int pos = m_proxy->data(ix, MarkerListModel::FrameRole).toInt();
0273     pCore->seekMonitor(m_markerMode ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor, pos);
0274 }
0275 
0276 GuidesList::~GuidesList() = default;
0277 
0278 void GuidesList::setClipMarkerModel(std::shared_ptr<ProjectClip> clip)
0279 {
0280     m_markerMode = true;
0281     guides_lock->setVisible(false);
0282     if (clip == m_clip) {
0283         return;
0284     }
0285     m_clip = clip;
0286     if (clip == nullptr) {
0287         m_sortModel = nullptr;
0288         m_proxy->setSourceModel(m_sortModel);
0289         guides_list->setModel(m_proxy);
0290         guideslist_label->clear();
0291         setEnabled(false);
0292         return;
0293     }
0294     setEnabled(true);
0295     guideslist_label->setText(i18n("Markers for %1", clip->clipName()));
0296     m_sortModel = clip->getFilteredMarkerModel().get();
0297     m_model = clip->getMarkerModel();
0298     m_proxy->setSourceModel(m_sortModel);
0299     guides_list->setModel(m_proxy);
0300     guides_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
0301     connect(guides_list->selectionModel(), &QItemSelectionModel::selectionChanged, this, &GuidesList::selectionChanged);
0302     rebuildCategories();
0303     if (auto markerModel = m_model.lock()) {
0304         show_categories->setMarkerModel(markerModel.get());
0305         show_categories->setCurrentCategories(m_lastSelectedMarkerCategories);
0306         switchFilter(!m_lastSelectedMarkerCategories.isEmpty() && !m_lastSelectedMarkerCategories.contains(-1));
0307         connect(markerModel.get(), &MarkerListModel::categoriesChanged, this, &GuidesList::rebuildCategories);
0308     }
0309 }
0310 
0311 void GuidesList::setModel(std::weak_ptr<MarkerListModel> model, std::shared_ptr<MarkerSortModel> viewModel)
0312 {
0313     m_clip = nullptr;
0314     m_markerMode = false;
0315     if (viewModel.get() == m_sortModel) {
0316         // already displayed
0317         return;
0318     }
0319     m_model = std::move(model);
0320     setEnabled(true);
0321     guideslist_label->setText(i18n("Timeline Guides"));
0322     if (!guides_lock->defaultAction()) {
0323         QAction *action = pCore->window()->actionCollection()->action("lock_guides");
0324         guides_lock->setDefaultAction(action);
0325     }
0326     guides_lock->setVisible(true);
0327     m_sortModel = viewModel.get();
0328     m_proxy->setSourceModel(m_sortModel);
0329     guides_list->setModel(m_proxy);
0330     guides_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
0331     connect(guides_list->selectionModel(), &QItemSelectionModel::selectionChanged, this, &GuidesList::selectionChanged);
0332     if (auto markerModel = m_model.lock()) {
0333         show_categories->setMarkerModel(markerModel.get());
0334         show_categories->setCurrentCategories(m_lastSelectedGuideCategories);
0335         switchFilter(!m_lastSelectedGuideCategories.isEmpty() && !m_lastSelectedGuideCategories.contains(-1));
0336         connect(markerModel.get(), &MarkerListModel::categoriesChanged, this, &GuidesList::rebuildCategories);
0337     }
0338     rebuildCategories();
0339 }
0340 
0341 void GuidesList::rebuildCategories()
0342 {
0343     if (pCore->markerTypes.isEmpty()) {
0344         // Categories are not ready, abort
0345         return;
0346     }
0347     QPixmap pixmap(32, 32);
0348     // Cleanup default marker category menu
0349     QMenu *markerDefaultMenu = default_category->menu();
0350     if (markerDefaultMenu) {
0351         markerDefaultMenu->clear();
0352     } else {
0353         markerDefaultMenu = new QMenu(this);
0354         connect(markerDefaultMenu, &QMenu::triggered, this, [this](QAction *ac) {
0355             int val = ac->data().toInt();
0356             KdenliveSettings::setDefault_marker_type(val);
0357             default_category->setIcon(ac->icon());
0358         });
0359         default_category->setMenu(markerDefaultMenu);
0360     }
0361 
0362     QMapIterator<int, Core::MarkerCategory> i(pCore->markerTypes);
0363     bool defaultCategoryFound = false;
0364     while (i.hasNext()) {
0365         i.next();
0366         pixmap.fill(i.value().color);
0367         QIcon colorIcon(pixmap);
0368         QAction *ac = new QAction(colorIcon, i.value().displayName);
0369         ac->setData(i.key());
0370         markerDefaultMenu->addAction(ac);
0371         if (i.key() == KdenliveSettings::default_marker_type()) {
0372             default_category->setIcon(colorIcon);
0373             defaultCategoryFound = true;
0374         }
0375     }
0376     if (!defaultCategoryFound) {
0377         // Default marker category not found. set it to first one
0378         QAction *ac = markerDefaultMenu->actions().first();
0379         if (ac) {
0380             default_category->setIcon(ac->icon());
0381             KdenliveSettings::setDefault_marker_type(ac->data().toInt());
0382         }
0383     }
0384 }
0385 
0386 void GuidesList::refreshDefaultCategory()
0387 {
0388     int ix = KdenliveSettings::default_marker_type();
0389     QMenu *menu = default_category->menu();
0390     if (menu) {
0391         QList<QAction *> actions = menu->actions();
0392         for (auto *ac : actions) {
0393             if (ac->data() == ix) {
0394                 default_category->setIcon(ac->icon());
0395                 break;
0396             }
0397         }
0398     }
0399 }
0400 
0401 void GuidesList::switchFilter(bool enable)
0402 {
0403     QList<int> cats = m_markerMode ? m_lastSelectedMarkerCategories : m_lastSelectedGuideCategories; // show_categories->currentCategories();
0404     if (enable && !cats.contains(-1)) {
0405         updateFilter(cats);
0406         show_categories->setChecked(true);
0407     } else {
0408         updateFilter({});
0409         show_categories->setChecked(false);
0410     }
0411 }
0412 
0413 void GuidesList::updateFilter(QList<int> categories)
0414 {
0415     if (m_markerMode) {
0416         m_clip->getFilteredMarkerModel()->slotSetFilters(categories);
0417         m_lastSelectedMarkerCategories = categories;
0418     } else if (m_sortModel) {
0419         m_sortModel->slotSetFilters(categories);
0420         m_lastSelectedGuideCategories = categories;
0421         Q_EMIT pCore->refreshActiveGuides();
0422     }
0423 }
0424 
0425 void GuidesList::filterView(const QString &text)
0426 {
0427     if (m_sortModel) {
0428         m_sortModel->slotSetFilterString(text);
0429         if (!text.isEmpty() && guides_list->model()->rowCount() > 0) {
0430             guides_list->setCurrentIndex(guides_list->model()->index(0, 0));
0431         }
0432         QModelIndex current = guides_list->currentIndex();
0433         if (current.isValid()) {
0434             guides_list->scrollTo(current);
0435         }
0436     }
0437 }
0438 
0439 void GuidesList::sortView(QAction *ac)
0440 {
0441     if (m_sortModel) {
0442         m_sortModel->slotSetSortColumn(ac->data().toInt());
0443     }
0444 }
0445 
0446 void GuidesList::changeSortOrder(bool descending)
0447 {
0448     if (m_sortModel) {
0449         m_sortModel->slotSetSortOrder(descending);
0450     }
0451 }
0452 
0453 void GuidesList::reset()
0454 {
0455     m_lastSelectedGuideCategories.clear();
0456     m_lastSelectedMarkerCategories.clear();
0457     show_categories->setCurrentCategories({-1});
0458     filter_line->clear();
0459 }