Branch data Line data Source code
1 : : #include "FolderPropertiesDialog.h"
2 : :
3 : : #include "ui/ThemeManager.h"
4 : : #include "MdiIconProvider.h"
5 : : #include "data/MailCache.h"
6 : : #include "data/FolderPredictor.h"
7 : :
8 : : #include <QCheckBox>
9 : : #include <QColorDialog>
10 : : #include <QDialogButtonBox>
11 : : #include <QGroupBox>
12 : : #include <QHBoxLayout>
13 : : #include <QLabel>
14 : : #include <QLineEdit>
15 : : #include <QListWidget>
16 : : #include <QPushButton>
17 : : #include <QVBoxLayout>
18 : : #include <QEvent>
19 : :
20 : : static constexpr int ICON_SIZE = 32; // display size in picker
21 : : static constexpr int ICON_RENDER = 28; // rendered glyph size
22 : :
23 : 10 : FolderPropertiesDialog::FolderPropertiesDialog(const Options &opts,
24 : : MailCache *cache,
25 : : FolderPredictor *predictor,
26 : : const QString &account,
27 : 10 : QWidget *parent)
28 : 20 : : QDialog(parent), m_opts(opts), m_cache(cache), m_predictor(predictor),
29 : 10 : m_account(account),
30 [ + - ]: 20 : m_selectedIcon(opts.currentIcon), m_selectedColor(opts.currentColor) {
31 [ + - + - : 20 : setWindowTitle(tr("Edit Folder – %1").arg(opts.folderPath));
+ - ]
32 [ + - ]: 10 : setMinimumWidth(480);
33 : :
34 [ + - + - : 10 : auto *mainLayout = new QVBoxLayout(this);
- + - - ]
35 [ + - ]: 10 : mainLayout->setSpacing(12);
36 : :
37 [ + - ]: 10 : buildAppearanceSection();
38 [ + - ]: 10 : buildSubscriptionSection();
39 [ + - ]: 10 : buildCacheSection();
40 [ + - ]: 10 : buildStatsSection(); // T-288
41 : :
42 : : auto *buttonBox =
43 [ + - + - : 10 : new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
- + - - ]
44 [ + - ]: 10 : connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
45 [ + - ]: 10 : connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
46 [ + - ]: 10 : mainLayout->addWidget(buttonBox);
47 : 10 : }
48 : :
49 : 10 : void FolderPropertiesDialog::buildAppearanceSection() {
50 [ + - + - : 10 : auto *group = new QGroupBox(tr("Appearance"), this);
- + - - ]
51 [ + - - + : 10 : auto *vbox = new QVBoxLayout(group);
- - ]
52 : :
53 : : // ── Icon picker ──────────────────────────────────────────────────────────
54 [ + - + - : 10 : vbox->addWidget(new QLabel(tr("Icon:")));
+ - + - -
+ - - ]
55 : :
56 : : // Search field
57 [ + - - + : 10 : m_iconFilter = new QLineEdit;
- - ]
58 [ + - + - ]: 10 : m_iconFilter->setPlaceholderText(tr("Search icon… (e.g. folder, star, email)"));
59 : 10 : m_iconFilter->setClearButtonEnabled(true);
60 [ + - ]: 10 : vbox->addWidget(m_iconFilter);
61 : :
62 : : // QListWidget in IconMode — only visible cells are rendered (virtual scroll)
63 [ + - - + : 10 : m_iconList = new QListWidget;
- - ]
64 : 10 : m_iconList->setViewMode(QListView::IconMode);
65 : 10 : m_iconList->setResizeMode(QListView::Adjust);
66 : 10 : m_iconList->setMovement(QListView::Static);
67 [ + - ]: 10 : m_iconList->setIconSize(QSize(ICON_RENDER, ICON_RENDER));
68 [ + - ]: 10 : m_iconList->setGridSize(QSize(ICON_SIZE + 4, ICON_SIZE + 4));
69 : 10 : m_iconList->setUniformItemSizes(true);
70 : 10 : m_iconList->setWordWrap(false);
71 : 10 : m_iconList->setFixedHeight(240);
72 : 10 : m_iconList->setSelectionMode(QAbstractItemView::SingleSelection);
73 : : // Styled via main.qss generic QListWidget rules (67.B3)
74 [ + - ]: 10 : vbox->addWidget(m_iconList);
75 : :
76 : : // Populate with all icons (lazy: QListWidget only paints visible ones)
77 : 10 : auto &mdi = MdiIconProvider::instance();
78 : 10 : mdi.ensureInit();
79 : 10 : const auto &icons = mdi.allIcons();
80 [ + + ]: 74490 : for (const auto &e : icons) {
81 [ + - + - : 74480 : auto *item = new QListWidgetItem;
- + - - ]
82 [ + - + - ]: 74480 : item->setIcon(mdi.icon(e.name, ICON_RENDER));
83 [ + - ]: 74480 : item->setData(Qt::UserRole, e.name);
84 [ + - ]: 74480 : item->setToolTip(e.name);
85 [ + - ]: 74480 : item->setSizeHint(QSize(ICON_SIZE + 4, ICON_SIZE + 4));
86 [ + - ]: 74480 : m_iconList->addItem(item);
87 [ - + ]: 74480 : if (e.name == m_selectedIcon) {
88 [ # # ]: 0 : m_iconList->setCurrentItem(item);
89 : : }
90 : : }
91 : :
92 : 10 : connect(m_iconFilter, &QLineEdit::textChanged,
93 [ + - ]: 10 : this, &FolderPropertiesDialog::filterIcons);
94 : :
95 : 10 : connect(m_iconList, &QListWidget::currentItemChanged, this,
96 [ + - ]: 10 : [this](QListWidgetItem *item) {
97 [ + - + - : 1 : m_selectedIcon = item ? item->data(Qt::UserRole).toString() : QString{};
+ - + - -
- ]
98 : 1 : });
99 : :
100 : : // ── Color picker ─────────────────────────────────────────────────────────
101 [ + - - + : 10 : auto *colorRow = new QHBoxLayout;
- - ]
102 [ + - + - : 10 : colorRow->addWidget(new QLabel(tr("Color:")));
+ - + - -
+ - - ]
103 [ + - - + : 10 : m_colorBtn = new QPushButton;
- - ]
104 : 10 : m_colorBtn->setFixedSize(80, 28);
105 : 10 : updateColorPreview();
106 [ + - ]: 10 : connect(m_colorBtn, &QPushButton::clicked, this, [this]() {
107 [ + - ]: 1 : QColor initial = m_selectedColor.isEmpty() ? Qt::white : QColor(m_selectedColor);
108 [ + - + - ]: 1 : QColor color = QColorDialog::getColor(initial, this, tr("Choose Folder Color"));
109 [ - + ]: 1 : if (color.isValid()) {
110 [ # # ]: 0 : m_selectedColor = color.name();
111 [ # # ]: 0 : updateColorPreview();
112 : : }
113 : 1 : });
114 [ + - ]: 10 : colorRow->addWidget(m_colorBtn);
115 : 10 : colorRow->addStretch();
116 : :
117 [ + - + - : 10 : auto *resetBtn = new QPushButton(tr("Reset"));
- + - - ]
118 [ + - ]: 10 : connect(resetBtn, &QPushButton::clicked, this, [this]() {
119 : 1 : m_selectedIcon.clear();
120 : 1 : m_selectedColor.clear();
121 : 1 : m_iconList->clearSelection();
122 : 1 : updateColorPreview();
123 : 1 : });
124 [ + - ]: 10 : colorRow->addWidget(resetBtn);
125 : 10 : vbox->addLayout(colorRow);
126 : :
127 [ + - ]: 10 : static_cast<QVBoxLayout *>(layout())->addWidget(group);
128 : 10 : }
129 : :
130 : 7 : void FolderPropertiesDialog::filterIcons(const QString &text) {
131 : : // Hide/show items based on name match — O(n) but instant for 7k items
132 : 7 : auto &mdi = MdiIconProvider::instance();
133 : 7 : const auto &icons = mdi.allIcons();
134 : :
135 : : // We iterate items and icons in lock-step (same order)
136 : 7 : int count = m_iconList->count();
137 : 7 : int iconCount = icons.size();
138 [ + + + - ]: 52143 : for (int i = 0; i < count && i < iconCount; ++i) {
139 [ + + + + ]: 52136 : bool visible = text.isEmpty() || icons[i].name.contains(text, Qt::CaseInsensitive);
140 : 52136 : m_iconList->item(i)->setHidden(!visible);
141 : : }
142 : 7 : }
143 : :
144 : 10 : void FolderPropertiesDialog::buildSubscriptionSection() {
145 [ + - + - : 10 : auto *group = new QGroupBox(tr("Subscription"), this);
- + - - ]
146 [ + - - + : 10 : auto *vbox = new QVBoxLayout(group);
- - ]
147 : :
148 [ + - + - : 10 : m_subscribedCheck = new QCheckBox(tr("Subscribe folder for badges"), group);
- + - - ]
149 : 10 : m_subscribedCheck->setChecked(m_opts.isSubscribed);
150 [ + - ]: 10 : m_subscribedCheck->setToolTip(
151 [ + - ]: 20 : tr("Subscribed folders are included in badge polling"));
152 [ + - ]: 10 : vbox->addWidget(m_subscribedCheck);
153 : :
154 [ + - + - : 10 : m_hiddenCheck = new QCheckBox(tr("Hide in folder tree"), group);
- + - - ]
155 : 10 : m_hiddenCheck->setChecked(m_opts.isHidden);
156 [ + - ]: 10 : m_hiddenCheck->setToolTip(
157 [ + - ]: 20 : tr("Hidden folders and their subfolders are not displayed"));
158 [ + - ]: 10 : vbox->addWidget(m_hiddenCheck);
159 : :
160 [ + - ]: 10 : static_cast<QVBoxLayout *>(layout())->addWidget(group);
161 : 10 : }
162 : :
163 : 10 : void FolderPropertiesDialog::buildCacheSection() {
164 [ + - + - : 10 : auto *group = new QGroupBox(tr("Cache"), this);
- + - - ]
165 [ + - - + : 10 : auto *vbox = new QVBoxLayout(group);
- - ]
166 : :
167 [ + - - + : 10 : m_headerCountLabel = new QLabel(group);
- - ]
168 [ + - - + : 10 : m_bodyCountLabel = new QLabel(group);
- - ]
169 : 10 : refreshCacheLabels();
170 [ + - ]: 10 : vbox->addWidget(m_headerCountLabel);
171 [ + - ]: 10 : vbox->addWidget(m_bodyCountLabel);
172 : :
173 [ + - + - : 10 : auto *purgeBtn = new QPushButton(tr("Clear Cache"));
- + - - ]
174 [ + - + - ]: 10 : purgeBtn->setToolTip(tr("Delete all cached data for this folder"));
175 [ + - ]: 10 : connect(purgeBtn, &QPushButton::clicked, this, [this]() {
176 [ + - ]: 1 : if (m_cache) {
177 : 1 : m_cache->purgeFolderByPath(m_account, m_opts.folderPath);
178 : 1 : refreshCacheLabels();
179 : : }
180 : 1 : });
181 [ + - ]: 10 : vbox->addWidget(purgeBtn);
182 : :
183 [ + - ]: 10 : static_cast<QVBoxLayout *>(layout())->addWidget(group);
184 : 10 : }
185 : :
186 : 11 : void FolderPropertiesDialog::updateColorPreview() {
187 [ + + ]: 11 : if (m_selectedColor.isEmpty()) {
188 [ + - ]: 10 : m_colorBtn->setStyleSheet(QString());
189 [ + - + - ]: 10 : m_colorBtn->setText(tr("None"));
190 : : } else {
191 [ + - ]: 2 : m_colorBtn->setStyleSheet(
192 : 2 : QStringLiteral("background-color: %1; border: 1px solid %2;")
193 [ + - ]: 2 : .arg(m_selectedColor,
194 [ + - + - ]: 3 : ThemeManager::instance().color(
195 : 2 : QStringLiteral("@border_medium"))));
196 : 1 : m_colorBtn->setText(m_selectedColor);
197 : : }
198 : 11 : }
199 : :
200 : 12 : void FolderPropertiesDialog::refreshCacheLabels() {
201 : 12 : int headers = 0, bodies = 0;
202 [ + + ]: 12 : if (m_cache) {
203 : 11 : headers = m_cache->cachedHeaderCount(m_account, m_opts.folderPath);
204 : 11 : bodies = m_cache->cachedBodyCount(m_account, m_opts.folderPath);
205 : : }
206 [ + - + - : 24 : m_headerCountLabel->setText(tr("Cached Headers: %1").arg(headers));
+ - ]
207 [ + - + - : 24 : m_bodyCountLabel->setText(tr("Body Cache: %1").arg(bodies));
+ - ]
208 : 12 : }
209 : :
210 : 7 : bool FolderPropertiesDialog::isSubscribed() const {
211 : 7 : return m_subscribedCheck->isChecked();
212 : : }
213 : :
214 : 7 : bool FolderPropertiesDialog::isHidden() const {
215 : 7 : return m_hiddenCheck->isChecked();
216 : : }
217 : :
218 : : // T-288: Human-readable file size formatting
219 : 33 : static QString formatSize(qint64 bytes) {
220 [ + - ]: 33 : if (bytes < 1024)
221 [ + - ]: 66 : return QStringLiteral("%1 B").arg(bytes);
222 [ # # ]: 0 : if (bytes < 1024 * 1024)
223 [ # # ]: 0 : return QStringLiteral("%1 KB").arg(bytes / 1024.0, 0, 'f', 1);
224 [ # # ]: 0 : if (bytes < 1024LL * 1024 * 1024)
225 [ # # ]: 0 : return QStringLiteral("%1 MB").arg(bytes / (1024.0 * 1024), 0, 'f', 1);
226 [ # # ]: 0 : return QStringLiteral("%1 GB").arg(bytes / (1024.0 * 1024 * 1024), 0, 'f', 2);
227 : : }
228 : :
229 : 10 : void FolderPropertiesDialog::buildStatsSection() {
230 [ + - + - : 10 : auto *group = new QGroupBox(tr("Statistics"), this);
- + - - ]
231 [ + - - + : 10 : auto *vbox = new QVBoxLayout(group);
- - ]
232 : :
233 [ + - - + : 10 : m_diskUsageLabel = new QLabel(group);
- - ]
234 [ + - - + : 10 : m_serverSizeLabel = new QLabel(group);
- - ]
235 [ + - - + : 10 : m_avgSizeLabel = new QLabel(group);
- - ]
236 [ + - - + : 10 : m_predictorDocsLabel = new QLabel(group);
- - ]
237 [ + - - + : 10 : m_predictorTokensLabel = new QLabel(group);
- - ]
238 : :
239 [ + - ]: 10 : vbox->addWidget(m_diskUsageLabel);
240 [ + - ]: 10 : vbox->addWidget(m_serverSizeLabel);
241 [ + - ]: 10 : vbox->addWidget(m_avgSizeLabel);
242 : :
243 : 10 : vbox->addSpacing(4);
244 [ + - ]: 10 : vbox->addWidget(m_predictorDocsLabel);
245 [ + - ]: 10 : vbox->addWidget(m_predictorTokensLabel);
246 : :
247 : : // Body-only purge button
248 [ + - + - : 10 : auto *bodyPurgeBtn = new QPushButton(tr("Clear Body Cache Only"));
- + - - ]
249 [ + - ]: 10 : bodyPurgeBtn->setToolTip(
250 [ + - ]: 20 : tr("Removes cached mail content but keeps headers"));
251 [ + - ]: 10 : connect(bodyPurgeBtn, &QPushButton::clicked, this, [this]() {
252 [ + - ]: 1 : if (m_cache) {
253 : 1 : m_cache->purgeBodyCache(m_account, m_opts.folderPath);
254 : 1 : refreshCacheLabels();
255 : 1 : refreshStatsLabels();
256 : : }
257 : 1 : });
258 [ + - ]: 10 : vbox->addWidget(bodyPurgeBtn);
259 : :
260 : : // Predictor reset button
261 [ + - + - : 10 : auto *resetPredBtn = new QPushButton(tr("Reset Predictor Data"));
- + - - ]
262 [ + - ]: 10 : resetPredBtn->setToolTip(
263 [ + - ]: 20 : tr("Resets the training data for this folder"));
264 [ + - ]: 10 : connect(resetPredBtn, &QPushButton::clicked, this, [this]() {
265 [ + - ]: 1 : if (m_predictor) {
266 : 1 : m_predictor->resetFolder(m_opts.folderPath);
267 : 1 : refreshStatsLabels();
268 : : }
269 : 1 : });
270 [ + - ]: 10 : vbox->addWidget(resetPredBtn);
271 : :
272 : 10 : refreshStatsLabels();
273 [ + - ]: 10 : static_cast<QVBoxLayout *>(layout())->addWidget(group);
274 : 10 : }
275 : :
276 : 12 : void FolderPropertiesDialog::refreshStatsLabels() {
277 [ + + ]: 12 : if (m_cache) {
278 : 11 : qint64 diskUsage = m_cache->cachedDiskUsage(m_account, m_opts.folderPath);
279 : 11 : qint64 serverSize = m_cache->totalServerSize(m_account, m_opts.folderPath);
280 : 11 : qint64 avgSize = m_cache->averageMailSize(m_account, m_opts.folderPath);
281 [ + - ]: 11 : m_diskUsageLabel->setText(
282 [ + - + - : 33 : tr("Cache Storage: %1").arg(formatSize(diskUsage)));
+ - ]
283 [ + - ]: 11 : m_serverSizeLabel->setText(
284 [ + - + - : 33 : tr("Server Size: %1").arg(formatSize(serverSize)));
+ - ]
285 [ + - ]: 11 : m_avgSizeLabel->setText(
286 [ + - + - : 33 : tr("Avg. Mail Size: %1").arg(formatSize(avgSize)));
+ - ]
287 : : } else {
288 [ + - + - ]: 1 : m_diskUsageLabel->setText(tr("Cache Storage: n/a"));
289 [ + - + - ]: 1 : m_serverSizeLabel->setText(tr("Server Size: n/a"));
290 [ + - + - ]: 1 : m_avgSizeLabel->setText(tr("Avg. Mail Size: n/a"));
291 : : }
292 : :
293 [ + + ]: 12 : if (m_predictor) {
294 : 11 : int docs = m_predictor->documentsForFolder(m_opts.folderPath);
295 : 11 : int tokens = m_predictor->tokenCountForFolder(m_opts.folderPath);
296 : 11 : bool sufficient = m_predictor->hasSufficientData(m_opts.folderPath);
297 [ + - ]: 11 : m_predictorDocsLabel->setText(
298 [ + - ]: 11 : tr("Training Documents: %1%2")
299 [ + - ]: 22 : .arg(docs)
300 [ - + + - : 22 : .arg(sufficient ? "" : " (unzureichend)"));
+ - ]
301 [ + - ]: 11 : m_predictorTokensLabel->setText(
302 [ + - + - ]: 33 : tr("Learned Tokens: %1").arg(tokens));
303 : : } else {
304 [ + - + - ]: 1 : m_predictorDocsLabel->setText(tr("Training Documents: n/a"));
305 [ + - + - ]: 1 : m_predictorTokensLabel->setText(tr("Learned Tokens: n/a"));
306 : : }
307 : 12 : }
308 : :
309 : : // T-304: Runtime language switching
310 : 23 : void FolderPropertiesDialog::changeEvent(QEvent *event) {
311 [ + + ]: 23 : if (event->type() == QEvent::LanguageChange)
312 : 1 : retranslateUi();
313 : 23 : QDialog::changeEvent(event);
314 : 23 : }
315 : :
316 : 1 : void FolderPropertiesDialog::retranslateUi() {
317 : : // Most labels are created once in setupUi; title is dynamic
318 : : // Nothing to retranslate since labels are inline in layout
319 : 1 : }
|