mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-02-23 07:59:54 -05:00
Fix changing focus around the main window using tab
* Override Qt's default [broken] behavior of handling Tab/Shift+Tab to navigate around the MainWindow. Completely fixes trapped focus. * Improve handling of search results when navigating the UI. * Fix selecting first entry after ending a search. * Add keyboard shortcuts to directly focus on search (F1), Group List (F2), and Entry List (F3) * Fixes #2878, #4636, and #4221
This commit is contained in:
parent
5142981018
commit
49487f9d4a
@ -193,8 +193,9 @@ DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
||||
connect(m_previewView, SIGNAL(errorOccurred(QString)), SLOT(showErrorMessage(QString)));
|
||||
connect(m_previewView, SIGNAL(entryUrlActivated(Entry*)), SLOT(openUrlForEntry(Entry*)));
|
||||
connect(m_entryView, SIGNAL(viewStateChanged()), SIGNAL(entryViewStateChanged()));
|
||||
connect(m_groupView, SIGNAL(groupSelectionChanged(Group*)), SLOT(onGroupChanged(Group*)));
|
||||
connect(m_groupView, SIGNAL(groupSelectionChanged(Group*)), SIGNAL(groupChanged()));
|
||||
connect(m_groupView, SIGNAL(groupSelectionChanged()), SLOT(onGroupChanged()));
|
||||
connect(m_groupView, SIGNAL(groupSelectionChanged()), SIGNAL(groupChanged()));
|
||||
connect(m_groupView, &GroupView::groupFocused, this, [this] { m_previewView->setGroup(currentGroup()); });
|
||||
connect(m_entryView, SIGNAL(entryActivated(Entry*,EntryModel::ModelColumn)),
|
||||
SLOT(entryActivationSignalReceived(Entry*,EntryModel::ModelColumn)));
|
||||
connect(m_entryView, SIGNAL(entrySelectionChanged(Entry*)), SLOT(onEntryChanged(Entry*)));
|
||||
@ -283,6 +284,11 @@ bool DatabaseWidget::isSearchActive() const
|
||||
return m_entryView->inSearchMode();
|
||||
}
|
||||
|
||||
bool DatabaseWidget::isEntryViewActive() const
|
||||
{
|
||||
return currentWidget() == m_mainWidget;
|
||||
}
|
||||
|
||||
bool DatabaseWidget::isEntryEditActive() const
|
||||
{
|
||||
return currentWidget() == m_editEntryWidget;
|
||||
@ -616,9 +622,27 @@ bool DatabaseWidget::confirmDeleteEntries(QList<Entry*> entries, bool permanent)
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseWidget::setFocus()
|
||||
void DatabaseWidget::setFocus(Qt::FocusReason reason)
|
||||
{
|
||||
m_entryView->setFocus();
|
||||
if (reason == Qt::BacktabFocusReason) {
|
||||
m_previewView->setFocus();
|
||||
} else {
|
||||
m_groupView->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseWidget::focusOnEntries()
|
||||
{
|
||||
if (isEntryViewActive()) {
|
||||
m_entryView->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseWidget::focusOnGroups()
|
||||
{
|
||||
if (isEntryViewActive()) {
|
||||
m_groupView->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
void DatabaseWidget::copyTitle()
|
||||
@ -925,6 +949,8 @@ int DatabaseWidget::addChildWidget(QWidget* w)
|
||||
|
||||
void DatabaseWidget::switchToMainView(bool previousDialogAccepted)
|
||||
{
|
||||
setCurrentWidget(m_mainWidget);
|
||||
|
||||
if (m_newGroup) {
|
||||
if (previousDialogAccepted) {
|
||||
m_newGroup->setParent(m_newParent);
|
||||
@ -950,12 +976,10 @@ void DatabaseWidget::switchToMainView(bool previousDialogAccepted)
|
||||
m_entryView->setFocus();
|
||||
}
|
||||
|
||||
setCurrentWidget(m_mainWidget);
|
||||
|
||||
if (sender() == m_entryView || sender() == m_editEntryWidget) {
|
||||
onEntryChanged(m_entryView->currentEntry());
|
||||
} else if (sender() == m_groupView || sender() == m_editGroupWidget) {
|
||||
onGroupChanged(m_groupView->currentGroup());
|
||||
onGroupChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1325,8 +1349,10 @@ void DatabaseWidget::setSearchLimitGroup(bool state)
|
||||
refreshSearch();
|
||||
}
|
||||
|
||||
void DatabaseWidget::onGroupChanged(Group* group)
|
||||
void DatabaseWidget::onGroupChanged()
|
||||
{
|
||||
auto group = m_groupView->currentGroup();
|
||||
|
||||
// Intercept group changes if in search mode
|
||||
if (isSearchActive() && m_searchLimitGroup) {
|
||||
search(m_lastSearchText);
|
||||
@ -1367,13 +1393,11 @@ QString DatabaseWidget::getCurrentSearch()
|
||||
void DatabaseWidget::endSearch()
|
||||
{
|
||||
if (isSearchActive()) {
|
||||
emit listModeAboutToActivate();
|
||||
|
||||
// Show the normal entry view of the current group
|
||||
emit listModeAboutToActivate();
|
||||
m_entryView->displayGroup(currentGroup());
|
||||
onGroupChanged(currentGroup());
|
||||
|
||||
emit listModeActivated();
|
||||
m_entryView->setFirstEntryActive();
|
||||
}
|
||||
|
||||
m_searchingLabel->setVisible(false);
|
||||
@ -1434,6 +1458,31 @@ void DatabaseWidget::showEvent(QShowEvent* event)
|
||||
event->accept();
|
||||
}
|
||||
|
||||
bool DatabaseWidget::focusNextPrevChild(bool next)
|
||||
{
|
||||
// [parent] <-> GroupView <-> EntryView <-> EntryPreview <-> [parent]
|
||||
if (next) {
|
||||
if (m_groupView->hasFocus()) {
|
||||
m_entryView->setFocus();
|
||||
return true;
|
||||
} else if (m_entryView->hasFocus()) {
|
||||
m_previewView->setFocus();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (m_previewView->hasFocus()) {
|
||||
m_entryView->setFocus();
|
||||
return true;
|
||||
} else if (m_entryView->hasFocus()) {
|
||||
m_groupView->setFocus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Defer to the parent widget to make a decision
|
||||
return QStackedWidget::focusNextPrevChild(next);
|
||||
}
|
||||
|
||||
bool DatabaseWidget::lock()
|
||||
{
|
||||
if (isLocked()) {
|
||||
|
@ -76,12 +76,15 @@ public:
|
||||
explicit DatabaseWidget(const QString& filePath, QWidget* parent = nullptr);
|
||||
~DatabaseWidget();
|
||||
|
||||
void setFocus(Qt::FocusReason reason);
|
||||
|
||||
QSharedPointer<Database> database() const;
|
||||
|
||||
DatabaseWidget::Mode currentMode() const;
|
||||
bool isLocked() const;
|
||||
bool isSaving() const;
|
||||
bool isSearchActive() const;
|
||||
bool isEntryViewActive() const;
|
||||
bool isEntryEditActive() const;
|
||||
bool isGroupEditActive() const;
|
||||
|
||||
@ -161,7 +164,8 @@ public slots:
|
||||
void cloneEntry();
|
||||
void deleteSelectedEntries();
|
||||
void deleteEntries(QList<Entry*> entries);
|
||||
void setFocus();
|
||||
void focusOnEntries();
|
||||
void focusOnGroups();
|
||||
void copyTitle();
|
||||
void copyUsername();
|
||||
void copyPassword();
|
||||
@ -217,6 +221,7 @@ public slots:
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
void showEvent(QShowEvent* event) override;
|
||||
bool focusNextPrevChild(bool next) override;
|
||||
|
||||
private slots:
|
||||
void entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column);
|
||||
@ -228,7 +233,7 @@ private slots:
|
||||
void emitGroupContextMenuRequested(const QPoint& pos);
|
||||
void emitEntryContextMenuRequested(const QPoint& pos);
|
||||
void onEntryChanged(Entry* entry);
|
||||
void onGroupChanged(Group* group);
|
||||
void onGroupChanged();
|
||||
void onDatabaseModified();
|
||||
void connectDatabaseSignals();
|
||||
void loadDatabase(bool accepted);
|
||||
|
@ -82,6 +82,8 @@ EntryPreviewWidget::EntryPreviewWidget(QWidget* parent)
|
||||
connect(m_ui->groupCloseButton, SIGNAL(clicked()), SLOT(hide()));
|
||||
connect(m_ui->groupTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection);
|
||||
|
||||
setFocusProxy(m_ui->entryTabWidget);
|
||||
|
||||
#if !defined(WITH_XC_KEESHARE)
|
||||
removeTab(m_ui->groupTabWidget, m_ui->groupShareTab);
|
||||
#endif
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>566</width>
|
||||
<height>169</height>
|
||||
<height>206</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
@ -106,9 +106,6 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="entryTotpButton">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::TabFocus</enum>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Display current TOTP value</string>
|
||||
</property>
|
||||
@ -122,9 +119,6 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QToolButton" name="entryCloseButton">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::TabFocus</enum>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Close</string>
|
||||
</property>
|
||||
@ -137,9 +131,6 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTabWidget" name="entryTabWidget">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::ClickFocus</enum>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
@ -1147,12 +1138,13 @@
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>entryCloseButton</tabstop>
|
||||
<tabstop>entryTotpButton</tabstop>
|
||||
<tabstop>entryTabWidget</tabstop>
|
||||
<tabstop>togglePasswordButton</tabstop>
|
||||
<tabstop>toggleEntryNotesButton</tabstop>
|
||||
<tabstop>entryAutotypeTree</tabstop>
|
||||
<tabstop>groupCloseButton</tabstop>
|
||||
<tabstop>groupTabWidget</tabstop>
|
||||
<tabstop>toggleGroupNotesButton</tabstop>
|
||||
<tabstop>entryTotpButton</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
|
@ -308,6 +308,14 @@ MainWindow::MainWindow()
|
||||
shortcut = new QShortcut(dbTabModifier + Qt::Key_9, this);
|
||||
connect(shortcut, &QShortcut::activated, [this]() { selectDatabaseTab(m_ui->tabWidget->count() - 1); });
|
||||
|
||||
// Allow for direct focus of search, group view, and entry view
|
||||
shortcut = new QShortcut(Qt::Key_F1, this);
|
||||
connect(shortcut, SIGNAL(activated()), m_searchWidget, SLOT(searchFocus()));
|
||||
shortcut = new QShortcut(Qt::Key_F2, this);
|
||||
m_actionMultiplexer.connect(shortcut, SIGNAL(activated()), SLOT(focusOnGroups()));
|
||||
shortcut = new QShortcut(Qt::Key_F3, this);
|
||||
m_actionMultiplexer.connect(shortcut, SIGNAL(activated()), SLOT(focusOnEntries()));
|
||||
|
||||
// Toggle password and username visibility in entry view
|
||||
new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_C, this, SLOT(togglePasswordsHidden()));
|
||||
new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_B, this, SLOT(toggleUsernamesHidden()));
|
||||
@ -1104,6 +1112,36 @@ void MainWindow::changeEvent(QEvent* event)
|
||||
}
|
||||
}
|
||||
|
||||
bool MainWindow::focusNextPrevChild(bool next)
|
||||
{
|
||||
// Only navigate around the main window if the database widget is showing the entry view
|
||||
auto dbWidget = m_ui->tabWidget->currentDatabaseWidget();
|
||||
if (dbWidget && dbWidget->isVisible() && dbWidget->isEntryViewActive()) {
|
||||
// Search Widget <-> Tab Widget <-> DbWidget
|
||||
if (next) {
|
||||
if (m_searchWidget->hasFocus()) {
|
||||
m_ui->tabWidget->setFocus(Qt::TabFocusReason);
|
||||
} else if (m_ui->tabWidget->hasFocus()) {
|
||||
dbWidget->setFocus(Qt::TabFocusReason);
|
||||
} else {
|
||||
m_searchWidget->setFocus(Qt::TabFocusReason);
|
||||
}
|
||||
} else {
|
||||
if (m_searchWidget->hasFocus()) {
|
||||
dbWidget->setFocus(Qt::BacktabFocusReason);
|
||||
} else if (m_ui->tabWidget->hasFocus()) {
|
||||
m_searchWidget->setFocus(Qt::BacktabFocusReason);
|
||||
} else {
|
||||
m_ui->tabWidget->setFocus(Qt::BacktabFocusReason);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Defer to Qt to make a decision, this maintains normal behavior
|
||||
return QMainWindow::focusNextPrevChild(next);
|
||||
}
|
||||
|
||||
void MainWindow::saveWindowInformation()
|
||||
{
|
||||
if (isVisible()) {
|
||||
|
@ -85,6 +85,7 @@ public slots:
|
||||
protected:
|
||||
void closeEvent(QCloseEvent* event) override;
|
||||
void changeEvent(QEvent* event) override;
|
||||
bool focusNextPrevChild(bool next) override;
|
||||
|
||||
private slots:
|
||||
void setMenuActionState(DatabaseWidget::Mode mode = DatabaseWidget::Mode::None);
|
||||
|
@ -79,9 +79,6 @@
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::TabFocus</enum>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>2</number>
|
||||
</property>
|
||||
@ -129,11 +126,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="ApplicationSettingsWidget" name="settingsWidget" native="true">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::TabFocus</enum>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="ApplicationSettingsWidget" name="settingsWidget" native="true"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
@ -158,11 +151,7 @@
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="WelcomeWidget" name="welcomeWidget" native="true">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::TabFocus</enum>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="WelcomeWidget" name="welcomeWidget" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
@ -209,11 +198,7 @@
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="PasswordGeneratorWidget" name="passwordGeneratorWidget" native="true">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::TabFocus</enum>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="PasswordGeneratorWidget" name="passwordGeneratorWidget" native="true"/>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
@ -240,12 +225,9 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>24</height>
|
||||
<height>22</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::NoFocus</enum>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::PreventContextMenu</enum>
|
||||
</property>
|
||||
@ -383,9 +365,6 @@
|
||||
<addaction name="menuHelp"/>
|
||||
</widget>
|
||||
<widget class="QToolBar" name="toolBar">
|
||||
<property name="focusPolicy">
|
||||
<enum>Qt::NoFocus</enum>
|
||||
</property>
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::PreventContextMenu</enum>
|
||||
</property>
|
||||
|
@ -137,10 +137,11 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx)
|
||||
mx.connect(this, SIGNAL(caseSensitiveChanged(bool)), SLOT(setSearchCaseSensitive(bool)));
|
||||
mx.connect(this, SIGNAL(limitGroupChanged(bool)), SLOT(setSearchLimitGroup(bool)));
|
||||
mx.connect(this, SIGNAL(copyPressed()), SLOT(copyPassword()));
|
||||
mx.connect(this, SIGNAL(downPressed()), SLOT(setFocus()));
|
||||
mx.connect(this, SIGNAL(downPressed()), SLOT(focusOnEntries()));
|
||||
mx.connect(SIGNAL(clearSearch()), m_ui->searchEdit, SLOT(clear()));
|
||||
mx.connect(SIGNAL(entrySelectionChanged()), this, SLOT(resetSearchClearTimer()));
|
||||
mx.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(resetSearchClearTimer()));
|
||||
mx.connect(SIGNAL(databaseUnlocked()), this, SLOT(searchFocus()));
|
||||
mx.connect(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(switchToEntryEdit()));
|
||||
}
|
||||
|
||||
@ -149,8 +150,6 @@ void SearchWidget::databaseChanged(DatabaseWidget* dbWidget)
|
||||
if (dbWidget != nullptr) {
|
||||
// Set current search text from this database
|
||||
m_ui->searchEdit->setText(dbWidget->getCurrentSearch());
|
||||
// Keyboard focus on search widget at database unlocking
|
||||
connect(dbWidget, SIGNAL(databaseUnlocked()), this, SLOT(searchFocus()));
|
||||
// Enforce search policy
|
||||
emit caseSensitiveChanged(m_actionCaseSensitive->isChecked());
|
||||
emit limitGroupChanged(m_actionLimitGroup->isChecked());
|
||||
|
@ -38,9 +38,10 @@ GroupView::GroupView(Database* db, QWidget* parent)
|
||||
// clang-format off
|
||||
connect(this, SIGNAL(expanded(QModelIndex)), SLOT(expandedChanged(QModelIndex)));
|
||||
connect(this, SIGNAL(collapsed(QModelIndex)), SLOT(expandedChanged(QModelIndex)));
|
||||
connect(this, SIGNAL(clicked(QModelIndex)), SIGNAL(groupSelectionChanged()));
|
||||
connect(m_model, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(syncExpandedState(QModelIndex,int,int)));
|
||||
connect(m_model, SIGNAL(modelReset()), SLOT(modelReset()));
|
||||
connect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(emitGroupChanged()));
|
||||
connect(selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SIGNAL(groupSelectionChanged()));
|
||||
// clang-format on
|
||||
|
||||
new QShortcut(Qt::CTRL + Qt::Key_F10, this, SLOT(contextMenuShortcutPressed()), nullptr, Qt::WidgetShortcut);
|
||||
@ -85,7 +86,7 @@ void GroupView::dragMoveEvent(QDragMoveEvent* event)
|
||||
|
||||
void GroupView::focusInEvent(QFocusEvent* event)
|
||||
{
|
||||
emitGroupChanged();
|
||||
emit groupFocused();
|
||||
QTreeView::focusInEvent(event);
|
||||
}
|
||||
|
||||
@ -140,11 +141,6 @@ void GroupView::setModel(QAbstractItemModel* model)
|
||||
Q_ASSERT(false);
|
||||
}
|
||||
|
||||
void GroupView::emitGroupChanged()
|
||||
{
|
||||
emit groupSelectionChanged(currentGroup());
|
||||
}
|
||||
|
||||
void GroupView::syncExpandedState(const QModelIndex& parent, int start, int end)
|
||||
{
|
||||
for (int row = start; row <= end; row++) {
|
||||
|
@ -38,11 +38,11 @@ public:
|
||||
void sortGroups(bool reverse = false);
|
||||
|
||||
signals:
|
||||
void groupSelectionChanged(Group* group);
|
||||
void groupSelectionChanged();
|
||||
void groupFocused();
|
||||
|
||||
private slots:
|
||||
void expandedChanged(const QModelIndex& index);
|
||||
void emitGroupChanged();
|
||||
void syncExpandedState(const QModelIndex& parent, int start, int end);
|
||||
void modelReset();
|
||||
void contextMenuShortcutPressed();
|
||||
|
@ -901,8 +901,8 @@ void TestGui::testSearch()
|
||||
QTest::keyClick(searchTextEdit, Qt::Key_Down);
|
||||
QTRY_VERIFY(entryView->hasFocus());
|
||||
auto* searchedEntry = entryView->currentEntry();
|
||||
// Restore focus and search text selection
|
||||
QTest::keyClick(m_mainWindow.data(), Qt::Key_F, Qt::ControlModifier);
|
||||
// Restore focus using F1 key and search text selection
|
||||
QTest::keyClick(m_mainWindow.data(), Qt::Key_F1);
|
||||
QTRY_COMPARE(searchTextEdit->selectedText(), QString("someTHING"));
|
||||
QTRY_VERIFY(searchTextEdit->hasFocus());
|
||||
|
||||
@ -965,12 +965,14 @@ void TestGui::testSearch()
|
||||
searchWidget->setLimitGroup(false);
|
||||
clickIndex(rootGroupIndex, groupView, Qt::LeftButton);
|
||||
QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
|
||||
QVERIFY(!m_dbWidget->isSearchActive());
|
||||
|
||||
// Try to edit the first entry from the search view
|
||||
// Refocus back to search edit
|
||||
QTest::mouseClick(searchTextEdit, Qt::LeftButton);
|
||||
QTRY_VERIFY(searchTextEdit->hasFocus());
|
||||
QVERIFY(m_dbWidget->isSearchActive());
|
||||
QTest::keyClicks(searchTextEdit, "someTHING");
|
||||
QTRY_VERIFY(m_dbWidget->isSearchActive());
|
||||
|
||||
QModelIndex item = entryView->model()->index(0, 1);
|
||||
Entry* entry = entryView->entryFromIndex(item);
|
||||
|
Loading…
x
Reference in New Issue
Block a user