Skip to content

Commit f2ffa2c

Browse files
committed
#3499 filenavigationwidget: allow deleting of files
Signed-off-by: Patrizio Bekerle <patrizio@bekerle.com>
1 parent f2734b5 commit f2ffa2c

File tree

3 files changed

+233
-6
lines changed

3 files changed

+233
-6
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
panel's "Files" tab via `F2`, with a follow-up refactoring prompt that tells
1111
you how many note files will be updated before renaming linked filenames
1212
everywhere (for [#3499](https://github.com/pbek/QOwnNotes/issues/3499))
13+
- Added deleting linked media files and attachments from the Navigation panel's
14+
"Files" tab via `Del` and a context menu action, with confirmations that show
15+
how many note files can be updated and an optional follow-up dialog to remove
16+
those file links everywhere (for [#3499](https://github.com/pbek/QOwnNotes/issues/3499))
1317
- Changed the Navigation panel to only show the optional "Files" and
1418
"Backlinks" tabs when the current note actually has file links or backlinks
1519
to display (for [#3499](https://github.com/pbek/QOwnNotes/issues/3499))

src/widgets/filenavigationwidget.cpp

Lines changed: 225 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
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+
6068
QRegularExpression 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+
91113
QVector<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

115231
FileNavigationWidget::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+
322529
bool 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

424637
void 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
}

src/widgets/filenavigationwidget.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <QTreeWidget>
1919
#include <QVector>
2020

21+
class QKeyEvent;
2122
class QTextDocument;
2223
class QTreeWidgetItem;
2324

@@ -58,10 +59,13 @@ class FileNavigationWidget : public QTreeWidget {
5859
void positionClicked(int position);
5960

6061
private:
62+
void keyPressEvent(QKeyEvent *event) override;
6163
void buildTree(const QVector<FileLinkNode> &nodes);
6264
void emitPositionForItem(const QTreeWidgetItem *item);
65+
bool deleteSelectedLinkedFiles();
6366
bool renameLinkedFile(QTreeWidgetItem *item, const QString &oldFileName,
6467
const QString &newFileName, FileItemType type);
68+
void refreshFromCurrentNote();
6569

6670
QVector<FileLinkNode> _fileLinkNodes;
6771
int _cursorPosition = 0;

0 commit comments

Comments
 (0)