2020#include < QDir>
2121#include < QFile>
2222#include < QFileInfo>
23+ #include < QKeyEvent>
2324#include < QMenu>
2425#include < QMessageBox>
2526#include < QRegularExpression>
27+ #include < QSet>
2628#include < QSignalBlocker>
2729#include < QTextCursor>
2830#include < QTextDocument>
@@ -57,6 +59,12 @@ QString encodedFileName(const QString &fileName) {
5759 return QString (QUrl::toPercentEncoding (fileName));
5860}
5961
62+ struct SelectedLinkedFile {
63+ QString filePath;
64+ QString fileName;
65+ FileNavigationWidget::FileItemType type;
66+ };
67+
6068QRegularExpression linkedFileNameRegex (const QString &folderName, const QString &fileName) {
6169 return QRegularExpression (
6270 QStringLiteral (R"( ((?:!\[.*?\]|\[.*?\])\((?:.*?%1\/.*?))%2((?:#[^)]+)?\)))" )
@@ -88,6 +96,20 @@ bool replaceLinkedFileNameInText(QString &text, const QString &folderName,
8896 return text != originalText;
8997}
9098
99+ bool removeLinkedFileLinksFromText (QString &text, const QString &folderName,
100+ const QString &fileName) {
101+ const QString encodedPath = encodedFileName (fileName);
102+ const QString originalText = text;
103+
104+ text.replace (linkedFileNameRegex (folderName, fileName), QString ());
105+
106+ if (encodedPath != fileName) {
107+ text.replace (linkedFileNameRegex (folderName, encodedPath), QString ());
108+ }
109+
110+ return text != originalText;
111+ }
112+
91113QVector<int > findAffectedNoteIds (const QString &fileName, FileNavigationWidget::FileItemType type) {
92114 QVector<int > affectedNoteIds;
93115 const QString folderName = linkedFolderName (type);
@@ -110,6 +132,100 @@ QVector<int> findAffectedNoteIds(const QString &fileName, FileNavigationWidget::
110132
111133 return affectedNoteIds;
112134}
135+
136+ QVector<SelectedLinkedFile> selectedLinkedFiles (const QList<QTreeWidgetItem *> &items) {
137+ QVector<SelectedLinkedFile> linkedFiles;
138+ QSet<QString> seenFilePaths;
139+
140+ for (const auto *item : items) {
141+ if (item == nullptr ) {
142+ continue ;
143+ }
144+
145+ const QString filePath = item->data (0 , FilePathRole).toString ();
146+ const QString fileName = item->data (0 , FileNameRole).toString ();
147+
148+ if (filePath.isEmpty () || fileName.isEmpty () || seenFilePaths.contains (filePath)) {
149+ continue ;
150+ }
151+
152+ seenFilePaths.insert (filePath);
153+ linkedFiles.append (
154+ {filePath, fileName,
155+ static_cast <FileNavigationWidget::FileItemType>(item->data (0 , FileTypeRole).toInt ())});
156+ }
157+
158+ return linkedFiles;
159+ }
160+
161+ QVector<int > findAffectedNoteIds (const QVector<SelectedLinkedFile> &linkedFiles) {
162+ QVector<int > affectedNoteIds;
163+ QSet<int > seenNoteIds;
164+
165+ for (const auto &linkedFile : linkedFiles) {
166+ for (const int noteId : findAffectedNoteIds (linkedFile.fileName , linkedFile.type )) {
167+ if (seenNoteIds.contains (noteId)) {
168+ continue ;
169+ }
170+
171+ seenNoteIds.insert (noteId);
172+ affectedNoteIds.append (noteId);
173+ }
174+ }
175+
176+ return affectedNoteIds;
177+ }
178+
179+ bool removeLinkedFileLinksFromNotes (const QVector<int > &affectedNoteIds,
180+ const QVector<SelectedLinkedFile> &linkedFiles) {
181+ if (affectedNoteIds.isEmpty () || linkedFiles.isEmpty ()) {
182+ return false ;
183+ }
184+
185+ MainWindow *mainWindow = MainWindow::instance ();
186+ const int currentNoteId = mainWindow == nullptr ? -1 : mainWindow->getCurrentNote ().getId ();
187+ QOwnNotesMarkdownTextEdit *textEdit =
188+ mainWindow == nullptr ? nullptr : mainWindow->activeNoteTextEdit ();
189+ QString updatedCurrentNoteText;
190+ bool currentNoteUpdated = false ;
191+ bool anyNoteUpdated = false ;
192+
193+ for (const int noteId : affectedNoteIds) {
194+ Note note = Note::fetch (noteId);
195+
196+ if (!note.isFetched ()) {
197+ continue ;
198+ }
199+
200+ QString text = ((noteId == currentNoteId) && (textEdit != nullptr ))
201+ ? textEdit->toPlainText ()
202+ : note.getNoteText ();
203+ bool noteChanged = false ;
204+
205+ for (const auto &linkedFile : linkedFiles) {
206+ noteChanged = removeLinkedFileLinksFromText (text, linkedFolderName (linkedFile.type ),
207+ linkedFile.fileName ) ||
208+ noteChanged;
209+ }
210+
211+ if (!noteChanged || !note.storeNewText (text)) {
212+ continue ;
213+ }
214+
215+ anyNoteUpdated = true ;
216+
217+ if (noteId == currentNoteId) {
218+ updatedCurrentNoteText = text;
219+ currentNoteUpdated = true ;
220+ }
221+ }
222+
223+ if (currentNoteUpdated && (mainWindow != nullptr )) {
224+ mainWindow->setCurrentNoteText (updatedCurrentNoteText);
225+ }
226+
227+ return anyNoteUpdated;
228+ }
113229} // namespace
114230
115231FileNavigationWidget::FileNavigationWidget (QWidget *parent) : QTreeWidget(parent) {
@@ -319,6 +435,97 @@ void FileNavigationWidget::onItemChanged(QTreeWidgetItem *item, int column) {
319435 }
320436}
321437
438+ void FileNavigationWidget::keyPressEvent (QKeyEvent *event) {
439+ if ((event != nullptr ) && (state () != QAbstractItemView::EditingState) &&
440+ ((event->key () == Qt::Key_Delete) || (event->key () == Qt::Key_Backspace))) {
441+ auto *item = currentItem ();
442+
443+ if ((item != nullptr ) && !item->data (0 , FilePathRole).toString ().isEmpty ()) {
444+ deleteSelectedLinkedFiles ();
445+ event->accept ();
446+ return ;
447+ }
448+ }
449+
450+ QTreeWidget::keyPressEvent (event);
451+ }
452+
453+ bool FileNavigationWidget::deleteSelectedLinkedFiles () {
454+ QVector<SelectedLinkedFile> linkedFiles = selectedLinkedFiles (selectedItems ());
455+
456+ if (linkedFiles.isEmpty ()) {
457+ auto *item = currentItem ();
458+
459+ if ((item == nullptr ) || item->data (0 , FilePathRole).toString ().isEmpty ()) {
460+ return false ;
461+ }
462+
463+ QList<QTreeWidgetItem *> items;
464+ items.append (item);
465+ linkedFiles = selectedLinkedFiles (items);
466+ }
467+
468+ if (linkedFiles.isEmpty ()) {
469+ return false ;
470+ }
471+
472+ const int selectedFileCount = linkedFiles.count ();
473+ const QVector<int > affectedNoteIds = findAffectedNoteIds (linkedFiles);
474+ const int affectedNoteCount = affectedNoteIds.count ();
475+
476+ if (Utils::Gui::question (
477+ this , tr (" Delete linked files" ),
478+ tr (" Delete <strong>%n</strong> selected linked file(s)? You can also remove "
479+ " their links from <strong>%1</strong> note file(s) afterwards." ,
480+ " " , selectedFileCount)
481+ .arg (affectedNoteCount),
482+ QStringLiteral (" delete-linked-files" )) != QMessageBox::Yes) {
483+ return true ;
484+ }
485+
486+ QVector<SelectedLinkedFile> deletedLinkedFiles;
487+ QVector<QString> failedFilePaths;
488+
489+ for (const auto &linkedFile : linkedFiles) {
490+ QFile file (linkedFile.filePath );
491+
492+ if (!file.exists () || !file.remove ()) {
493+ failedFilePaths.append (linkedFile.filePath );
494+ continue ;
495+ }
496+
497+ deletedLinkedFiles.append (linkedFile);
498+ }
499+
500+ if (!failedFilePaths.isEmpty ()) {
501+ const QString message = failedFilePaths.count () == 1
502+ ? tr (" Deleting the file <strong>%1</strong> failed!" )
503+ .arg (failedFilePaths.first ().toHtmlEscaped ())
504+ : tr (" Deleting <strong>%n</strong> linked file(s) failed!" , " " ,
505+ failedFilePaths.count ());
506+ QMessageBox::warning (this , tr (" File deleting failed" ), message);
507+ }
508+
509+ if (!deletedLinkedFiles.isEmpty ()) {
510+ const QVector<int > affectedDeletedNoteIds = findAffectedNoteIds (deletedLinkedFiles);
511+ const int affectedDeletedNoteCount = affectedDeletedNoteIds.count ();
512+
513+ if ((affectedDeletedNoteCount > 0 ) &&
514+ (Utils::Gui::questionNoSkipOverride (
515+ this , tr (" Remove linked file references" ),
516+ tr (" The deleted file(s) are used in <strong>%n</strong> note file(s). "
517+ " Would you like to remove those media and attachment links "
518+ " everywhere? This will update <strong>%n</strong> note file(s)." ,
519+ " " , affectedDeletedNoteCount),
520+ QStringLiteral (" note-remove-linked-file-references" )) == QMessageBox::Yes)) {
521+ removeLinkedFileLinksFromNotes (affectedDeletedNoteIds, deletedLinkedFiles);
522+ }
523+ }
524+
525+ refreshFromCurrentNote ();
526+ return true ;
527+ }
528+
322529bool FileNavigationWidget::renameLinkedFile (QTreeWidgetItem *item, const QString &oldFileName,
323530 const QString &newFileName, FileItemType type) {
324531 const QString oldFilePath = item->data (0 , FilePathRole).toString ();
@@ -410,15 +617,21 @@ bool FileNavigationWidget::renameLinkedFile(QTreeWidgetItem *item, const QString
410617 }
411618 }
412619
620+ refreshFromCurrentNote ();
621+
622+ return true ;
623+ }
624+
625+ void FileNavigationWidget::refreshFromCurrentNote () {
413626 MainWindow *mainWindow = MainWindow::instance ();
414- if (mainWindow != nullptr ) {
415- if (auto *textEdit = mainWindow->activeNoteTextEdit (); textEdit != nullptr ) {
416- _fileLinkNodes.clear ();
417- parse (textEdit->document (), textEdit->textCursor ().position ());
418- }
627+ if (mainWindow == nullptr ) {
628+ return ;
419629 }
420630
421- return true ;
631+ if (auto *textEdit = mainWindow->activeNoteTextEdit (); textEdit != nullptr ) {
632+ _fileLinkNodes.clear ();
633+ parse (textEdit->document (), textEdit->textCursor ().position ());
634+ }
422635}
423636
424637void FileNavigationWidget::showContextMenu (const QPoint &pos) {
@@ -431,8 +644,14 @@ void FileNavigationWidget::showContextMenu(const QPoint &pos) {
431644
432645 QMenu menu (this );
433646 auto *openAction = menu.addAction (tr (" &Open file externally" ));
647+ auto *deleteAction = menu.addAction (tr (" &Delete file" ));
434648
435649 QAction *selectedAction = menu.exec (mapToGlobal (pos));
650+ if (selectedAction == deleteAction) {
651+ deleteSelectedLinkedFiles ();
652+ return ;
653+ }
654+
436655 if (selectedAction != openAction) {
437656 return ;
438657 }
0 commit comments