• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IJHack / QtPass / 24292535626

11 Apr 2026 09:59PM UTC coverage: 20.891% (-0.04%) from 20.927%
24292535626

Pull #977

github

web-flow
Merge 040e66119 into 10a6fc9bd
Pull Request #977: perf: optimize UsersDialog performance

0 of 14 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1111 of 5318 relevant lines covered (20.89%)

7.73 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/usersdialog.cpp
1
// SPDX-FileCopyrightText: 2015 Anne Jan Brouwer
2
// SPDX-License-Identifier: GPL-3.0-or-later
3
#include "usersdialog.h"
4
#include "qtpasssettings.h"
5
#include "ui_usersdialog.h"
6
#include <QApplication>
7
#include <QCloseEvent>
8
#include <QDateTime>
9
#include <QKeyEvent>
10
#include <QMessageBox>
11
#include <QRegularExpression>
12
#include <QSet>
13
#include <QWidget>
14
#include <utility>
15

16
#ifdef QT_DEBUG
17
#include "debughelper.h"
18
#endif
19
/**
20
 * @brief UsersDialog::UsersDialog basic constructor
21
 * @param dir Password directory
22
 * @param parent
23
 */
24
UsersDialog::UsersDialog(const QString &dir, QWidget *parent)
×
25
    : QDialog(parent), ui(new Ui::UsersDialog), m_dir(dir) {
×
26

27
  ui->setupUi(this);
×
28

29
  restoreDialogState();
×
30
  if (!loadGpgKeys()) {
×
31
    connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
×
32
    return;
×
33
  }
34

35
  loadRecipients();
×
36
  populateList();
×
37

38
  connectSignals();
×
39
}
×
40

41
void UsersDialog::connectSignals() {
×
42
  connect(ui->buttonBox, &QDialogButtonBox::accepted, this,
×
43
          &UsersDialog::accept);
×
44
  connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
×
45
  connect(ui->listWidget, &QListWidget::itemChanged, this,
×
46
          &UsersDialog::itemChange);
×
47

48
  ui->lineEdit->setClearButtonEnabled(true);
×
49
}
×
50

51
/**
52
 * @brief Restore dialog geometry from settings.
53
 */
54
void UsersDialog::restoreDialogState() {
×
55
  QByteArray savedGeometry = QtPassSettings::getDialogGeometry("usersDialog");
×
56
  bool hasSavedGeometry = !savedGeometry.isEmpty();
57
  if (hasSavedGeometry) {
×
58
    restoreGeometry(savedGeometry);
×
59
  }
60
  if (QtPassSettings::isDialogMaximized("usersDialog")) {
×
61
    showMaximized();
×
62
  } else if (hasSavedGeometry) {
×
63
    move(QtPassSettings::getDialogPos("usersDialog"));
×
64
    resize(QtPassSettings::getDialogSize("usersDialog"));
×
65
  }
66
}
×
67

68
auto UsersDialog::loadGpgKeys() -> bool {
×
69
  QList<UserInfo> users = QtPassSettings::getPass()->listKeys();
×
70
  if (users.isEmpty()) {
×
71
    QMessageBox::critical(parentWidget(), tr("Keylist missing"),
×
72
                          tr("Could not fetch list of available GPG keys"));
×
73
    reject();
×
74
    return false;
75
  }
76

77
  markSecretKeys(users);
×
78

79
  m_userList = users;
80
  return true;
×
81
}
82

83
void UsersDialog::markSecretKeys(QList<UserInfo> &users) {
×
84
  QList<UserInfo> secret_keys = QtPassSettings::getPass()->listKeys("", true);
×
85
  QSet<QString> secretKeyIds;
86
  for (const UserInfo &sec : secret_keys) {
×
87
    secretKeyIds.insert(sec.key_id);
×
88
  }
89
  for (auto &user : users) {
×
90
    if (secretKeyIds.contains(user.key_id)) {
×
91
      user.have_secret = true;
×
92
    }
93
  }
94
}
×
95

