File indexing completed on 2024-04-28 04:52:14

0001 /*
0002     SPDX-FileCopyrightText: 2008 Jean-Baptiste Mardelle <jb@kdenlive.org>
0003 
0004 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "noteswidget.h"
0008 #include "bin/bin.h"
0009 #include "bin/projectclip.h"
0010 #include "bin/projectitemmodel.h"
0011 #include "core.h"
0012 #include "kdenlive_debug.h"
0013 
0014 #include <KLocalizedString>
0015 #include <QMenu>
0016 #include <QMimeData>
0017 #include <QMouseEvent>
0018 #include <QToolTip>
0019 #include <QUuid>
0020 
0021 NotesWidget::NotesWidget(QWidget *parent)
0022     : QTextEdit(parent)
0023 {
0024     setMouseTracking(true);
0025 }
0026 
0027 NotesWidget::~NotesWidget() = default;
0028 
0029 void NotesWidget::contextMenuEvent(QContextMenuEvent *event)
0030 {
0031     QMenu *menu = createStandardContextMenu();
0032     if (menu) {
0033         QAction *a = new QAction(i18n("Insert current timecode"), menu);
0034         connect(a, &QAction::triggered, this, &NotesWidget::insertNotesTimecode);
0035         menu->insertAction(menu->actions().at(0), a);
0036         QPair<QStringList, QList<QPoint>> result = getSelectedAnchors();
0037         QStringList anchors = result.first;
0038         QList<QPoint> anchorPoints = result.second;
0039         if (anchors.isEmpty()) {
0040             const QString anchor = anchorAt(event->pos());
0041             if (!anchor.isEmpty()) {
0042                 anchors << anchor;
0043             }
0044         }
0045         if (!anchors.isEmpty()) {
0046             a = new QAction(i18np("Create marker", "create markers", anchors.count()), menu);
0047             connect(a, &QAction::triggered, this, [this, anchors]() { createMarker(anchors); });
0048             menu->insertAction(menu->actions().at(1), a);
0049             if (!anchorPoints.isEmpty()) {
0050                 a = new QAction(i18n("Assign timestamps to current Bin Clip"), menu);
0051                 connect(a, &QAction::triggered, this, [this, anchors, anchorPoints]() { Q_EMIT reAssign(anchors, anchorPoints); });
0052                 menu->insertAction(menu->actions().at(2), a);
0053             }
0054         }
0055         menu->exec(event->globalPos());
0056         delete menu;
0057     }
0058 }
0059 
0060 void NotesWidget::createMarker(const QStringList &anchors)
0061 {
0062     QMap<QString, QList<int>> clipMarkers;
0063     QList<int> guides;
0064     for (const QString &anchor : anchors) {
0065         if (anchor.contains(QLatin1Char('#'))) {
0066             // That's a Bin Clip reference.
0067             const QString binId = anchor.section(QLatin1Char('#'), 0, 0);
0068             QList<int> timecodes;
0069             if (clipMarkers.contains(binId)) {
0070                 timecodes = clipMarkers.value(binId);
0071                 timecodes << anchor.section(QLatin1Char('#'), 1).toInt();
0072             } else {
0073                 timecodes = {anchor.section(QLatin1Char('#'), 1).toInt()};
0074             }
0075             clipMarkers.insert(binId, timecodes);
0076         } else {
0077             // That is a guide
0078             if (anchor.contains(QLatin1Char('?'))) {
0079                 guides << anchor.section(QLatin1Char('?'), 0, 0).toInt();
0080             } else {
0081                 guides << anchor.toInt();
0082             }
0083         }
0084     }
0085     QMapIterator<QString, QList<int>> i(clipMarkers);
0086     while (i.hasNext()) {
0087         i.next();
0088         // That's a Bin Clip reference.
0089         pCore->bin()->addClipMarker(i.key(), i.value());
0090     }
0091     if (!clipMarkers.isEmpty()) {
0092         const QString &binId = clipMarkers.firstKey();
0093         pCore->selectBinClip(binId, true, clipMarkers.value(binId).constFirst(), QPoint());
0094     }
0095     if (!guides.isEmpty()) {
0096         pCore->addGuides(guides);
0097     }
0098 }
0099 
0100 void NotesWidget::mouseMoveEvent(QMouseEvent *e)
0101 {
0102     const QString anchor = anchorAt(e->pos());
0103     if (anchor.isEmpty()) {
0104         viewport()->setCursor(Qt::IBeamCursor);
0105     } else {
0106         viewport()->setCursor(Qt::PointingHandCursor);
0107     }
0108     QTextEdit::mouseMoveEvent(e);
0109 }
0110 
0111 void NotesWidget::mousePressEvent(QMouseEvent *e)
0112 {
0113     QString anchor = anchorAt(e->pos());
0114     if (anchor.isEmpty() || e->button() != Qt::LeftButton) {
0115         QTextEdit::mousePressEvent(e);
0116         return;
0117     }
0118     if (anchor.contains(QLatin1Char('#'))) {
0119         // That's a Bin Clip reference.
0120         pCore->selectBinClip(anchor.section(QLatin1Char('#'), 0, 0), true, anchor.section(QLatin1Char('#'), 1).toInt(), QPoint());
0121     } else {
0122         Q_EMIT seekProject(anchor);
0123     }
0124     e->setAccepted(true);
0125 }
0126 
0127 QPair<QStringList, QList<QPoint>> NotesWidget::getSelectedAnchors()
0128 {
0129     int startPos = textCursor().selectionStart();
0130     int endPos = textCursor().selectionEnd();
0131     QStringList anchors;
0132     QList<QPoint> anchorPoints;
0133     if (endPos > startPos) {
0134         textCursor().clearSelection();
0135         QTextCursor cur(textCursor());
0136         // Ensure we are at the start of current selection
0137         if (!cur.atBlockStart()) {
0138             cur.setPosition(startPos, QTextCursor::MoveAnchor);
0139             int pos = startPos;
0140             const QString an = anchorAt(cursorRect(cur).center());
0141             while (!cur.atBlockStart()) {
0142                 pos--;
0143                 cur.setPosition(pos, QTextCursor::MoveAnchor);
0144                 if (anchorAt(cursorRect(cur).center()) == an) {
0145                     startPos = pos;
0146                 } else {
0147                     break;
0148                 }
0149             }
0150         }
0151         bool isInAnchor = false;
0152         QPoint anchorPoint;
0153         for (int p = startPos; p <= endPos; ++p) {
0154             cur.setPosition(p, QTextCursor::MoveAnchor);
0155             const QString anchor = anchorAt(cursorRect(cur).center());
0156             if (isInAnchor && !anchor.isEmpty() && p == endPos) {
0157                 endPos++;
0158             }
0159             if (isInAnchor && (anchor.isEmpty() || !anchors.contains(anchor) || cur.atEnd())) {
0160                 // End of current anchor
0161                 anchorPoint.setY(p);
0162                 anchorPoints.prepend(anchorPoint);
0163                 isInAnchor = false;
0164             }
0165             if (!anchor.isEmpty() && !anchors.contains(anchor)) {
0166                 anchors.prepend(anchor);
0167                 if (!isInAnchor) {
0168                     isInAnchor = true;
0169                     anchorPoint.setX(p);
0170                 }
0171             }
0172         }
0173     }
0174     return {anchors, anchorPoints};
0175 }
0176 
0177 void NotesWidget::assignProjectNote()
0178 {
0179     QPair<QStringList, QList<QPoint>> result = getSelectedAnchors();
0180     QStringList anchors = result.first;
0181     QList<QPoint> anchorPoints = result.second;
0182     if (!anchors.isEmpty()) {
0183         Q_EMIT reAssign(anchors, anchorPoints);
0184     } else {
0185         pCore->displayMessage(i18n("Select some timecodes to reassign"), ErrorMessage);
0186     }
0187 }
0188 
0189 void NotesWidget::createMarkers()
0190 {
0191     QPair<QStringList, QList<QPoint>> result = getSelectedAnchors();
0192     QStringList anchors = result.first;
0193     if (!anchors.isEmpty()) {
0194         createMarker(anchors);
0195     } else {
0196         pCore->displayMessage(i18n("Select some timecodes to create markers"), ErrorMessage);
0197     }
0198 }
0199 
0200 void NotesWidget::addProjectNote()
0201 {
0202     if (!textCursor().atBlockStart()) {
0203         QTextCursor cur = textCursor();
0204         cur.movePosition(QTextCursor::EndOfBlock);
0205         setTextCursor(cur);
0206         insertPlainText(QStringLiteral("\n"));
0207     }
0208     Q_EMIT insertNotesTimecode();
0209 }
0210 
0211 void NotesWidget::addTextNote(const QString &text)
0212 {
0213     if (!textCursor().atEnd()) {
0214         QTextCursor cur = textCursor();
0215         cur.movePosition(QTextCursor::End);
0216         setTextCursor(cur);
0217     }
0218     Q_EMIT insertTextNote(text);
0219 }
0220 
0221 void NotesWidget::insertFromMimeData(const QMimeData *source)
0222 {
0223     QString pastedText = source->text();
0224     bool enforceHtml = false;
0225     // Check for timecodes
0226     QStringList words = pastedText.split(QLatin1Char(' '));
0227     for (const QString &w : qAsConst(words)) {
0228         if (w.size() > 4 && w.size() < 13 && w.count(QLatin1Char(':')) > 1) {
0229             // This is probably a timecode
0230             int frames = pCore->timecode().getFrameCount(w);
0231             if (frames > 0) {
0232                 pastedText.replace(w, QStringLiteral("<a href=\"") + QString::number(frames) + QStringLiteral("\">") + w + QStringLiteral("</a> "));
0233                 enforceHtml = true;
0234             }
0235         }
0236     }
0237     if (enforceHtml || Qt::mightBeRichText(pastedText)) {
0238         insertHtml(pastedText);
0239     } else {
0240         insertPlainText(pastedText);
0241     }
0242 }
0243 
0244 bool NotesWidget::event(QEvent *event)
0245 {
0246     if (event->type() == QEvent::ToolTip) {
0247         QHelpEvent *helpEvent = static_cast<QHelpEvent *>(event);
0248         const QString anchor = anchorAt(helpEvent->pos());
0249         if (!anchor.isEmpty()) {
0250             QString sequenceName;
0251             if (anchor.contains(QLatin1Char('!'))) {
0252                 // We have a sequence reference
0253                 const QString binId = pCore->projectItemModel()->getSequenceId(QUuid(anchor.section(QLatin1Char('!'), 0, 0)));
0254                 if (!binId.isEmpty()) {
0255                     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
0256                     if (clip) {
0257                         sequenceName = clip->clipName();
0258                     }
0259                 }
0260             }
0261             if (!sequenceName.isEmpty()) {
0262                 QToolTip::showText(helpEvent->globalPos(), sequenceName);
0263             } else {
0264                 QToolTip::hideText();
0265             }
0266         } else {
0267             QToolTip::hideText();
0268         }
0269         return true;
0270     }
0271     return QTextEdit::event(event);
0272 }