diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index d28f737148..a7531537ea 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -193,8 +193,9 @@ DatabaseWidget::DatabaseWidget(QSharedPointer 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 entries, bool permanent) } } -void DatabaseWidget::setFocus() +void DatabaseWidget::setFocus(Qt::FocusReason reason) +{ + if (reason == Qt::BacktabFocusReason) { + m_previewView->setFocus(); + } else { + m_groupView->setFocus(); + } +} + +void DatabaseWidget::focusOnEntries() { - m_entryView->setFocus(); + 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()) { diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index d54c63439b..0d0afe8f15 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -76,12 +76,15 @@ class DatabaseWidget : public QStackedWidget explicit DatabaseWidget(const QString& filePath, QWidget* parent = nullptr); ~DatabaseWidget(); + void setFocus(Qt::FocusReason reason); + QSharedPointer 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 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); diff --git a/src/gui/EntryPreviewWidget.cpp b/src/gui/EntryPreviewWidget.cpp index 0a46417e00..06152c5541 100644 --- a/src/gui/EntryPreviewWidget.cpp +++ b/src/gui/EntryPreviewWidget.cpp @@ -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 diff --git a/src/gui/EntryPreviewWidget.ui b/src/gui/EntryPreviewWidget.ui index 4ac3702d5b..8c68ab9a05 100644 --- a/src/gui/EntryPreviewWidget.ui +++ b/src/gui/EntryPreviewWidget.ui @@ -7,7 +7,7 @@ 0 0 566 - 169 + 206 @@ -106,9 +106,6 @@ - - Qt::TabFocus - Display current TOTP value @@ -122,9 +119,6 @@ - - Qt::TabFocus - Close @@ -137,9 +131,6 @@ - - Qt::ClickFocus - 0 @@ -1147,12 +1138,13 @@ entryCloseButton - entryTotpButton + entryTabWidget togglePasswordButton toggleEntryNotesButton - entryAutotypeTree groupCloseButton groupTabWidget + toggleGroupNotesButton + entryTotpButton diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 3d129bb94d..0a0118eda1 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -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()) { diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 81d8212af6..9872145a70 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -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); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 9bf0eae326..c40191d6bf 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -79,9 +79,6 @@ 0 - - Qt::TabFocus - 2 @@ -129,11 +126,7 @@ 0 - - - Qt::TabFocus - - + @@ -158,11 +151,7 @@ - - - Qt::TabFocus - - + @@ -209,11 +198,7 @@ - - - Qt::TabFocus - - + @@ -240,12 +225,9 @@ 0 0 800 - 24 + 22 - - Qt::NoFocus - Qt::PreventContextMenu @@ -383,9 +365,6 @@ - - Qt::NoFocus - Qt::PreventContextMenu diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 8425ab2d01..1c7b683c10 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -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()); diff --git a/src/gui/group/GroupView.cpp b/src/gui/group/GroupView.cpp index 48945085b3..056015ca8d 100644 --- a/src/gui/group/GroupView.cpp +++ b/src/gui/group/GroupView.cpp @@ -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++) { diff --git a/src/gui/group/GroupView.h b/src/gui/group/GroupView.h index 00b5a28c0b..aa4fd85de9 100644 --- a/src/gui/group/GroupView.h +++ b/src/gui/group/GroupView.h @@ -38,11 +38,11 @@ class GroupView : public QTreeView 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(); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 7ce8a0f4a1..b1513cadfa 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -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);