Branch data Line data Source code
1 : : #include "FolderOperationsController.h"
2 : :
3 : : #include <QInputDialog>
4 : : #include <QLineEdit>
5 : : #include <QMessageBox>
6 : : #include <algorithm>
7 : : #include <memory>
8 : :
9 : : #include "service/ImapService.h"
10 : : #include "ui/CommandBar.h"
11 : : #include "ui/FolderTree.h"
12 : :
13 : 58 : FolderOperationsController::FolderOperationsController(const Deps &deps,
14 : 58 : QObject *parent)
15 : 58 : : QObject(parent), m_d(deps) {
16 : : // Modal test seams: real dialogs by default (unit tests override).
17 : 1 : m_promptText = [this](const QString &title, const QString &label,
18 : : const QString &initial, bool *ok) {
19 : 1 : return QInputDialog::getText(m_d.folderTree, title, label,
20 [ + - ]: 1 : QLineEdit::Normal, initial, ok);
21 : 58 : };
22 : 117 : m_confirm = [this](const QString &title, const QString &text) {
23 [ + - ]: 1 : return QMessageBox::question(m_d.folderTree, title, text) ==
24 : 1 : QMessageBox::Yes;
25 : 58 : };
26 : :
27 : : // T-290: FolderTree context-menu requests
28 : 58 : connect(m_d.folderTree, &FolderTree::createFolderRequested, this,
29 [ + - ]: 58 : &FolderOperationsController::createFolder);
30 : 58 : connect(m_d.folderTree, &FolderTree::deleteFolderRequested, this,
31 [ + - ]: 58 : &FolderOperationsController::deleteFolder);
32 : 58 : connect(m_d.folderTree, &FolderTree::renameFolderRequested, this,
33 [ + - ]: 58 : &FolderOperationsController::renameFolder);
34 : 58 : connect(m_d.folderTree, &FolderTree::moveFolderRequested, this,
35 [ + - ]: 58 : &FolderOperationsController::moveFolder);
36 : 58 : }
37 : :
38 : 33 : QStringList FolderOperationsController::folderFlagsForPath(
39 : : const QString &folderPath) const {
40 [ + + ]: 55 : for (const auto &folder : m_folders) {
41 [ + + ]: 44 : if (folder.path == folderPath)
42 : 22 : return folder.flags;
43 : : }
44 : 11 : return {};
45 : : }
46 : :
47 : 30 : bool FolderOperationsController::isProtectedFolderPath(
48 : : const QString &folderPath) const {
49 [ + - ]: 30 : return FolderTree::isSpecialFolder(folderPath,
50 [ + - ]: 60 : folderFlagsForPath(folderPath));
51 : : }
52 : :
53 : 9 : void FolderOperationsController::createFolder(const QString &parentPath) {
54 : 9 : bool ok = false;
55 : : QString name = m_promptText(
56 : 18 : QStringLiteral("Neuer Ordner"),
57 : 9 : parentPath.isEmpty()
58 [ + + + - : 32 : ? QStringLiteral("Name des neuen Ordners:")
- - ]
59 [ + + + + : 13 : : QStringLiteral("Name des neuen Unterordners von '%1':")
+ + - - -
- ]
60 : : .arg(parentPath),
61 [ + - ]: 27 : QString(), &ok);
62 : :
63 [ + + + - : 9 : if (!ok || name.trimmed().isEmpty())
+ + + + +
+ - - ]
64 : 4 : return;
65 : :
66 [ + - ]: 5 : name = name.trimmed();
67 : 5 : QString fullPath = parentPath.isEmpty()
68 : 5 : ? name
69 [ + + + - : 5 : : parentPath + effectiveDelimiter() + name;
+ - + - +
+ + + - -
- - ]
70 : :
71 [ + - + - ]: 5 : m_d.imap->executeAfterIdle(
72 : 15 : [this, fullPath]() { m_d.imap->createFolder(fullPath); });
73 [ + + ]: 9 : }
74 : :
75 : 9 : void FolderOperationsController::deleteFolder(const QString &folderPath) {
76 [ + - + + ]: 9 : if (isProtectedFolderPath(folderPath)) {
77 [ + - ]: 2 : emit keyedStatusMessage(
78 : 4 : QStringLiteral("folder"),
79 : 4 : QStringLiteral("Spezialordner koennen nicht geloescht werden"), 3000);
80 : 5 : return;
81 : : }
82 : :
83 : : // Count children for warning
84 [ + - ]: 7 : const QString delimiter = effectiveDelimiter();
85 : 7 : int childCount = 0;
86 : 7 : QStringList childPaths;
87 [ + - + - : 26 : for (const auto &f : m_folders) {
+ + ]
88 [ + - + - : 19 : if (f.path.startsWith(folderPath + delimiter)) {
+ + ]
89 : 4 : childCount++;
90 [ + - ]: 4 : childPaths.append(f.path);
91 : : }
92 : : }
93 : :
94 : : QString msg =
95 [ + - ]: 14 : QStringLiteral("Ordner '%1' wirklich loeschen?").arg(folderPath);
96 [ + + ]: 7 : if (childCount > 0)
97 : 4 : msg += QStringLiteral("\n\nDieser Ordner hat %1 Unterordner, die "
98 : : "ebenfalls geloescht werden.")
99 [ + - + - ]: 4 : .arg(childCount);
100 : :
101 [ + - + + ]: 14 : if (!m_confirm(QStringLiteral("Ordner loeschen"), msg))
102 : 3 : return;
103 : :
104 : : // Build delete list: children first (bottom-up by path depth)
105 : 4 : QStringList deleteList = childPaths;
106 [ + - + - : 4 : std::sort(deleteList.begin(), deleteList.end(),
+ - ]
107 : 2 : [&delimiter](const QString &a, const QString &b) {
108 : 2 : return a.count(delimiter) > b.count(delimiter);
109 : : });
110 [ + - ]: 4 : deleteList.append(folderPath); // parent last
111 : :
112 : : // FIX: IMAP servers send BYE when deleting the currently SELECTed folder.
113 : : // We must switch to INBOX before starting the delete chain.
114 : : // Check if current folder is the folder being deleted or a child of it.
115 : 4 : QString currentFolder = m_d.imap->selectedFolder();
116 [ + - ]: 8 : bool needSwitch = (currentFolder == folderPath) ||
117 [ + - + - : 8 : currentFolder.startsWith(folderPath + delimiter);
- + + - -
- ]
118 : :
119 : : // Chain sequential deletes
120 [ + - ]: 4 : auto remaining = std::make_shared<QStringList>(deleteList);
121 [ + - ]: 4 : auto deleteNext = std::make_shared<std::function<void()>>();
122 [ - - ]: 8 : *deleteNext = [this, remaining, deleteNext]() {
123 [ - + ]: 6 : if (remaining->isEmpty())
124 : 0 : return;
125 [ + - ]: 6 : QString next = remaining->takeFirst();
126 [ + + ]: 6 : if (!remaining->isEmpty()) {
127 [ + - ]: 2 : auto conn = std::make_shared<QMetaObject::Connection>();
128 : 4 : *conn = connect(m_d.imap, &ImapService::folderDeleted, this,
129 [ + - - - ]: 4 : [deleteNext, conn](const QString &) {
130 : 2 : QObject::disconnect(*conn);
131 : 2 : (*deleteNext)();
132 : 2 : });
133 : 2 : }
134 [ + - + - ]: 6 : m_d.imap->executeAfterIdle(
135 : 18 : [this, next]() { m_d.imap->deleteFolder(next); });
136 [ + - ]: 10 : };
137 : :
138 [ - + ]: 4 : if (needSwitch) {
139 : : // Switch to INBOX first, then start deleting
140 [ # # ]: 0 : m_d.folderTree->selectFolder(QStringLiteral("INBOX"));
141 : : // Wait for SELECT INBOX to complete before deleting
142 [ # # ]: 0 : auto conn = std::make_shared<QMetaObject::Connection>();
143 : 0 : *conn = connect(m_d.imap, &ImapService::folderSelected, this,
144 [ # # # # ]: 0 : [deleteNext, conn](const QString &, int, quint32, quint64) {
145 : 0 : QObject::disconnect(*conn);
146 : 0 : (*deleteNext)();
147 : 0 : });
148 : 0 : } else {
149 [ + - ]: 4 : (*deleteNext)();
150 : : }
151 [ + + + + : 13 : }
+ + ]
152 : :
153 : 5 : void FolderOperationsController::renameFolder(const QString &folderPath) {
154 [ + - + + ]: 5 : if (isProtectedFolderPath(folderPath)) {
155 [ + - ]: 1 : emit keyedStatusMessage(
156 : 2 : QStringLiteral("folder"),
157 : 2 : QStringLiteral("Spezialordner koennen nicht umbenannt werden"), 3000);
158 : 2 : return;
159 : : }
160 : :
161 : : // Extract current name (last segment after delimiter)
162 [ + - ]: 4 : const QString delimiter = effectiveDelimiter();
163 [ + - ]: 4 : int lastSep = folderPath.lastIndexOf(delimiter);
164 : : QString currentName = (lastSep >= 0)
165 [ + + ]: 4 : ? folderPath.mid(lastSep + delimiter.length())
166 [ + - ]: 4 : : folderPath;
167 [ + + + - ]: 4 : QString parentPath = (lastSep >= 0) ? folderPath.left(lastSep) : QString();
168 : :
169 : 4 : bool ok = false;
170 : : QString newName = m_promptText(
171 : 8 : QStringLiteral("Ordner umbenennen"),
172 [ + - ]: 12 : QStringLiteral("Neuer Name für '%1':").arg(currentName),
173 [ + - ]: 8 : currentName, &ok);
174 : :
175 [ + - + - : 4 : if (!ok || newName.trimmed().isEmpty() || newName.trimmed() == currentName)
+ - + - +
+ + - + -
+ + - - -
- ]
176 : 1 : return;
177 : :
178 [ + - ]: 3 : newName = newName.trimmed();
179 : : QString newPath =
180 [ + + + - : 3 : parentPath.isEmpty() ? newName : parentPath + delimiter + newName;
+ - + + -
- ]
181 : :
182 [ + - + - : 3 : m_d.imap->executeAfterIdle([this, folderPath, newPath]() {
- - ]
183 : 3 : m_d.imap->renameFolder(folderPath, newPath);
184 : 3 : });
185 [ + + + + : 7 : }
+ + + + ]
186 : :
187 : 6 : void FolderOperationsController::moveFolder(const QString &folderPath) {
188 [ + - + + ]: 6 : if (isProtectedFolderPath(folderPath)) {
189 [ + - ]: 1 : emit keyedStatusMessage(
190 : 2 : QStringLiteral("folder"),
191 : 2 : QStringLiteral("Spezialordner koennen nicht verschoben werden"), 3000);
192 : 1 : return;
193 : : }
194 : :
195 : : // Activate CommandBar in FolderSwitch mode to pick target
196 [ + - ]: 5 : m_d.commandBar->activate(CommandBar::FolderSwitch);
197 : :
198 : : // One-shot connection: when folder is selected, perform RENAME
199 [ + - ]: 5 : auto conn = std::make_shared<QMetaObject::Connection>();
200 : 10 : *conn = connect(
201 : 5 : m_d.commandBar, &CommandBar::folderSelected, this,
202 [ + - - - ]: 10 : [this, folderPath, conn](CommandBar::Mode mode,
203 : : const QString &targetFolder) {
204 [ + - ]: 3 : QObject::disconnect(*conn);
205 [ + + ]: 3 : if (mode != CommandBar::FolderSwitch)
206 : 1 : return;
207 : :
208 : : // Build new path: targetFolder + delimiter + folderName
209 [ + - ]: 2 : const QString delimiter = effectiveDelimiter();
210 [ + - ]: 2 : int lastSep = folderPath.lastIndexOf(delimiter);
211 : : QString folderName = (lastSep >= 0)
212 [ - + ]: 2 : ? folderPath.mid(lastSep + delimiter.length())
213 [ - - ]: 2 : : folderPath;
214 [ + - + - ]: 2 : QString newPath = targetFolder + delimiter + folderName;
215 : :
216 [ + - + - : 2 : m_d.imap->executeAfterIdle([this, folderPath, newPath]() {
- - ]
217 : 2 : m_d.imap->renameFolder(folderPath, newPath);
218 : 2 : });
219 : 7 : });
220 : :
221 : : // Clean up if cancelled
222 [ + - ]: 5 : auto cancelConn = std::make_shared<QMetaObject::Connection>();
223 : 10 : *cancelConn = connect(m_d.commandBar, &CommandBar::cancelled, this,
224 [ + - - - ]: 10 : [conn, cancelConn]() {
225 : 3 : QObject::disconnect(*conn);
226 : 3 : QObject::disconnect(*cancelConn);
227 : 5 : });
228 : 5 : }
|