Commit 52ac5f04 authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Add filter line for guides, allow sorting them by category, timecode or...

Add filter line for guides, allow sorting them by category, timecode or comment, allow exporting JSON data
parent 4db4d42f
Pipeline #259764 failed with stage
in 6 minutes and 53 seconds
......@@ -632,6 +632,10 @@ bool MarkerListModel::importFromJson(const QString &data, bool ignoreConflicts,
Fun undo = []() { return true; };
Fun redo = []() { return true; };
bool result = importFromJson(data, ignoreConflicts, undo, redo);
/*if (!result) {
// Import failed, try to import as txt data
result = importFromTxt(data, ignoreConflicts, undo, redo);
}*/
if (pushUndo) {
PUSH_UNDO(undo, redo, m_guide ? i18n("Import guides") : i18n("Import markers"));
}
......@@ -678,20 +682,87 @@ bool MarkerListModel::importFromJson(const QString &data, bool ignoreConflicts,
return false;
}
}
return true;
}
QString MarkerListModel::toJson() const
bool MarkerListModel::importFromTxt(const QString &fileName, Fun &undo, Fun &redo)
{
QWriteLocker locker(&m_lock);
QFile inputFile(fileName);
bool res = true;
bool lineRead = false;
// TODO: ask for a category
int type = 0;
if (inputFile.open(QIODevice::ReadOnly)) {
QTextStream in(&inputFile);
while (!in.atEnd()) {
QString line = in.readLine().simplified();
if (line.isEmpty()) {
continue;
}
QString pos = line.section(QLatin1Char(' '), 0, 0);
GenTime position;
// Try to read timecode
bool ok = false;
int separatorsCount = pos.count(QLatin1Char(':'));
switch (separatorsCount) {
case 0:
// assume we are dealing with seconds
position = GenTime(pos.toDouble(&ok));
break;
case 1: {
// assume min:sec
QString sec = pos.section(QLatin1Char(':'), 1);
QString min = pos.section(QLatin1Char(':'), 0, 0);
double seconds = sec.toDouble(&ok);
int minutes = ok ? min.toInt(&ok) : 0;
position = GenTime(seconds + 60 * minutes);
break;
}
case 2:
default: {
// assume hh:min:sec
QString sec = pos.section(QLatin1Char(':'), 2);
QString min = pos.section(QLatin1Char(':'), 1, 1);
QString hours = pos.section(QLatin1Char(':'), 0, 0);
double seconds = sec.toDouble(&ok);
int minutes = ok ? min.toInt(&ok) : 0;
int h = ok ? hours.toInt(&ok) : 0;
position = GenTime(seconds + (60 * minutes) + (3600 * h));
break;
}
}
if (!ok) {
// Could not read timecode
qDebug() << "::: Could not read timecode from line: " << line;
continue;
}
QString comment = line.section(QLatin1Char(' '), 1);
bool res = addMarker(position, comment, type, undo, redo);
if (!res) {
break;
} else if (!lineRead) {
lineRead = true;
}
}
inputFile.close();
}
return res && lineRead;
}
QString MarkerListModel::toJson(QList<int> categories) const
{
READ_LOCK();
QJsonArray list;
bool exportAllCategories = categories.isEmpty() || categories == (QList<int>() << -1);
for (const auto &marker : m_markerList) {
QJsonObject currentMarker;
currentMarker.insert(QLatin1String("pos"), QJsonValue(marker.second.time().frames(pCore->getCurrentFps())));
currentMarker.insert(QLatin1String("comment"), QJsonValue(marker.second.comment()));
currentMarker.insert(QLatin1String("type"), QJsonValue(marker.second.markerType()));
list.push_back(currentMarker);
if (exportAllCategories || categories.contains(marker.second.markerType())) {
list.push_back(currentMarker);
}
}
QJsonDocument json(list);
return QString::fromUtf8(json.toJson());
......
......@@ -123,8 +123,10 @@ public:
*/
void registerSnapModel(const std::weak_ptr<SnapInterface> &snapModel);
/** @brief Exports the model to json using format above */
QString toJson() const;
/** @brief Exports the model to json using format above
* @param categories will only export selected categories. If param is empty, all categories will be exported
*/
QString toJson(QList<int> categories = {}) const;
/** @brief Shows a dialog to edit a marker/guide
@param pos: position of the marker to edit, or new position for a marker
......@@ -167,6 +169,7 @@ public slots:
*/
bool importFromJson(const QString &data, bool ignoreConflicts, bool pushUndo = true);
bool importFromJson(const QString &data, bool ignoreConflicts, Fun &undo, Fun &redo);
bool importFromTxt(const QString &fileName, Fun &undo, Fun &redo);
protected:
/** @brief Adds a snap point at marker position in the registered snap models
......
......@@ -10,6 +10,8 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
MarkerSortModel::MarkerSortModel(QObject *parent)
: QSortFilterProxyModel(parent)
, m_sortColumn(1)
, m_sortOrder(0)
{
setDynamicSortFilter(true);
}
......@@ -26,17 +28,33 @@ bool MarkerSortModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceP
bool MarkerSortModel::filterAcceptsRowItself(int sourceRow, const QModelIndex &sourceParent) const
{
if (m_filterList.isEmpty()) {
return true;
return filterString(sourceRow, sourceParent);
}
QModelIndex row = sourceModel()->index(sourceRow, 0, sourceParent);
if (m_filterList.contains(sourceModel()->data(row, MarkerListModel::TypeRole).toInt())) {
return true;
return filterString(sourceRow, sourceParent);
} else {
m_ignoredPositions << sourceModel()->data(row, MarkerListModel::FrameRole).toInt();
}
return false;
}
bool MarkerSortModel::filterString(int sourceRow, const QModelIndex &sourceParent) const
{
if (m_searchString.isEmpty()) {
return true;
}
QModelIndex index0 = sourceModel()->index(sourceRow, 0, sourceParent);
if (!index0.isValid()) {
return false;
}
auto data = sourceModel()->data(index0);
if (data.toString().contains(m_searchString, Qt::CaseInsensitive)) {
return true;
}
return false;
}
void MarkerSortModel::slotSetFilters(const QList<int> filters)
{
m_filterList = filters;
......@@ -51,8 +69,77 @@ void MarkerSortModel::slotClearSearchFilters()
invalidateFilter();
}
void MarkerSortModel::slotSetFilterString(const QString &filter)
{
m_searchString = filter;
invalidateFilter();
}
std::vector<int> MarkerSortModel::getIgnoredSnapPoints() const
{
std::vector<int> markers(m_ignoredPositions.cbegin(), m_ignoredPositions.cend());
return markers;
}
void MarkerSortModel::slotSetSortColumn(int column)
{
m_sortColumn = column;
invalidate();
}
void MarkerSortModel::slotSetSortOrder(bool descending)
{
m_sortOrder = descending ? Qt::DescendingOrder : Qt::AscendingOrder;
invalidate();
}
bool MarkerSortModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
{
switch (m_sortColumn) {
case 0: {
// Sort by category
int leftCategory = sourceModel()->data(left, MarkerListModel::TypeRole).toInt();
int rightCategory = sourceModel()->data(right, MarkerListModel::TypeRole).toInt();
if (leftCategory == rightCategory) {
// sort by time
double leftTime = sourceModel()->data(left, MarkerListModel::PosRole).toDouble();
double rightTime = sourceModel()->data(right, MarkerListModel::PosRole).toDouble();
if (m_sortOrder == Qt::AscendingOrder) {
return leftTime < rightTime;
}
return leftTime > rightTime;
}
if (m_sortOrder == Qt::AscendingOrder) {
return leftCategory < rightCategory;
}
return leftCategory > rightCategory;
}
case 2: {
// Sort by comment
const QString leftComment = sourceModel()->data(left, MarkerListModel::CommentRole).toString();
const QString rightComment = sourceModel()->data(right, MarkerListModel::CommentRole).toString();
if (leftComment == rightComment) {
// sort by time
double leftTime = sourceModel()->data(left, MarkerListModel::PosRole).toDouble();
double rightTime = sourceModel()->data(right, MarkerListModel::PosRole).toDouble();
if (m_sortOrder == Qt::AscendingOrder) {
return leftTime < rightTime;
}
return leftTime > rightTime;
}
if (m_sortOrder == Qt::AscendingOrder) {
return QString::localeAwareCompare(leftComment, rightComment) < 0;
}
return QString::localeAwareCompare(leftComment, rightComment) > 0;
}
case 1:
default: {
double leftTime = sourceModel()->data(left, MarkerListModel::PosRole).toDouble();
double rightTime = sourceModel()->data(right, MarkerListModel::PosRole).toDouble();
if (m_sortOrder == Qt::AscendingOrder) {
return leftTime < rightTime;
}
return leftTime > rightTime;
}
}
}
......@@ -26,6 +26,9 @@ public slots:
void slotSetFilters(const QList<int> filter);
/** @brief Reset search filters */
void slotClearSearchFilters();
void slotSetFilterString(const QString &filter);
void slotSetSortColumn(int column);
void slotSetSortOrder(bool descending);
std::vector<int> getIgnoredSnapPoints() const;
protected:
......@@ -33,8 +36,14 @@ protected:
// cppcheck-suppress unusedFunction
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
bool filterAcceptsRowItself(int source_row, const QModelIndex &source_parent) const;
bool filterString(int sourceRow, const QModelIndex &sourceParent) const;
/** @brief Reimplemented to allow sorting by category, time or comment */
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
private:
QList<int> m_filterList;
mutable QList<int> m_ignoredPositions;
QString m_searchString;
int m_sortColumn;
int m_sortOrder;
};
......@@ -8,13 +8,16 @@
#include "bin/model/markerlistmodel.hpp"
#include "core.h"
#include "doc/kdenlivedoc.h"
#include "kdenlivesettings.h"
#include "project/projectmanager.h"
#include "kdenlive_debug.h"
#include <KMessageWidget>
#include <QAction>
#include <QClipboard>
#include <QDateTimeEdit>
#include <QFileDialog>
#include <QFontDatabase>
#include <QPushButton>
#include <QTime>
......@@ -48,6 +51,8 @@ ExportGuidesDialog::ExportGuidesDialog(const MarkerListModel *model, const GenTi
QPushButton *btn = buttonBox->addButton(i18n("Copy to Clipboard"), QDialogButtonBox::ActionRole);
btn->setIcon(QIcon::fromTheme("edit-copy"));
QPushButton *btn2 = buttonBox->addButton(i18n("Save"), QDialogButtonBox::ActionRole);
btn2->setIcon(QIcon::fromTheme("document-save"));
connect(categoryChooser, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this]() { updateContentByModel(); });
......@@ -65,6 +70,29 @@ ExportGuidesDialog::ExportGuidesDialog(const MarkerListModel *model, const GenTi
clipboard->setText(this->generatedContent->toPlainText());
});
connect(btn2, &QAbstractButton::clicked, this, [this]() {
QString filter = format_text->isChecked() ? QString("%1 (*.txt)").arg(i18n("Text File")) : QString("%1 (*.json)").arg(i18n("JSON File"));
const QString startFolder = pCore->projectManager()->current()->projectDataFolder();
QString filename = QFileDialog::getSaveFileName(this, i18nc("@title:window", "Export Guides Data"), startFolder, filter);
QFile file(filename);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
messageWidget->setText(i18n("Cannot write to file %1", QUrl::fromLocalFile(filename).fileName()));
messageWidget->setMessageType(KMessageWidget::Warning);
messageWidget->animatedShow();
return;
}
file.write(generatedContent->toPlainText().toUtf8());
file.close();
messageWidget->setText(i18n("Guides saved to %1", QUrl::fromLocalFile(filename).fileName()));
messageWidget->setMessageType(KMessageWidget::Positive);
messageWidget->animatedShow();
});
connect(format_json, &QRadioButton::toggled, this, [this](bool jsonFormat) {
textOptions->setEnabled(!jsonFormat);
updateContentByModel();
});
connect(buttonReset, &QAbstractButton::clicked, [this, defaultFormat]() {
formatEdit->setText(defaultFormat);
updateContentByModel();
......@@ -128,8 +156,12 @@ QString chapterTimeStringFromMs(double timeMs)
void ExportGuidesDialog::updateContentByModel() const
{
const QString format(formatEdit->text());
const int markerIndex = categoryChooser->currentCategory();
if (format_json->isChecked()) {
generatedContent->setPlainText(m_markerListModel->toJson({markerIndex}));
return;
}
const QString format(formatEdit->text());
const GenTime offset(offsetTime());
QStringList chapterTexts;
......
......@@ -140,7 +140,7 @@ KdenliveDoc::KdenliveDoc(const QUrl &url, QDomDocument& newDom, QString projectF
connect(m_commandStack.get(), &QUndoStack::indexChanged, this, &KdenliveDoc::slotModified);
connect(m_commandStack.get(), &DocUndoStack::invalidate, this, &KdenliveDoc::checkPreviewStack, Qt::DirectConnection);
initializeProperties();
initializeProperties(false);
m_guidesFilterModel.reset(new MarkerSortModel(this));
m_guidesFilterModel->setSourceModel(m_guideModel.get());
m_guidesFilterModel->setSortRole(MarkerListModel::PosRole);
......@@ -340,7 +340,8 @@ KdenliveDoc::~KdenliveDoc()
}
}
void KdenliveDoc::initializeProperties() {
void KdenliveDoc::initializeProperties(bool newDocument)
{
// init default document properties
m_documentProperties[QStringLiteral("zoom")] = QLatin1Char('8');
m_documentProperties[QStringLiteral("verticalzoom")] = QLatin1Char('1');
......@@ -363,13 +364,16 @@ void KdenliveDoc::initializeProperties() {
m_documentProperties[QStringLiteral("zonein")] = QLatin1Char('0');
m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("75");
m_documentProperties[QStringLiteral("seekOffset")] = QString::number(TimelineModel::seekDuration);
m_documentProperties[QStringLiteral("guidesCategories")] = KdenliveSettings::guidesCategories().join(QLatin1Char('\n'));
if (newDocument) {
// For existing documents, don't define guidesCategories, so that we can use the getDefaultGuideCategories() for backwards compatibility
m_documentProperties[QStringLiteral("guidesCategories")] = KdenliveSettings::guidesCategories().join(QLatin1Char('\n'));
}
}
const QStringList KdenliveDoc::guidesCategories() const
{
if (!m_documentProperties.contains(QStringLiteral("guidesCategories"))) {
return KdenliveSettings::guidesCategories();
return getDefaultGuideCategories();
}
return m_documentProperties.value(QStringLiteral("guidesCategories")).split(QLatin1Char('\n'));
}
......@@ -2074,3 +2078,16 @@ void KdenliveDoc::setGuidesFilter(const QList<int> filter)
{
m_guidesFilterModel->slotSetFilters(filter);
}
// static
const QStringList KdenliveDoc::getDefaultGuideCategories()
{
// Don't change this or it will break compatibility for projects created with Kdenlive < 22.12
QStringList colors = {QLatin1String("#9b59b6"), QLatin1String("#3daee9"), QLatin1String("#1abc9c"), QLatin1String("#1cdc9a"), QLatin1String("#c9ce3b"),
QLatin1String("#fdbc4b"), QLatin1String("#f39c1f"), QLatin1String("#f47750"), QLatin1String("#da4453")};
QStringList guidesCategories;
for (int i = 0; i < 9; i++) {
guidesCategories << i18n("Category %1:%2:%3", QString::number(i + 1), QString::number(i), colors.at(i));
}
return guidesCategories;
}
......@@ -243,14 +243,18 @@ public:
bool hasSubtitles() const;
/** @brief Generate a temporary subtitle file for a zone. */
void generateRenderSubtitleFile(int in, int out, const QString &subtitleFile);
/** @brief Returns the dafult definition for guide categories.*/
static const QStringList getDefaultGuideCategories();
private:
/** @brief Create a new KdenliveDoc using the provided QDomDocument (an
* existing project file), used by the Open() named constructor. */
KdenliveDoc(const QUrl &url, QDomDocument& newDom, QString projectFolder, QUndoGroup *undoGroup,
MainWindow *parent = nullptr);
/** @brief Set document default properties using hard-coded values and KdenliveSettings. */
void initializeProperties();
/** @brief Set document default properties using hard-coded values and KdenliveSettings.
* @param newDocument true if we are creating a new document, false when opening an existing one
*/
void initializeProperties(bool newDocument = true);
QDomDocument m_document;
int m_clipsCount;
/** @brief MLT's root (base path) that is stripped from urls in saved xml */
......
......@@ -1966,14 +1966,7 @@ bool MainWindow::readOptions()
}
initialGroup.writeEntry("version", version);
if (KdenliveSettings::guidesCategories().isEmpty()) {
QStringList colors = {QLatin1String("#9b59b6"), QLatin1String("#3daee9"), QLatin1String("#1abc9c"), QLatin1String("#1cdc9a"), QLatin1String("#c9ce3b"),
QLatin1String("#fdbc4b"), QLatin1String("#f39c1f"), QLatin1String("#f47750"), QLatin1String("#da4453")};
QStringList guidesCategories;
for (int i = 0; i < 9; i++) {
guidesCategories << i18n("Category %1:%2:%3", QString::number(i + 1), QString::number(i), colors.at(i));
}
qDebug() << "::: GOT GUIDES CAT:\n" << guidesCategories;
KdenliveSettings::setGuidesCategories(guidesCategories);
KdenliveSettings::setGuidesCategories(KdenliveDoc::getDefaultGuideCategories());
}
return firstRun;
}
......
......@@ -1505,6 +1505,11 @@ void ClipPropertiesController::slotLoadMarkers()
return;
}
QFile file(url);
if (file.size() > 1048576 &&
KMessageBox::warningContinueCancel(this, i18n("Marker file is larger than 1MB, are you sure you want to import ?")) != KMessageBox::Continue) {
// If marker file is larger than 1MB, ask for confirmation
return;
}
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
KMessageBox::error(this, i18n("Cannot open file %1", QUrl::fromLocalFile(url).fileName()));
return;
......
......@@ -52,11 +52,54 @@ GuidesList::GuidesList(QWidget *parent)
connect(guide_add, &QToolButton::clicked, this, &GuidesList::addGuide);
connect(guide_save, &QToolButton::clicked, this, &GuidesList::saveGuides);
connect(configure, &QToolButton::clicked, this, &GuidesList::configureGuides);
connect(filter_line, &QLineEdit::textChanged, this, &GuidesList::filterView);
QMenu *menu = new QMenu(this);
QAction *ac = new QAction(i18n("add multiple guides"), this);
connect(ac, &QAction::triggered, this, &GuidesList::addMutipleGuides);
menu->addAction(ac);
guide_add->setMenu(menu);
// Sort menu
m_filterGroup = new QActionGroup(this);
QMenu *sortMenu = new QMenu(this);
QAction *sort1 = new QAction(i18n("Sort by Category"), this);
sort1->setCheckable(true);
QAction *sort2 = new QAction(i18n("Sort by Time"), this);
sort2->setCheckable(true);
QAction *sort3 = new QAction(i18n("Sort by Comment"), this);
sort3->setCheckable(true);
QAction *sortDescending = new QAction(i18n("Descending"), this);
sortDescending->setCheckable(true);
sort1->setData(0);
sort2->setData(1);
sort3->setData(2);
m_filterGroup->addAction(sort1);
m_filterGroup->addAction(sort2);
m_filterGroup->addAction(sort3);
sortMenu->addAction(sort1);
sortMenu->addAction(sort2);
sortMenu->addAction(sort3);
sortMenu->addSeparator();
sortMenu->addAction(sortDescending);
sort2->setChecked(true);
sort_guides->setMenu(sortMenu);
connect(m_filterGroup, &QActionGroup::triggered, this, &GuidesList::sortView);
connect(sortDescending, &QAction::triggered, this, &GuidesList::changeSortOrder);
guide_add->setToolTip(i18n("Add new guide."));
guide_add->setWhatsThis(xi18nc("@info:whatsthis", "Add new guide. This will add a guide at the current frame position."));
guide_delete->setToolTip(i18n("Dereturn leftTime < rightTime;lete guide."));
guide_delete->setWhatsThis(xi18nc("@info:whatsi18nthis", "Delete guide. This will erase all selected guides."));
guide_save->setToolTip(i18n("Export guides."));
guide_save->setWhatsThis(
xi18nc("@info:whatsthis", "Export guide. This allows you to copy the guides data in a text format for use in web platforms for example."));
configure->setToolTip(i18n("Configure project guide categories."));
configure->setWhatsThis(xi18nc(
"@info:whatsthis", "Configure guide categories. This allows you to customize the guide categories used in this project (add, remove, rename, ...)."));
show_categories->setToolTip(i18n("Filter guide categories."));
show_categories->setWhatsThis(
xi18nc("@info:whatsthis", "Filter guide categories. This allows you to show or hide selected guide categories in this dialog and in the timeline."));
}
void GuidesList::configureGuides()
......@@ -139,6 +182,7 @@ void GuidesList::setModel(std::weak_ptr<MarkerListModel> model, MarkerSortModel
{
m_model = std::move(model);
if (auto markerModel = m_model.lock()) {
m_sortModel = viewModel;
m_proxy->setSourceModel(viewModel);
guides_list->setModel(m_proxy);
guides_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
......@@ -189,3 +233,24 @@ void GuidesList::updateFilter(QAbstractButton *, bool)
emit pCore->refreshActiveGuides();
qDebug() << "::: GOT FILTERS: " << filters;
}
void GuidesList::filterView(const QString &text)
{
if (m_sortModel) {
m_sortModel->slotSetFilterString(text);
}
}
void GuidesList::sortView(QAction *ac)
{
if (m_sortModel) {
m_sortModel->slotSetSortColumn(ac->data().toInt());
}
}
void GuidesList::changeSortOrder(bool descending)
{
if (m_sortModel) {
m_sortModel->slotSetSortOrder(descending);
}
}
......@@ -13,6 +13,7 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QSortFilterProxyModel>
class MarkerSortModel;
class QActionGroup;
/** @class GuidesList
@brief A widget listing project guides and allowing some advanced editing.
......@@ -37,13 +38,18 @@ private slots:
void configureGuides();
void rebuildCategories();
void updateFilter(QAbstractButton *, bool);
void filterView(const QString &text);
void sortView(QAction *ac);
void changeSortOrder(bool descending);
private:
/** @brief Set the marker model that will be displayed. */
std::weak_ptr<MarkerListModel> m_model;
QIdentityProxyModel *m_proxy;
QIdentityProxyModel *m_proxy{nullptr};
MarkerSortModel *m_sortModel{nullptr};
QVBoxLayout m_categoriesLayout;
QButtonGroup *catGroup{nullptr};
QActionGroup *m_filterGroup;
signals:
};
......@@ -10,14 +10,14 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
<rect>
<x>0</x>
<y>0</y>
<width>529</width>
<height>386</height>
<width>361</width>
<height>450</height>
</rect>
</property>
<property name="windowTitle">
<string>Marker</string>
</property>
<layout class="QFormLayout" name="formLayout">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
......@@ -31,104 +31,153 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label">
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Format:</string>
<string>Save As:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">