File indexing completed on 2024-05-12 08:54:21
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 }