keepassxc

Форк
0
/
AutoTypeSelectDialog.cpp 
408 строк · 14.1 Кб
1
/*
2
 *  Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
3
 *  Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
4
 *
5
 *  This program is free software: you can redistribute it and/or modify
6
 *  it under the terms of the GNU General Public License as published by
7
 *  the Free Software Foundation, either version 2 or (at your option)
8
 *  version 3 of the License.
9
 *
10
 *  This program is distributed in the hope that it will be useful,
11
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 *  GNU General Public License for more details.
14
 *
15
 *  You should have received a copy of the GNU General Public License
16
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18

19
#include "AutoTypeSelectDialog.h"
20
#include "ui_AutoTypeSelectDialog.h"
21

22
#include <QCloseEvent>
23
#include <QMenu>
24
#include <QShortcut>
25
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
26
#include <QScreen>
27
#else
28
#include <QDesktopWidget>
29
#endif
30

31
#include "core/Config.h"
32
#include "core/Database.h"
33
#include "core/Entry.h"
34
#include "core/EntrySearcher.h"
35
#include "gui/Clipboard.h"
36
#include "gui/Icons.h"
37

38
const auto MENU_FIELD_PROP_NAME = "menu_field";
39
enum MENU_FIELD
40
{
41
    USERNAME = 1,
42
    PASSWORD,
43
    TOTP,
44
};
45

46
AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
47
    : QDialog(parent)
48
    , m_ui(new Ui::AutoTypeSelectDialog())
49
    , m_lastMatch(nullptr, QString())
50
{
51
    setAttribute(Qt::WA_DeleteOnClose);
52
    // Places the window on the active (virtual) desktop instead of where the main window is.
53
    setAttribute(Qt::WA_X11BypassTransientForHint);
54
    setWindowFlags((windowFlags() | Qt::WindowStaysOnTopHint) & ~Qt::WindowContextHelpButtonHint);
55
    setWindowIcon(icons()->applicationIcon());
56

57
    buildActionMenu();
58

59
    m_ui->setupUi(this);
60

61
    connect(m_ui->view, &AutoTypeMatchView::matchActivated, this, &AutoTypeSelectDialog::submitAutoTypeMatch);
62
    connect(m_ui->view, &AutoTypeMatchView::currentMatchChanged, this, &AutoTypeSelectDialog::updateActionMenu);
63
    connect(m_ui->view, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
64
        if (m_ui->view->currentMatch().first) {
65
            m_actionMenu->popup(m_ui->view->viewport()->mapToGlobal(pos));
66
        }
67
    });
68

69
    m_ui->helpButton->setIcon(icons()->icon("system-help"));
70

71
    m_ui->search->installEventFilter(this);
72

73
    m_searchTimer.setInterval(0);
74
    m_searchTimer.setSingleShot(true);
75

76
    connect(m_ui->search, SIGNAL(textChanged(QString)), &m_searchTimer, SLOT(start()));
77
    connect(m_ui->search, SIGNAL(returnPressed()), SLOT(activateCurrentMatch()));
78
    connect(&m_searchTimer, SIGNAL(timeout()), SLOT(performSearch()));
79

80
    m_ui->searchCheckBox->setShortcut(Qt::CTRL + Qt::Key_F);
81
    connect(m_ui->searchCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
82
        setDelayedSearch(checked);
83
        performSearch();
84
    });
85

86
    m_actionMenu->installEventFilter(this);
87
    m_ui->action->setMenu(m_actionMenu);
88
    m_ui->action->installEventFilter(this);
89
    connect(m_ui->action, &QToolButton::clicked, this, &AutoTypeSelectDialog::activateCurrentMatch);
90

91
    connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
92
}
93

94
// Required for QScopedPointer
95
AutoTypeSelectDialog::~AutoTypeSelectDialog() = default;
96

97
void AutoTypeSelectDialog::setMatches(const QList<AutoTypeMatch>& matches,
98
                                      const QList<QSharedPointer<Database>>& dbs,
99
                                      const AutoTypeMatch& lastMatch)
100
{
101
    m_matches = matches;
102
    m_dbs = dbs;
103
    m_lastMatch = lastMatch;
104
    bool noMatches = m_matches.isEmpty();
105

106
    // disable changing search scope if we have no direct matches
107
    m_ui->searchCheckBox->setDisabled(noMatches);
108

109
    // changing check also performs search so block signals temporarily
110
    bool blockSignals = m_ui->searchCheckBox->blockSignals(true);
111
    m_ui->searchCheckBox->setChecked(noMatches);
112
    m_ui->searchCheckBox->blockSignals(blockSignals);
113

114
    // always perform search when updating matches to refresh view
115
    performSearch();
116
    setDelayedSearch(noMatches);
117
}
118

119
void AutoTypeSelectDialog::setSearchString(const QString& search)
120
{
121
    m_ui->search->setText(search);
122
    m_ui->searchCheckBox->setChecked(true);
123
}
124

125
void AutoTypeSelectDialog::setDelayedSearch(bool state)
126
{
127
    m_searchTimer.setInterval(state ? 150 : 0);
128
}
129

130
void AutoTypeSelectDialog::submitAutoTypeMatch(AutoTypeMatch match)
131
{
132
    if (match.first) {
133
        m_accepted = true;
134
        accept();
135
        emit matchActivated(std::move(match), m_virtualMode);
136
    }
137
}
138

139
void AutoTypeSelectDialog::performSearch()
140
{
141
    if (!m_ui->searchCheckBox->isChecked()) {
142
        m_ui->view->setMatchList(m_matches);
143
        m_ui->view->filterList(m_ui->search->text());
144
    } else {
145
        auto searchText = m_ui->search->text();
146
        // If no search text, find all entries
147
        if (searchText.isEmpty()) {
148
            searchText.append("*");
149
        }
150

151
        EntrySearcher searcher;
152
        QList<AutoTypeMatch> matches;
153
        for (const auto& db : m_dbs) {
154
            auto found = searcher.search(searchText, db->rootGroup());
155
            for (auto* entry : found) {
156
                QSet<QString> sequences;
157
                auto defSequence = entry->effectiveAutoTypeSequence();
158
                if (!defSequence.isEmpty()) {
159
                    matches.append({entry, defSequence});
160
                    sequences << defSequence;
161
                }
162
                for (const auto& assoc : entry->autoTypeAssociations()->getAll()) {
163
                    if (!sequences.contains(assoc.sequence) && !assoc.sequence.isEmpty()) {
164
                        matches.append({entry, assoc.sequence});
165
                        sequences << assoc.sequence;
166
                    }
167
                }
168
            }
169
        }
170

171
        m_ui->view->setMatchList(matches);
172
    }
173

174
    bool selected = false;
175
    if (m_lastMatch.first) {
176
        selected = m_ui->view->selectMatch(m_lastMatch);
177
    }
178

179
    if (!selected && !m_ui->search->text().isEmpty()) {
180
        m_ui->view->selectFirstMatch();
181
    }
182

183
    m_ui->search->setFocus();
184
}
185

186
void AutoTypeSelectDialog::activateCurrentMatch()
187
{
188
    submitAutoTypeMatch(m_ui->view->currentMatch());
189
}
190

191
bool AutoTypeSelectDialog::eventFilter(QObject* obj, QEvent* event)
192
{
193
    if (obj == m_ui->action) {
194
        if (event->type() == QEvent::FocusIn) {
195
            m_ui->action->showMenu();
196
            return true;
197
        } else if (event->type() == QEvent::KeyPress && static_cast<QKeyEvent*>(event)->key() == Qt::Key_Return) {
198
            // handle case where the menu is closed but the button has focus
199
            activateCurrentMatch();
200
            return true;
201
        }
202
    } else if (obj == m_actionMenu) {
203
        if (event->type() == QEvent::KeyPress) {
204
            auto* keyEvent = static_cast<QKeyEvent*>(event);
205
            switch (keyEvent->key()) {
206
            case Qt::Key_Tab:
207
                m_actionMenu->close();
208
                focusNextPrevChild(true);
209
                return true;
210
            case Qt::Key_Backtab:
211
                m_actionMenu->close();
212
                focusNextPrevChild(false);
213
                return true;
214
            case Qt::Key_Return:
215
                // accept the dialog with default sequence if no action selected
216
                if (!m_actionMenu->activeAction()) {
217
                    activateCurrentMatch();
218
                    return true;
219
                }
220
            default:
221
                break;
222
            }
223
        }
224
    } else if (obj == m_ui->search) {
225
        if (event->type() == QEvent::KeyPress) {
226
            auto* keyEvent = static_cast<QKeyEvent*>(event);
227
            switch (keyEvent->key()) {
228
            case Qt::Key_Up:
229
                m_ui->view->moveSelection(-1);
230
                return true;
231
            case Qt::Key_Down:
232
                m_ui->view->moveSelection(1);
233
                return true;
234
            case Qt::Key_PageUp:
235
                m_ui->view->moveSelection(-5);
236
                return true;
237
            case Qt::Key_PageDown:
238
                m_ui->view->moveSelection(5);
239
                return true;
240
            case Qt::Key_Escape:
241
                if (m_ui->search->text().isEmpty()) {
242
                    reject();
243
                } else {
244
                    m_ui->search->clear();
245
                }
246
                return true;
247
            default:
248
                break;
249
            }
250
        }
251
    }
252

253
    return QDialog::eventFilter(obj, event);
254
}
255

256
void AutoTypeSelectDialog::updateActionMenu(const AutoTypeMatch& match)
257
{
258
    if (!match.first) {
259
        m_ui->action->setEnabled(false);
260
        return;
261
    }
262

263
    m_ui->action->setEnabled(true);
264

265
    bool hasUsername = !match.first->username().isEmpty();
266
    bool hasPassword = !match.first->password().isEmpty();
267
    bool hasTotp = match.first->hasTotp();
268

269
    for (auto action : m_actionMenu->actions()) {
270
        auto prop = action->property(MENU_FIELD_PROP_NAME);
271
        if (prop.isValid()) {
272
            switch (prop.toInt()) {
273
            case MENU_FIELD::USERNAME:
274
                action->setEnabled(hasUsername);
275
                break;
276
            case MENU_FIELD::PASSWORD:
277
                action->setEnabled(hasPassword);
278
                break;
279
            case MENU_FIELD::TOTP:
280
                action->setEnabled(hasTotp);
281
                break;
282
            }
283
        }
284
    }
285
}
286

287
void AutoTypeSelectDialog::buildActionMenu()
288
{
289
    m_actionMenu = new QMenu(this);
290
    auto typeUsernameAction = new QAction(icons()->icon("auto-type"), tr("Type {USERNAME}"), this);
291
    auto typePasswordAction = new QAction(icons()->icon("auto-type"), tr("Type {PASSWORD}"), this);
292
    auto typeTotpAction = new QAction(icons()->icon("auto-type"), tr("Type {TOTP}"), this);
293
    auto copyUsernameAction = new QAction(icons()->icon("username-copy"), tr("Copy Username"), this);
294
    auto copyPasswordAction = new QAction(icons()->icon("password-copy"), tr("Copy Password"), this);
295
    auto copyTotpAction = new QAction(icons()->icon("totp"), tr("Copy TOTP"), this);
296
    m_actionMenu->addAction(typeUsernameAction);
297
    m_actionMenu->addAction(typePasswordAction);
298
    m_actionMenu->addAction(typeTotpAction);
299
    m_actionMenu->addAction(copyUsernameAction);
300
    m_actionMenu->addAction(copyPasswordAction);
301
    m_actionMenu->addAction(copyTotpAction);
302

303
    typeUsernameAction->setShortcut(Qt::CTRL + Qt::Key_1);
304
    typeUsernameAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::USERNAME);
305
    connect(typeUsernameAction, &QAction::triggered, this, [&] {
306
        auto match = m_ui->view->currentMatch();
307
        match.second = "{USERNAME}";
308
        submitAutoTypeMatch(match);
309
    });
310

311
    typePasswordAction->setShortcut(Qt::CTRL + Qt::Key_2);
312
    typePasswordAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::PASSWORD);
313
    connect(typePasswordAction, &QAction::triggered, this, [&] {
314
        auto match = m_ui->view->currentMatch();
315
        match.second = "{PASSWORD}";
316
        submitAutoTypeMatch(match);
317
    });
318

319
    typeTotpAction->setShortcut(Qt::CTRL + Qt::Key_3);
320
    typeTotpAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::TOTP);
321
    connect(typeTotpAction, &QAction::triggered, this, [&] {
322
        auto match = m_ui->view->currentMatch();
323
        match.second = "{TOTP}";
324
        submitAutoTypeMatch(match);
325
    });
326

327
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
328
    auto typeVirtualAction = new QAction(icons()->icon("auto-type"), tr("Use Virtual Keyboard"), nullptr);
329
    m_actionMenu->insertAction(copyUsernameAction, typeVirtualAction);
330
    typeVirtualAction->setShortcut(Qt::CTRL + Qt::Key_4);
331
    connect(typeVirtualAction, &QAction::triggered, this, [&] {
332
        m_virtualMode = true;
333
        activateCurrentMatch();
334
    });
335
#endif
336

337
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
338
    // Qt 5.10 introduced a new "feature" to hide shortcuts in context menus
339
    // Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them
340
    typeUsernameAction->setShortcutVisibleInContextMenu(true);
341
    typePasswordAction->setShortcutVisibleInContextMenu(true);
342
    typeTotpAction->setShortcutVisibleInContextMenu(true);
343
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
344
    typeVirtualAction->setShortcutVisibleInContextMenu(true);
345
#endif
346
#endif
347

348
    copyUsernameAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::USERNAME);
349
    connect(copyUsernameAction, &QAction::triggered, this, [&] {
350
        auto entry = m_ui->view->currentMatch().first;
351
        if (entry) {
352
            clipboard()->setText(entry->resolvePlaceholder(entry->username()));
353
            reject();
354
        }
355
    });
356

357
    copyPasswordAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::PASSWORD);
358
    connect(copyPasswordAction, &QAction::triggered, this, [&] {
359
        auto entry = m_ui->view->currentMatch().first;
360
        if (entry) {
361
            clipboard()->setText(entry->resolvePlaceholder(entry->password()));
362
            reject();
363
        }
364
    });
365

366
    copyTotpAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::TOTP);
367
    connect(copyTotpAction, &QAction::triggered, this, [&] {
368
        auto entry = m_ui->view->currentMatch().first;
369
        if (entry) {
370
            clipboard()->setText(entry->totp());
371
            reject();
372
        }
373
    });
374
}
375

376
void AutoTypeSelectDialog::showEvent(QShowEvent* event)
377
{
378
    QDialog::showEvent(event);
379

380
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
381
    auto screen = QApplication::screenAt(QCursor::pos());
382
    if (!screen) {
383
        // screenAt can return a nullptr, default to the primary screen
384
        screen = QApplication::primaryScreen();
385
    }
386
    QRect screenGeometry = screen->availableGeometry();
387
#else
388
    QRect screenGeometry = QApplication::desktop()->availableGeometry(QCursor::pos());
389
#endif
390

391
    // Resize to last used size
392
    QSize size = config()->get(Config::GUI_AutoTypeSelectDialogSize).toSize();
393
    size.setWidth(qMin(size.width(), screenGeometry.width()));
394
    size.setHeight(qMin(size.height(), screenGeometry.height()));
395
    resize(size);
396

397
    // move dialog to the center of the screen
398
    move(screenGeometry.center().x() - (size.width() / 2), screenGeometry.center().y() - (size.height() / 2));
399
}
400

401
void AutoTypeSelectDialog::hideEvent(QHideEvent* event)
402
{
403
    config()->set(Config::GUI_AutoTypeSelectDialogSize, size());
404
    if (!m_accepted) {
405
        emit rejected();
406
    }
407
    QDialog::hideEvent(event);
408
}
409

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.