Project notes: allow creating markers from timestamps and assign timestamps to current bin clip

CCBUG: 420843
parent e892a7a2
Pipeline #22403 passed with stage
in 9 minutes and 37 seconds
......@@ -4156,3 +4156,18 @@ void Bin::checkProjectAudioTracks(QString clipId, int minimumTracksCount)
m_infoMessage->animatedHide();
}
}
void Bin::addClipMarker(const QString binId, QList<int> positions)
{
std::shared_ptr<ProjectClip> clip = getBinClip(binId);
if (!clip) {
pCore->displayMessage(i18n("Cannot find clip to add marker"), ErrorMessage);
return;
}
QMap <GenTime, QString> markers;
for (int pos : positions) {
GenTime p(pos, pCore->getCurrentFps());
markers.insert(p, pCore->currentDoc()->timecode().getDisplayTimecode(p, false));
}
clip->getMarkerModel()->addMarkers(markers, KdenliveSettings::default_marker_type());
}
......@@ -199,6 +199,9 @@ public:
/** @brief Get a clip's name from it's id */
const QString getBinClipName(const QString &id) const;
/** @brief Add markers on clip @param binId at @param positions */
void addClipMarker(const QString binId, QList<int> positions);
/** @brief Returns a list of selected clip ids.
* @param allowSubClips: if true, will include subclip ids in the form: "master clip id/in/out"
*/
......
......@@ -95,6 +95,32 @@ bool MarkerListModel::addMarker(GenTime pos, const QString &comment, int type, F
return false;
}
bool MarkerListModel::addMarkers(QMap <GenTime, QString> markers, int type)
{
QWriteLocker locker(&m_lock);
Fun undo = []() { return true; };
Fun redo = []() { return true; };
QMapIterator<GenTime, QString> i(markers);
bool rename = false;
bool res = true;
while (i.hasNext() && res) {
i.next();
if (m_markerList.count(i.key()) > 0) {
rename = true;
}
res = addMarker(i.key(), i.value(), type, undo, redo);
}
if (res) {
if (rename) {
PUSH_UNDO(undo, redo, m_guide ? i18n("Rename guide") : i18n("Rename marker"));
} else {
PUSH_UNDO(undo, redo, m_guide ? i18n("Add guide") : i18n("Add marker"));
}
}
return res;
}
bool MarkerListModel::addMarker(GenTime pos, const QString &comment, int type)
{
QWriteLocker locker(&m_lock);
......
......@@ -64,6 +64,7 @@ public:
@param type is the type (color) associated with the marker. If -1 is passed, then the value is pulled from kdenlive's defaults
*/
bool addMarker(GenTime pos, const QString &comment, int type = -1);
bool addMarkers(QMap <GenTime, QString> markers, int type = -1);
protected:
/* @brief Same function but accumulates undo/redo */
......
......@@ -928,3 +928,13 @@ int Core::audioChannels()
}
return 2;
}
void Core::addGuides(QList <int> guides)
{
QMap <GenTime, QString> markers;
for (int pos : guides) {
GenTime p(pos, pCore->getCurrentFps());
markers.insert(p, pCore->currentDoc()->timecode().getDisplayTimecode(p, false));
}
pCore->currentDoc()->getGuideModel()->addMarkers(markers);
}
......@@ -224,6 +224,8 @@ public:
bool enableMultiTrack(bool enable);
/** @brief Returns number of audio channels for this project. */
int audioChannels();
/** @brief Add guides in the project. */
void addGuides(QList <int> guides);
private:
explicit Core();
......
......@@ -1637,7 +1637,7 @@ void MainWindow::setupActions()
connect(switchTrackTarget, &QAction::triggered, this, &MainWindow::slotSwitchTrackAudioStream);
timelineActions->addAction(QStringLiteral("switch_target_stream"), switchTrackTarget);
actionCollection()->setDefaultShortcut(switchTrackTarget, Qt::Key_Apostrophe);
QAction *deleteTrack = new QAction(QIcon(), i18n("Delete Track"), this);
connect(deleteTrack, &QAction::triggered, this, &MainWindow::slotDeleteTrack);
timelineActions->addAction(QStringLiteral("delete_track"), deleteTrack);
......@@ -1737,7 +1737,7 @@ void MainWindow::setupActions()
addAction(QStringLiteral("activate_all_targets"), i18n("Switch All Tracks Active"), pCore->projectManager(), SLOT(slotMakeAllTrackActive()), QIcon(),
Qt::SHIFT + Qt::ALT + Qt::Key_A);
addAction(QStringLiteral("add_project_note"), i18n("Add Project Note"), pCore->projectManager(), SLOT(slotAddProjectNote()),
QIcon::fromTheme(QStringLiteral("bookmark")));
QIcon::fromTheme(QStringLiteral("bookmark-new")));
pCore->bin()->setupMenu();
......@@ -2559,15 +2559,7 @@ void MainWindow::slotAddMarkerGuideQuickly()
}
if (m_clipMonitor->isActive()) {
std::shared_ptr<ProjectClip> clip(m_clipMonitor->currentController());
GenTime pos(m_clipMonitor->position(), pCore->getCurrentFps());
if (!clip) {
m_messageLabel->setMessage(i18n("Cannot find clip to add marker"), ErrorMessage);
return;
}
CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type());
clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType());
pCore->bin()->addClipMarker(m_clipMonitor->activeClipId(), {m_clipMonitor->position()});
} else {
int selectedClip = getMainTimeline()->controller()->getMainSelectedItem();
if (selectedClip == -1) {
......
......@@ -20,6 +20,7 @@
#include "noteswidget.h"
#include "kdenlive_debug.h"
#include "core.h"
#include "bin/bin.h"
#include <QMenu>
#include <QMouseEvent>
......@@ -29,21 +30,79 @@
NotesWidget::NotesWidget(QWidget *parent)
: QTextEdit(parent)
{
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &NotesWidget::customContextMenuRequested, this, &NotesWidget::slotFillNotesMenu);
setMouseTracking(true);
}
NotesWidget::~NotesWidget() = default;
void NotesWidget::slotFillNotesMenu(const QPoint &pos)
void NotesWidget::contextMenuEvent(QContextMenuEvent *event)
{
QMenu *menu = createStandardContextMenu();
if (menu) {
QAction *a = new QAction(i18n("Insert current timecode"), menu);
connect(a, &QAction::triggered, this, &NotesWidget::insertNotesTimecode);
menu->insertAction(menu->actions().at(0), a);
menu->exec(viewport()->mapToGlobal(pos));
QPair <QStringList, QList <QPoint> > result = getSelectedAnchors();
QStringList anchors = result.first;
QList <QPoint> anchorPoints = result.second;
if (anchors.isEmpty()) {
const QString anchor = anchorAt(event->pos());
if (!anchor.isEmpty()) {
anchors << anchor;
}
}
if (!anchors.isEmpty()) {
a = new QAction(i18np("Create marker", "create markers", anchors.count()), menu);
connect(a, &QAction::triggered, [this, anchors] () {
createMarker(anchors);
});
menu->insertAction(menu->actions().at(1), a);
if (!anchorPoints.isEmpty()) {
a = new QAction(i18n("Assign timestamps to current Bin Clip"), menu);
connect(a, &QAction::triggered, [this, anchors, anchorPoints] () {
emit reAssign(anchors, anchorPoints);
});
menu->insertAction(menu->actions().at(2), a);
}
}
menu->exec(event->globalPos());
delete menu;
}
}
void NotesWidget::createMarker(QStringList anchors)
{
QMap <QString, QList<int>> clipMarkers;
QList<int> guides;
for (const QString &anchor : anchors) {
if (anchor.contains(QLatin1Char('#'))) {
// That's a Bin Clip reference.
const QString binId = anchor.section(QLatin1Char('#'), 0, 0);
QList <int> timecodes;
if (clipMarkers.contains(binId)) {
timecodes = clipMarkers.value(binId);
timecodes << anchor.section(QLatin1Char('#'), 1).toInt();
} else {
timecodes = {anchor.section(QLatin1Char('#'), 1).toInt()};
}
clipMarkers.insert(binId, timecodes);
} else {
// That is a guide
guides << anchor.toInt();
}
}
QMapIterator<QString, QList<int>> i(clipMarkers);
while (i.hasNext()) {
i.next();
// That's a Bin Clip reference.
pCore->bin()->addClipMarker(i.key(), i.value());
}
if (!clipMarkers.isEmpty()) {
const QString &binId = clipMarkers.firstKey();
pCore->selectBinClip(binId, clipMarkers.value(binId).constFirst(), QPoint());
}
if (!guides.isEmpty()) {
pCore->addGuides(guides);
}
}
......@@ -61,7 +120,7 @@ void NotesWidget::mouseMoveEvent(QMouseEvent *e)
void NotesWidget::mousePressEvent(QMouseEvent *e)
{
QString anchor = anchorAt(e->pos());
if (anchor.isEmpty()) {
if (anchor.isEmpty() || e->button() != Qt::LeftButton) {
QTextEdit::mousePressEvent(e);
return;
}
......@@ -74,6 +133,79 @@ void NotesWidget::mousePressEvent(QMouseEvent *e)
e->setAccepted(true);
}
QPair <QStringList, QList <QPoint> > NotesWidget::getSelectedAnchors()
{
int startPos = textCursor().selectionStart();
int endPos = textCursor().selectionEnd();
QStringList anchors;
QList <QPoint> anchorPoints;
if (endPos > startPos) {
textCursor().clearSelection();
QTextCursor cur(textCursor());
// Ensure we are at the start of current selection
if (!cur.atBlockStart()) {
cur.setPosition(startPos, QTextCursor::MoveAnchor);
int pos = startPos;
const QString an = anchorAt(cursorRect(cur).center());
while (!cur.atBlockStart()) {
pos--;
cur.setPosition(pos, QTextCursor::MoveAnchor);
if (anchorAt(cursorRect(cur).center()) == an) {
startPos = pos;
} else {
break;
}
}
}
bool isInAnchor = false;
QPoint anchorPoint;
for (int p = startPos; p <= endPos; ++p) {
cur.setPosition(p, QTextCursor::MoveAnchor);
const QString anchor = anchorAt(cursorRect(cur).center());
if (isInAnchor && !anchor.isEmpty() && p == endPos) {
endPos++;
}
if (isInAnchor && (anchor.isEmpty() || !anchors.contains(anchor) || cur.atEnd())) {
// End of current anchor
anchorPoint.setY(p);
anchorPoints.prepend(anchorPoint);
isInAnchor = false;
}
if (!anchor.isEmpty() && !anchors.contains(anchor)) {
anchors.prepend(anchor);
if (!isInAnchor) {
isInAnchor = true;
anchorPoint.setX(p);
}
}
}
}
return {anchors, anchorPoints};
}
void NotesWidget::assignProjectNote()
{
QPair <QStringList, QList <QPoint> > result = getSelectedAnchors();
QStringList anchors = result.first;
QList <QPoint> anchorPoints = result.second;
if (!anchors.isEmpty()) {
emit reAssign(anchors, anchorPoints);
} else {
pCore->displayMessage(i18n("Select some timecodes to reassign"), InformationMessage);
}
}
void NotesWidget::createMarkers()
{
QPair <QStringList, QList <QPoint> > result = getSelectedAnchors();
QStringList anchors = result.first;
if (!anchors.isEmpty()) {
createMarker(anchors);
} else {
pCore->displayMessage(i18n("Select some timecodes to create markers"), InformationMessage);
}
}
void NotesWidget::addProjectNote()
{
if (!textCursor().atBlockStart()) {
......
......@@ -41,13 +41,21 @@ protected:
void mouseMoveEvent(QMouseEvent *e) override;
void mousePressEvent(QMouseEvent *e) override;
void insertFromMimeData(const QMimeData *source) override;
void contextMenuEvent(QContextMenuEvent *event) override;
private slots:
void slotFillNotesMenu(const QPoint &pos);
public slots:
void createMarkers();
void assignProjectNote();
private:
QAction *m_markerAction;
void createMarker(QStringList anchors);
QPair <QStringList, QList <QPoint> > getSelectedAnchors();
signals:
void insertNotesTimecode();
void seekProject(int);
void reAssign(QStringList anchors, QList <QPoint> points);
};
#endif
......@@ -20,11 +20,19 @@ the Free Software Foundation, either version 3 of the License, or
NotesPlugin::NotesPlugin(ProjectManager *projectManager)
: QObject(projectManager)
{
QWidget *container = new QWidget();
auto *lay = new QVBoxLayout();
m_tb = new QToolBar();
m_tb->setToolButtonStyle(Qt::ToolButtonIconOnly);
lay->addWidget(m_tb);
m_widget = new NotesWidget();
lay->addWidget(m_widget);
container->setLayout(lay);
connect(m_widget, &NotesWidget::insertNotesTimecode, this, &NotesPlugin::slotInsertTimecode);
connect(m_widget, &NotesWidget::reAssign, this, &NotesPlugin::slotReAssign);
m_widget->setTabChangesFocus(true);
m_widget->setPlaceholderText(i18n("Enter your project notes here ..."));
m_notesDock = pCore->window()->addDock(i18n("Project Notes"), QStringLiteral("notes_widget"), m_widget);
m_notesDock = pCore->window()->addDock(i18n("Project Notes"), QStringLiteral("notes_widget"), container);
m_notesDock->close();
connect(projectManager, &ProjectManager::docOpened, this, &NotesPlugin::setProject);
}
......@@ -33,6 +41,16 @@ void NotesPlugin::setProject(KdenliveDoc *document)
{
connect(m_widget, &NotesWidget::seekProject, pCore->monitorManager()->projectMonitor(), &Monitor::requestSeek);
connect(m_widget, SIGNAL(textChanged()), document, SLOT(setModified()));
if (m_tb->actions().isEmpty()) {
// initialize toolbar
m_tb->addAction(pCore->window()->action("add_project_note"));
QAction *a = new QAction(QIcon::fromTheme(QStringLiteral("edit-find-replace")), i18n("Reassign selected timecodes to current Bin clip"));
connect(a, &QAction::triggered, m_widget, &NotesWidget::assignProjectNote);
m_tb->addAction(a);
a = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Create markers from selected timecodes"));
connect(a, &QAction::triggered, m_widget, &NotesWidget::createMarkers);
m_tb->addAction(a);
}
}
void NotesPlugin::showDock()
......@@ -46,17 +64,59 @@ void NotesPlugin::slotInsertTimecode()
// Add a note on the current bin clip
int frames = pCore->monitorManager()->clipMonitor()->position();
QString position = pCore->timecode().getTimecodeFromFrames(frames);
QString binId = pCore->monitorManager()->clipMonitor()->activeClipId();
const QString binId = pCore->monitorManager()->clipMonitor()->activeClipId();
if (binId.isEmpty()) {
pCore->displayMessage(i18n("Cannot add note, no clip selected in project bin"), InformationMessage);
return;
}
QString clipName = pCore->bin()->getBinClipName(binId);
m_widget->insertHtml(QString("<a href=\"%1#%2\">%3(%4)</a> ").arg(binId).arg(frames).arg(clipName).arg(position));
m_widget->insertHtml(QString("<a href=\"%1#%2\">%3:%4</a> ").arg(binId).arg(frames).arg(clipName).arg(position));
} else {
int frames = pCore->monitorManager()->projectMonitor()->position();
QString position = pCore->timecode().getTimecodeFromFrames(frames);
m_widget->insertHtml(QStringLiteral("<a href=\"") + QString::number(frames) + QStringLiteral("\">") + position + QStringLiteral("</a> "));
m_widget->insertHtml(QString("<a href=\"%1\">%2</a> ").arg(frames).arg(position));
}
}
void NotesPlugin::slotReAssign(QStringList anchors, QList <QPoint> points)
{
const QString binId = pCore->monitorManager()->clipMonitor()->activeClipId();
int ix = 0;
if (points.count() != anchors.count()) {
// Something is wrong, abort
pCore->displayMessage(i18n("Cannot perform assign"), InformationMessage);
return;
}
for (const QString & a : anchors) {
QPoint pt = points.at(ix);
QString updatedLink = a;
int position = 0;
if (a.contains(QLatin1Char('#'))) {
// Link was previously attached to another clip
updatedLink = a.section(QLatin1Char('#'), 1);
position = updatedLink.toInt();
if (!binId.isEmpty()) {
updatedLink.prepend(QString("%1#").arg(binId));
}
} else {
updatedLink = a;
position = a.toInt();
if (!binId.isEmpty()) {
updatedLink.prepend(QString("%1#").arg(binId));
}
}
QTextCursor cur(m_widget->textCursor());
cur.setPosition(pt.x());
cur.setPosition(pt.y(), QTextCursor::KeepAnchor);
QString pos = pCore->timecode().getTimecodeFromFrames(position);
if (!binId.isEmpty()) {
QString clipName = pCore->bin()->getBinClipName(binId);
cur.insertHtml(QString("<a href=\"%1\">%2:%3</a> ").arg(updatedLink).arg(clipName).arg(pos));
} else {
// Timestamp relative to project timeline
cur.insertHtml(QString("<a href=\"%1\">%2</a> ").arg(updatedLink).arg(pos));
}
ix++;
}
}
......
......@@ -17,6 +17,7 @@ class NotesWidget;
class KdenliveDoc;
class ProjectManager;
class QDockWidget;
class QToolBar;
/**
* @class NotesPlugin
......@@ -39,10 +40,13 @@ private slots:
void setProject(KdenliveDoc *document);
/** @brief Insert current timecode/cursor position into the widget. */
void slotInsertTimecode();
/** @brief Re-assign timestamps to current Bin Clip. */
void slotReAssign(QStringList anchors, QList <QPoint> points);
private:
NotesWidget *m_widget;
QDockWidget *m_notesDock;
QToolBar *m_tb;
};
#endif
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment