From 714b1367b71b07517f117df1eca026f08a4992f0 Mon Sep 17 00:00:00 2001 From: Petr Mironychev <9195189+Palm1r@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:52:59 +0200 Subject: [PATCH] feat: Improve list in agent settngs list --- settings/AgentListPane.cpp | 76 ++++++++++++++++--- settings/AgentListPane.hpp | 3 + settings/CMakeLists.txt | 1 + settings/CollapsibleHeader.cpp | 130 ++++++++++++++++++++++++++++++++ settings/CollapsibleHeader.hpp | 45 +++++++++++ settings/SettingsUiBuilders.cpp | 16 +++- 6 files changed, 256 insertions(+), 15 deletions(-) create mode 100644 settings/CollapsibleHeader.cpp create mode 100644 settings/CollapsibleHeader.hpp diff --git a/settings/AgentListPane.cpp b/settings/AgentListPane.cpp index 81448db..0fca167 100644 --- a/settings/AgentListPane.cpp +++ b/settings/AgentListPane.cpp @@ -5,6 +5,7 @@ #include "AgentListPane.hpp" #include "AgentListItem.hpp" +#include "CollapsibleHeader.hpp" #include "SettingsTheme.hpp" #include "SettingsUiBuilders.hpp" #include "TagFilterStrip.hpp" @@ -21,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -90,12 +92,18 @@ void AgentListPane::selectByName(const QString &name) { if (name.isEmpty()) return; + if (m_factory) { + if (const AgentConfig *cfg = m_factory->configByName(name)) + m_expandedGroups.insert(groupKey(*cfg)); + } setCurrentNameInternal(name, false); rebuildList(); for (auto *item : m_rows) { if (item->agentName() == name) { - QTimer::singleShot(0, this, [this, item] { - m_listScroll->ensureWidgetVisible(item, 0, 60); + QPointer guarded(item); + QTimer::singleShot(0, this, [this, guarded] { + if (guarded) + m_listScroll->ensureWidgetVisible(guarded, 0, 60); }); break; } @@ -183,6 +191,7 @@ void AgentListPane::rebuildList() contentLayout->setSpacing(0); const QSet &activeTags = m_tagStrip->activeTags(); + const bool filtersActive = !lowerFilter.isEmpty() || !activeTags.isEmpty(); auto addAgents = [&](const std::vector &agents) { for (const AgentConfig *cfg : agents) { auto *item = new AgentListItem(*cfg, content); @@ -196,16 +205,50 @@ void AgentListPane::rebuildList() newRows.append(item); } }; + QSet liveKeys; + auto addSection = [&](const QString &title, const QString §ionKey, + const std::vector &agents) { + if (agents.empty()) + return; + contentLayout->addWidget(makeSectionHeader(title, content)); - if (!userAgents.empty()) { - contentLayout->addWidget(makeSectionHeader(tr("User"), content)); - addAgents(userAgents); - } - if (!bundledAgents.empty()) { - contentLayout->addWidget(makeSectionHeader(tr("Bundled"), content)); - addAgents(bundledAgents); - } - if (newRows.isEmpty()) { + QMap> byProvider; + for (const AgentConfig *cfg : agents) + byProvider[providerLabel(*cfg)].push_back(cfg); + QStringList providers = byProvider.keys(); + std::sort(providers.begin(), providers.end(), + [](const QString &l, const QString &r) { return l.localeAwareCompare(r) < 0; }); + + for (const QString &provider : providers) { + const std::vector &group = byProvider[provider]; + const QString key = sectionKey + QLatin1Char('/') + provider; + liveKeys.insert(key); + const bool expanded = filtersActive || m_expandedGroups.contains(key); + + auto *header = new CollapsibleHeader(provider, int(group.size()), content); + header->setExpanded(expanded); + header->setClickable(!filtersActive); + if (!filtersActive) { + connect(header, &CollapsibleHeader::toggled, this, + [this, key] { + if (!m_expandedGroups.remove(key)) + m_expandedGroups.insert(key); + rebuildList(); + }, + Qt::QueuedConnection); + } + contentLayout->addWidget(header); + + if (expanded) + addAgents(group); + } + }; + + addSection(tr("User"), QStringLiteral("user"), userAgents); + addSection(tr("Bundled"), QStringLiteral("bundled"), bundledAgents); + if (!filtersActive) + m_expandedGroups.intersect(liveKeys); + if (userAgents.empty() && bundledAgents.empty()) { auto *empty = new QLabel(tr("No agents match these filters."), content); empty->setAlignment(Qt::AlignCenter); empty->setContentsMargins(10, 16, 10, 16); @@ -248,4 +291,15 @@ void AgentListPane::setCurrentNameInternal(const QString &name, bool emitSignal) emit currentAgentChanged(m_currentName); } +QString AgentListPane::providerLabel(const AgentConfig &a) const +{ + return a.providerInstance.isEmpty() ? tr("Other") : a.providerInstance; +} + +QString AgentListPane::groupKey(const AgentConfig &a) const +{ + const QString section = a.isUserSource() ? QStringLiteral("user") : QStringLiteral("bundled"); + return section + QLatin1Char('/') + providerLabel(a); +} + } // namespace QodeAssist::Settings diff --git a/settings/AgentListPane.hpp b/settings/AgentListPane.hpp index 3182d2d..6843cac 100644 --- a/settings/AgentListPane.hpp +++ b/settings/AgentListPane.hpp @@ -49,6 +49,8 @@ private: std::vector visibleAgents() const; void setCurrentNameInternal(const QString &name, bool emitSignal); void onRowClicked(const QString &name); + QString providerLabel(const AgentConfig &a) const; + QString groupKey(const AgentConfig &a) const; AgentFactory *m_factory; QLineEdit *m_filterEdit = nullptr; @@ -58,6 +60,7 @@ private: QScrollArea *m_listScroll = nullptr; QList m_rows; QString m_currentName; + QSet m_expandedGroups; }; } // namespace QodeAssist::Settings diff --git a/settings/CMakeLists.txt b/settings/CMakeLists.txt index 7e85f4e..c519d3e 100644 --- a/settings/CMakeLists.txt +++ b/settings/CMakeLists.txt @@ -30,6 +30,7 @@ add_library(QodeAssistSettings STATIC AgentListPane.hpp AgentListPane.cpp AgentDuplicator.hpp AgentDuplicator.cpp TagFilterStrip.hpp TagFilterStrip.cpp + CollapsibleHeader.hpp CollapsibleHeader.cpp ) target_link_libraries(QodeAssistSettings diff --git a/settings/CollapsibleHeader.cpp b/settings/CollapsibleHeader.cpp new file mode 100644 index 0000000..aa09e47 --- /dev/null +++ b/settings/CollapsibleHeader.cpp @@ -0,0 +1,130 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later +// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE + +#include "CollapsibleHeader.hpp" + +#include "SettingsTheme.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace QodeAssist::Settings { + +CollapsibleHeader::CollapsibleHeader(const QString &title, int count, QWidget *parent) + : QFrame(parent) +{ + setObjectName(QStringLiteral("CollapsibleHeader")); + setAutoFillBackground(true); + setCursor(Qt::PointingHandCursor); + + m_arrow = new QLabel(this); + QFont af = m_arrow->font(); + af.setPixelSize(9); + m_arrow->setFont(af); + m_arrow->setFixedWidth(12); + m_arrow->setAlignment(Qt::AlignCenter); + + m_title = new QLabel(title.toUpper(), this); + QFont tf = m_title->font(); + tf.setPixelSize(11); + tf.setBold(true); + tf.setLetterSpacing(QFont::AbsoluteSpacing, 0.4); + m_title->setFont(tf); + + m_count = new QLabel(QString::number(count), this); + m_count->setFont(monospaceFont(10)); + + auto *row = new QHBoxLayout(this); + row->setContentsMargins(16, 5, 10, 5); + row->setSpacing(6); + row->addWidget(m_arrow, 0, Qt::AlignVCenter); + row->addWidget(m_title, 0, Qt::AlignVCenter); + row->addStretch(1); + row->addWidget(m_count, 0, Qt::AlignVCenter); + + updateArrow(); + applyTheme(); +} + +void CollapsibleHeader::setExpanded(bool expanded) +{ + m_expanded = expanded; + updateArrow(); +} + +void CollapsibleHeader::setClickable(bool clickable) +{ + m_clickable = clickable; + setCursor(clickable ? Qt::PointingHandCursor : Qt::ArrowCursor); +} + +void CollapsibleHeader::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_clickable && event->button() == Qt::LeftButton) + emit toggled(); + QFrame::mouseReleaseEvent(event); +} + +void CollapsibleHeader::enterEvent(QEnterEvent *event) +{ + m_hovered = true; + applyTheme(); + QFrame::enterEvent(event); +} + +void CollapsibleHeader::leaveEvent(QEvent *event) +{ + m_hovered = false; + applyTheme(); + QFrame::leaveEvent(event); +} + +void CollapsibleHeader::changeEvent(QEvent *event) +{ + QFrame::changeEvent(event); + if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) + applyTheme(); +} + +void CollapsibleHeader::updateArrow() +{ + m_arrow->setText(m_expanded ? QStringLiteral("▾") : QStringLiteral("▸")); +} + +void CollapsibleHeader::applyTheme() +{ + if (m_inApplyTheme) + return; + QScopedValueRollback guard(m_inApplyTheme, true); + + const QColor base = Utils::creatorColor(Utils::Theme::BackgroundColorNormal); + const QColor mid = Utils::creatorColor(Utils::Theme::PanelTextColorMid); + const QColor bg = mix(base, mid, m_hovered ? 0.18 : 0.08); + + setStyleSheet(QStringLiteral("#CollapsibleHeader { background:%1;" + " border-top:1px solid %2; }") + .arg(cssColor(bg), cssColor(mix(base, mid, 0.25)))); + + QPalette ap = m_arrow->palette(); + ap.setColor(QPalette::WindowText, mid); + m_arrow->setPalette(ap); + + QPalette tp = m_title->palette(); + tp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorLight)); + m_title->setPalette(tp); + + QPalette cp = m_count->palette(); + cp.setColor(QPalette::WindowText, mid); + m_count->setPalette(cp); +} + +} // namespace QodeAssist::Settings diff --git a/settings/CollapsibleHeader.hpp b/settings/CollapsibleHeader.hpp new file mode 100644 index 0000000..0117032 --- /dev/null +++ b/settings/CollapsibleHeader.hpp @@ -0,0 +1,45 @@ +// Copyright (C) 2024-2026 Petr Mironychev +// SPDX-License-Identifier: GPL-3.0-or-later +// Additional attribution terms under GPLv3 §7(b) apply — see LICENSE + +#pragma once + +#include + +class QLabel; + +namespace QodeAssist::Settings { + +class CollapsibleHeader : public QFrame +{ + Q_OBJECT +public: + explicit CollapsibleHeader(const QString &title, int count, QWidget *parent = nullptr); + + void setExpanded(bool expanded); + bool isExpanded() const { return m_expanded; } + void setClickable(bool clickable); + +signals: + void toggled(); + +protected: + void mouseReleaseEvent(QMouseEvent *event) override; + void enterEvent(QEnterEvent *event) override; + void leaveEvent(QEvent *event) override; + void changeEvent(QEvent *event) override; + +private: + void applyTheme(); + void updateArrow(); + + bool m_expanded = false; + bool m_clickable = true; + bool m_hovered = false; + bool m_inApplyTheme = false; + QLabel *m_arrow = nullptr; + QLabel *m_title = nullptr; + QLabel *m_count = nullptr; +}; + +} // namespace QodeAssist::Settings diff --git a/settings/SettingsUiBuilders.cpp b/settings/SettingsUiBuilders.cpp index c1de0bc..336dc78 100644 --- a/settings/SettingsUiBuilders.cpp +++ b/settings/SettingsUiBuilders.cpp @@ -31,14 +31,22 @@ void applyMutedSmallCaps(QLabel *label) QLabel *makeSectionHeader(const QString &title, QWidget *parent) { auto *header = new QLabel(title.toUpper(), parent); - applyMutedSmallCaps(header); - header->setContentsMargins(8, 4, 8, 4); + QFont f = header->font(); + f.setPixelSize(11); + f.setBold(true); + f.setLetterSpacing(QFont::AbsoluteSpacing, 0.6); + header->setFont(f); + QPalette p = header->palette(); + p.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorLight)); + header->setPalette(p); + header->setContentsMargins(8, 5, 8, 5); header->setAutoFillBackground(true); + const QColor base = Utils::creatorColor(Utils::Theme::BackgroundColorNormal); + const QColor mid = Utils::creatorColor(Utils::Theme::PanelTextColorMid); header->setStyleSheet( QStringLiteral("QLabel { background:%1; border-top:1px solid %2;" " border-bottom:1px solid %2; }") - .arg(cssColor(Utils::creatorColor(Utils::Theme::BackgroundColorNormal)), - cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)))); + .arg(cssColor(mix(base, mid, 0.16)), cssColor(mix(base, mid, 0.30)))); return header; }