diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts
index 97aab6cc1..280feae52 100644
--- a/share/translations/keepassxc_en.ts
+++ b/share/translations/keepassxc_en.ts
@@ -577,6 +577,10 @@
Skip confirmation for main window Auto-Type actions
+
+ Show subgroup entries in entry list view
+
+
Auto-generate password for new entries
diff --git a/src/core/Config.cpp b/src/core/Config.cpp
index 0a0815661..2b479f27c 100644
--- a/src/core/Config.cpp
+++ b/src/core/Config.cpp
@@ -120,6 +120,7 @@ static const QHash configStrings = {
{Config::GUI_CheckForUpdatesIncludeBetas, {QS("GUI/CheckForUpdatesIncludeBetas"), Roaming, false}},
{Config::GUI_ShowExpiredEntriesOnDatabaseUnlock, {QS("GUI/ShowExpiredEntriesOnDatabaseUnlock"), Roaming, true}},
{Config::GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays, {QS("GUI/ShowExpiredEntriesOnDatabaseUnlockOffsetDays"), Roaming, 3}},
+ {Config::GUI_ShowSubgroupEntries, {QS("GUI/ShowSubgroupEntries"), Roaming, false}},
{Config::GUI_FontSizeOffset, {QS("GUI/FontSizeOffset"), Local, 0}},
{Config::GUI_MainWindowGeometry, {QS("GUI/MainWindowGeometry"), Local, {}}},
diff --git a/src/core/Config.h b/src/core/Config.h
index f08ea72fd..6e3bf8b83 100644
--- a/src/core/Config.h
+++ b/src/core/Config.h
@@ -102,6 +102,7 @@ public:
SearchWaitForEnter,
GUI_ShowExpiredEntriesOnDatabaseUnlock,
GUI_ShowExpiredEntriesOnDatabaseUnlockOffsetDays,
+ GUI_ShowSubgroupEntries,
GUI_FontSizeOffset,
GUI_MainWindowGeometry,
diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp
index cb2ee3af1..5e2d30665 100644
--- a/src/gui/ApplicationSettingsWidget.cpp
+++ b/src/gui/ApplicationSettingsWidget.cpp
@@ -218,6 +218,7 @@ void ApplicationSettingsWidget::loadSettings()
m_generalUi->dropToBackgroundOnCopyRadioButton->setChecked(config()->get(Config::DropToBackgroundOnCopy).toBool());
m_generalUi->useGroupIconOnEntryCreationCheckBox->setChecked(
config()->get(Config::UseGroupIconOnEntryCreation).toBool());
+ m_generalUi->showSubgroupEntriesCheckBox->setChecked(config()->get(Config::GUI_ShowSubgroupEntries).toBool());
m_generalUi->autoTypeEntryTitleMatchCheckBox->setChecked(config()->get(Config::AutoTypeEntryTitleMatch).toBool());
m_generalUi->autoTypeEntryURLMatchCheckBox->setChecked(config()->get(Config::AutoTypeEntryURLMatch).toBool());
m_generalUi->autoTypeHideExpiredEntryCheckBox->setChecked(config()->get(Config::AutoTypeHideExpiredEntry).toBool());
@@ -394,6 +395,7 @@ void ApplicationSettingsWidget::saveSettings()
config()->set(Config::MinimizeOnCopy, m_generalUi->minimizeOnCopyRadioButton->isChecked());
config()->set(Config::DropToBackgroundOnCopy, m_generalUi->dropToBackgroundOnCopyRadioButton->isChecked());
config()->set(Config::UseGroupIconOnEntryCreation, m_generalUi->useGroupIconOnEntryCreationCheckBox->isChecked());
+ config()->set(Config::GUI_ShowSubgroupEntries, m_generalUi->showSubgroupEntriesCheckBox->isChecked());
config()->set(Config::AutoTypeEntryTitleMatch, m_generalUi->autoTypeEntryTitleMatchCheckBox->isChecked());
config()->set(Config::AutoTypeEntryURLMatch, m_generalUi->autoTypeEntryURLMatchCheckBox->isChecked());
config()->set(Config::AutoTypeHideExpiredEntry, m_generalUi->autoTypeHideExpiredEntryCheckBox->isChecked());
diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui
index 0e297e0ec..c5dd8266a 100644
--- a/src/gui/ApplicationSettingsWidgetGeneral.ui
+++ b/src/gui/ApplicationSettingsWidgetGeneral.ui
@@ -573,6 +573,13 @@
+ -
+
+
+ Show subgroup entries in entry list view
+
+
+
-
@@ -1441,6 +1448,7 @@
EnableCopyOnDoubleClickCheckBox
openUrlOnDoubleClick
useGroupIconOnEntryCreationCheckBox
+ showSubgroupEntriesCheckBox
minimizeOnOpenUrlCheckBox
hideWindowOnCopyCheckBox
minimizeOnCopyRadioButton
diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp
index 218da6ca8..378bfce82 100644
--- a/src/gui/entry/EntryModel.cpp
+++ b/src/gui/entry/EntryModel.cpp
@@ -68,10 +68,38 @@ void EntryModel::setGroup(Group* group)
m_group = group;
m_allGroups.clear();
- m_entries = group->entries();
- m_orgEntries.clear();
- makeConnections(group);
+ // Check if we should show subgroup entries
+ if (config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ m_entries = group->entriesRecursive();
+
+ // When showing subgroup entries, we need to connect to all groups
+ // that contain the entries being displayed
+ for (const auto& entry : asConst(m_entries)) {
+ if (entry->group() && !m_allGroups.contains(entry->group())) {
+ m_allGroups.append(entry->group());
+ }
+ }
+
+ // Always include the current group itself to handle new entries added directly to it
+ if (!m_allGroups.contains(group)) {
+ m_allGroups.append(group);
+ }
+
+ // Connect to all groups that have entries in the view (or could have entries)
+ for (const auto& groupToConnect : m_allGroups) {
+ if (groupToConnect) {
+ makeConnections(groupToConnect);
+ }
+ }
+
+ // Also connect to groupAdded signal from the main group to detect new subgroups
+ connect(group, SIGNAL(groupAdded()), SLOT(groupAdded()));
+ } else {
+ m_entries = group->entries();
+ makeConnections(group);
+ }
+ m_orgEntries.clear();
endResetModel();
}
@@ -88,13 +116,15 @@ void EntryModel::setEntries(const QList& entries)
m_orgEntries = entries;
for (const auto entry : asConst(m_entries)) {
- if (entry->group()) {
- m_allGroups.insert(entry->group());
+ if (entry->group() && !m_allGroups.contains(entry->group())) {
+ m_allGroups.append(entry->group());
}
}
for (const auto group : m_allGroups) {
- makeConnections(group);
+ if (group) {
+ makeConnections(group);
+ }
}
endResetModel();
@@ -539,7 +569,12 @@ void EntryModel::entryAdded(Entry* entry)
}
if (m_group) {
- m_entries = m_group->entries();
+ // Check if we should show subgroup entries
+ if (config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ m_entries = m_group->entriesRecursive();
+ } else {
+ m_entries = m_group->entries();
+ }
}
endInsertRows();
}
@@ -555,7 +590,12 @@ void EntryModel::entryAboutToRemove(Entry* entry)
void EntryModel::entryRemoved()
{
if (m_group) {
- m_entries = m_group->entries();
+ // Check if we should show subgroup entries
+ if (config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ m_entries = m_group->entriesRecursive();
+ } else {
+ m_entries = m_group->entries();
+ }
}
endRemoveRows();
}
@@ -571,7 +611,12 @@ void EntryModel::entryAboutToMoveUp(int row)
void EntryModel::entryMovedUp()
{
if (m_group) {
- m_entries = m_group->entries();
+ // Check if we should show subgroup entries
+ if (config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ m_entries = m_group->entriesRecursive();
+ } else {
+ m_entries = m_group->entries();
+ }
}
endMoveRows();
}
@@ -587,11 +632,26 @@ void EntryModel::entryAboutToMoveDown(int row)
void EntryModel::entryMovedDown()
{
if (m_group) {
- m_entries = m_group->entries();
+ // Check if we should show subgroup entries
+ if (config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ m_entries = m_group->entriesRecursive();
+ } else {
+ m_entries = m_group->entries();
+ }
}
endMoveRows();
}
+void EntryModel::groupAdded()
+{
+ // When a new subgroup is added to the current group and we're showing subgroup entries,
+ // we need to refresh our connections to include the new subgroup
+ if (m_group && config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ // Refresh the entry list and connections to include new subgroups
+ setGroup(m_group);
+ }
+}
+
void EntryModel::entryDataChanged(Entry* entry)
{
int row = m_entries.indexOf(entry);
@@ -607,6 +667,12 @@ void EntryModel::onConfigChanged(Config::ConfigKey key)
case Config::GUI_HidePasswords:
emit dataChanged(index(0, Password), index(rowCount() - 1, Password), {Qt::DisplayRole});
break;
+ case Config::GUI_ShowSubgroupEntries:
+ // Refresh the entry list when the subgroup setting changes
+ if (m_group) {
+ setGroup(m_group);
+ }
+ break;
default:
break;
}
@@ -618,8 +684,17 @@ void EntryModel::severConnections()
disconnect(m_group, nullptr, this, nullptr);
}
- for (const Group* group : asConst(m_allGroups)) {
- disconnect(group, nullptr, this, nullptr);
+ // Use an iterator to safely remove null pointers while iterating
+ auto it = m_allGroups.begin();
+ while (it != m_allGroups.end()) {
+ if (*it) {
+ // Group is still valid, disconnect from it
+ disconnect(*it, nullptr, this, nullptr);
+ ++it;
+ } else {
+ // Group has been deleted, remove the null pointer
+ it = m_allGroups.erase(it);
+ }
}
}
diff --git a/src/gui/entry/EntryModel.h b/src/gui/entry/EntryModel.h
index 7b7f17a1f..398ca0a0c 100644
--- a/src/gui/entry/EntryModel.h
+++ b/src/gui/entry/EntryModel.h
@@ -19,7 +19,9 @@
#define KEEPASSX_ENTRYMODEL_H
#include
+#include
#include
+#include
#include
#include "core/Config.h"
@@ -81,6 +83,7 @@ private slots:
void entryAboutToMoveDown(int row);
void entryMovedDown();
void entryDataChanged(Entry* entry);
+ void groupAdded();
void onConfigChanged(Config::ConfigKey key);
@@ -89,10 +92,10 @@ private:
void makeConnections(const Group* group);
bool m_backgroundColorVisible = true;
- Group* m_group;
+ QPointer m_group;
QList m_entries;
QList m_orgEntries;
- QSet m_allGroups;
+ QList> m_allGroups;
const QString HiddenContentDisplay;
};
diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp
index bacf0c05f..6d0bd8b3e 100644
--- a/src/gui/entry/EntryView.cpp
+++ b/src/gui/entry/EntryView.cpp
@@ -33,6 +33,8 @@
#include "gui/Icons.h"
#include "gui/SortFilterHideProxyModel.h"
+#include "core/Config.h"
+
#define ICON_ONLY_SECTION_SIZE 26
class PasswordStrengthItemDelegate : public QStyledItemDelegate
@@ -94,6 +96,9 @@ EntryView::EntryView(QWidget* parent)
emit entrySelectionChanged(currentEntry());
});
+ // Listen for config changes to update Group column visibility
+ connect(config(), &Config::changed, this, &EntryView::onConfigChanged);
+
new QShortcut(Qt::CTRL + Qt::Key_F10, this, SLOT(contextMenuShortcutPressed()), nullptr, Qt::WidgetShortcut);
new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_J, this, SLOT(jumpToGroupShortcut()), nullptr, Qt::WidgetShortcut);
@@ -210,7 +215,16 @@ void EntryView::focusInEvent(QFocusEvent* event)
void EntryView::displayGroup(Group* group)
{
m_model->setGroup(group);
- header()->hideSection(EntryModel::ParentGroup);
+
+ // Show Group column when subgroup entries are enabled, since entries from different groups will be shown
+ // But respect user's preference if they've manually hidden it
+ if (config()->get(Config::GUI_ShowSubgroupEntries).toBool() && !m_userHidGroupColumnInSubgroupMode) {
+ header()->showSection(EntryModel::ParentGroup);
+ } else if (!config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ header()->hideSection(EntryModel::ParentGroup);
+ }
+ // If user has hidden the column in subgroup mode, don't force it to show
+
setFirstEntryActive();
m_inSearchMode = false;
}
@@ -356,7 +370,8 @@ void EntryView::showHeaderMenu(const QPoint& position)
int columnIndex = action->data().toInt();
action->setChecked(!isColumnHidden(columnIndex));
}
- actions[EntryModel::ParentGroup]->setVisible(inSearchMode());
+ actions[EntryModel::ParentGroup]->setVisible(inSearchMode()
+ || config()->get(Config::GUI_ShowSubgroupEntries).toBool());
m_headerMenu->popup(mapToGlobal(position));
}
@@ -386,11 +401,21 @@ void EntryView::toggleColumnVisibility(QAction* action)
if (header()->sectionSize(columnIndex) == 0) {
header()->resizeSection(columnIndex, header()->defaultSectionSize());
}
+ // Reset flag when user manually shows Group column
+ if (columnIndex == EntryModel::ParentGroup && !m_inSearchMode
+ && config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ m_userHidGroupColumnInSubgroupMode = false;
+ }
resetFixedColumns();
return;
}
if ((header()->count() - header()->hiddenSectionCount()) > 1) {
header()->hideSection(columnIndex);
+ // Track when user manually hides Group column while subgroup entries is enabled
+ if (columnIndex == EntryModel::ParentGroup && !m_inSearchMode
+ && config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ m_userHidGroupColumnInSubgroupMode = true;
+ }
return;
}
action->setChecked(true);
@@ -462,7 +487,8 @@ void EntryView::resetFixedColumns()
void EntryView::resetViewToDefaults()
{
// Reduce number of columns that are shown by default
- if (m_inSearchMode) {
+ if (m_inSearchMode
+ || (config()->get(Config::GUI_ShowSubgroupEntries).toBool() && !m_userHidGroupColumnInSubgroupMode)) {
header()->showSection(EntryModel::ParentGroup);
} else {
header()->hideSection(EntryModel::ParentGroup);
@@ -597,6 +623,22 @@ bool EntryView::isColumnHidden(int logicalIndex)
return header()->isSectionHidden(logicalIndex) || header()->sectionSize(logicalIndex) == 0;
}
+void EntryView::onConfigChanged(Config::ConfigKey key)
+{
+ if (key == Config::GUI_ShowSubgroupEntries && !m_inSearchMode) {
+ // Reset user preference when setting is toggled - this allows the
+ // Group column to auto-appear when re-enabling the feature
+ m_userHidGroupColumnInSubgroupMode = false;
+
+ // Update Group column visibility when subgroup entries setting changes
+ if (config()->get(Config::GUI_ShowSubgroupEntries).toBool()) {
+ header()->showSection(EntryModel::ParentGroup);
+ } else {
+ header()->hideSection(EntryModel::ParentGroup);
+ }
+ }
+}
+
void EntryView::jumpToGroupShortcut()
{
// Only allow jump to group in search mode
diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h
index f74e6a3e7..90a161fa2 100644
--- a/src/gui/entry/EntryView.h
+++ b/src/gui/entry/EntryView.h
@@ -21,6 +21,7 @@
#include
+#include "core/Config.h"
#include "gui/entry/EntryModel.h"
class Entry;
@@ -72,6 +73,7 @@ private slots:
void resetViewToDefaults();
void contextMenuShortcutPressed();
void sortIndicatorChanged(int logicalIndex, Qt::SortOrder order);
+ void onConfigChanged(Config::ConfigKey key);
void jumpToGroupShortcut();
private:
@@ -85,6 +87,7 @@ private:
Qt::SortOrder m_lastOrder;
bool m_inSearchMode = false;
bool m_columnsNeedRelayout = true;
+ bool m_userHidGroupColumnInSubgroupMode = false;
QMenu* m_headerMenu;
QActionGroup* m_columnActions;