96
void UsersDialog::loadRecipients() {
×
97
  int count = 0;
×
98
  QStringList recipients = QtPassSettings::getPass()->getRecipientString(
×
99
      m_dir.isEmpty() ? "" : m_dir, " ", &count);
×
100

101
  QList<UserInfo> selectedUsers =
102
      QtPassSettings::getPass()->listKeys(recipients);
×
103
  QSet<QString> selectedKeyIds;
104
  for (const UserInfo &sel : selectedUsers) {
×
105
    selectedKeyIds.insert(sel.key_id);
×
106
  }
107
  for (auto &user : m_userList) {
×
108
    if (selectedKeyIds.contains(user.key_id)) {
×
109
      user.enabled = true;
×
110
    }
111
  }
112

113
  if (count > selectedUsers.size()) {
×
114
    QStringList allRecipients = QtPassSettings::getPass()->getRecipientList(
×
115
        m_dir.isEmpty() ? "" : m_dir);
×
116
    QSet<QString> missingKeyRecipients;
117

118
    // Pre-fetch all keys once to avoid N+1 queries
NEW
119
    QList<UserInfo> allKeys = QtPassSettings::getPass()->listKeys();
×
120
    QSet<QString> allKeyIds;
NEW
121
    for (const UserInfo &key : allKeys) {
×
NEW
122
      allKeyIds.insert(key.key_id);
×
123
    }
124

125
    for (const QString &recipient : allRecipients) {
×
NEW
126
      if (!allKeyIds.contains(recipient) &&
×
127
          !missingKeyRecipients.contains(recipient)) {
128
        missingKeyRecipients.insert(recipient);
×
129
        UserInfo i;
×
130
        i.enabled = true;
×
131
        i.key_id = recipient;
×
132
        i.name = " ?? " + tr("Key not found in keyring");
×
133
        m_userList.append(i);
134
      }
×
135
    }
136
  }
137
}
×
138

139
/**
140
 * @brief UsersDialog::~UsersDialog basic destructor.
141
 */
142
UsersDialog::~UsersDialog() { delete ui; }
×
143

144
/**
145
 * @brief UsersDialog::accept
146
 */
147
void UsersDialog::accept() {
×
148
  QtPassSettings::getPass()->Init(m_dir, m_userList);
×
149

150
  QDialog::accept();
×
151
}
×
152

153
/**
154
 * @brief UsersDialog::closeEvent save window state on close.
155
 * @param event
156
 */
157
void UsersDialog::closeEvent(QCloseEvent *event) {
×
158
  QtPassSettings::setDialogGeometry("usersDialog", saveGeometry());
×
159
  if (!isMaximized()) {
×
160
    QtPassSettings::setDialogPos("usersDialog", pos());
×
161
    QtPassSettings::setDialogSize("usersDialog", size());
×
162
  }
163
  QtPassSettings::setDialogMaximized("usersDialog", isMaximized());
×
164
  event->accept();
165
}
×
166

167
/**
168
 * @brief UsersDialog::keyPressEvent clear the lineEdit when escape is pressed.
169
 * No action for Enter currently.
170
 * @param event
171
 */
172
void UsersDialog::keyPressEvent(QKeyEvent *event) {
×
173
  switch (event->key()) {
×
174
  case Qt::Key_Escape:
×
175
    ui->lineEdit->clear();
×
176
    break;
×
177
  default:
178
    break;
179
  }
180
}
×
181

182
/**
183
 * @brief UsersDialog::itemChange update the item information.
184
 * @param item
185
 */
186
void UsersDialog::itemChange(QListWidgetItem *item) {
×
187
  if (!item) {
×
188
    return;
×
189
  }
190
  bool ok = false;
×
191
  const int index = item->data(Qt::UserRole).toInt(&ok);
×
192
  if (!ok) {
×
193
#ifdef QT_DEBUG
194
    qWarning() << "UsersDialog::itemChange: invalid user index data for item";
195
#endif
196
    return;
197
  }
198
  if (index < 0 || index >= m_userList.size()) {
×
199
#ifdef QT_DEBUG
200
    qWarning() << "UsersDialog::itemChange: user index out of range:" << index
201
               << "valid range is [0," << (m_userList.size() - 1) << "]";
202
#endif
203
    return;
204
  }
205
  m_userList[index].enabled = item->checkState() == Qt::Checked;
×
206
}
207

208
/**
209
 * @brief UsersDialog::populateList update the view based on filter options
210
 * (such as searching).
211
 * @param filter
212
 */
213
void UsersDialog::populateList(const QString &filter) {
×
NEW
214
  QString patternString = "*" + filter + "*";
×
NEW
215
  if (m_cachedPatternString != patternString) {
×
216
    QRegularExpression re(
NEW
217
        QRegularExpression::wildcardToRegularExpression(patternString),
×
UNCOV
218
        QRegularExpression::CaseInsensitiveOption);
×
NEW
219
    if (re.isValid()) {
×
NEW
220
      m_cachedNameFilter = re;
×
NEW
221
      m_cachedPatternString = patternString;
×
222
    }
223
  }
×
224
  const QRegularExpression &nameFilter = m_cachedNameFilter;
×
225
  ui->listWidget->clear();
×
226

227
  for (int i = 0; i < m_userList.size(); ++i) {
×
228
    const auto &user = m_userList.at(i);
229
    if (!passesFilter(user, filter, nameFilter)) {
×
230
      continue;
×
231
    }
232

233
    auto *item = new QListWidgetItem(buildUserText(user), ui->listWidget);
×
234
    applyUserStyling(item, user);
×
235
    item->setCheckState(user.enabled ? Qt::Checked : Qt::Unchecked);
×
236
    item->setData(Qt::UserRole, QVariant::fromValue(i));
×
237
    ui->listWidget->addItem(item);
×
238
  }
239
}
×
240

241
/**
242
 * @brief Checks if a user passes the filter criteria.
243
 * @param user User to check
244
 * @param filter Filter string
245
 * @param nameFilter Compiled name filter regex
246
 * @return true if user passes filter
247
 */
248
bool UsersDialog::passesFilter(const UserInfo &user, const QString &filter,
×
249
                               const QRegularExpression &nameFilter) const {
250
  if (!filter.isEmpty() && !nameFilter.match(user.name).hasMatch()) {
×
251
    return false;
252
  }
253
  if (!user.isValid() && !ui->checkBox->isChecked()) {
×
254
    return false;
255
  }
256
  const bool expired = isUserExpired(user);
×
257
  return !(expired && !ui->checkBox->isChecked());
×
258
}
259

260
/**
261
 * @brief Checks if a user's key has expired.
262
 * @param user User to check
263
 * @return true if user's key is expired
264
 */
265
auto UsersDialog::isUserExpired(const UserInfo &user) const -> bool {
×
NEW
266
  if (!m_cachedDateTimeValid) {
×
NEW
267
    m_cachedCurrentDateTime = QDateTime::currentDateTime();
×
NEW
268
    m_cachedDateTimeValid = true;
×
269
  }
270
  return user.expiry.toSecsSinceEpoch() > 0 &&
×
NEW
271
         m_cachedCurrentDateTime > user.expiry;
×
272
}
273

274
/**
275
 * @brief Builds display text for a user.
276
 * @param user User to format
277
 * @return Formatted user text
278
 */
279
QString UsersDialog::buildUserText(const UserInfo &user) const {
×
280
  QString text = user.name + "\n" + user.key_id;
×
281
  if (user.created.toSecsSinceEpoch() > 0) {
×
282
    text += " " + tr("created") + " " +
×
283
            QLocale::system().toString(user.created, QLocale::ShortFormat);
×
284
  }
285
  if (user.expiry.toSecsSinceEpoch() > 0) {
×
286
    text += " " + tr("expires") + " " +
×
287
            QLocale::system().toString(user.expiry, QLocale::ShortFormat);
×
288
  }
289
  return text;
×
290
}
291

292
/**
293
 * @brief Applies visual styling to a user list item based on key status.
294
 * @param item List widget item to style
295
 * @param user User whose status determines styling
296
 */
297
void UsersDialog::applyUserStyling(QListWidgetItem *item,
×
298
                                   const UserInfo &user) const {
299
  const QString originalText = item->text();
×
300
  if (user.have_secret) {
×
301
    const QPalette palette = QApplication::palette();
×
302
    item->setForeground(palette.color(QPalette::Link));
×
303
    QFont font = item->font();
×
304
    font.setBold(true);
305
    item->setFont(font);
×
306
  } else if (!user.isValid()) {
×
307
    item->setBackground(Qt::darkRed);
×
308
    item->setForeground(Qt::white);
×
309
    item->setText(tr("[INVALID] ") + originalText);
×
310
  } else if (isUserExpired(user)) {
×
311
    item->setForeground(Qt::darkRed);
×
312
    item->setText(tr("[EXPIRED] ") + originalText);
×
313
  } else if (!user.fullyValid()) {
×
314
    item->setBackground(Qt::darkYellow);
×
315
    item->setForeground(Qt::white);
×
316
    item->setText(tr("[PARTIAL] ") + originalText);
×
317
  } else {
318
    item->setText(originalText);
×
319
  }
320
}
×
321

322
/**
323
 * @brief UsersDialog::on_lineEdit_textChanged typing in the searchbox.
324
 * @param filter
325
 */
326
void UsersDialog::on_lineEdit_textChanged(const QString &filter) {
×
327
  populateList(filter);
×
328
}
×
329

330
/**
331
 * @brief UsersDialog::on_checkBox_clicked filtering.
332
 */
333
void UsersDialog::on_checkBox_clicked() { populateList(ui->lineEdit->text()); }
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc