refactor: Improve UX

This commit is contained in:
Petr Mironychev
2026-07-07 22:36:06 +02:00
parent bf3a67a1eb
commit 0382cd17a0
23 changed files with 1051 additions and 779 deletions

View File

@@ -7,6 +7,7 @@
#include "Pill.hpp"
#include "PipelinesConfig.hpp"
#include "SettingsTr.hpp"
#include "SettingsUiBuilders.hpp"
#include "TagFilterStrip.hpp"
#include <coreplugin/icore.h>
@@ -19,9 +20,11 @@
#include <QEvent>
#include <QFont>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLineEdit>
#include <QMap>
#include <QMouseEvent>
#include <QPointer>
#include <QPushButton>
#include <QScopedValueRollback>
#include <QScrollArea>
@@ -64,6 +67,11 @@ bool ListRowCard::hasAllTags(const QSet<QString> &activeTags) const
return true;
}
void ListRowCard::setFilterHighlight(const QString &lowerNeedle)
{
Q_UNUSED(lowerNeedle)
}
void ListRowCard::buildSearchHaystack(const QStringList &parts)
{
m_searchHaystack = parts.join(QLatin1Char(' ')).toLower();
@@ -121,9 +129,8 @@ void ListRowCard::applyTheme()
} else if (m_hover) {
bg = t.hoverBg;
}
setStyleSheet(QStringLiteral(
"#ListRowCard { background-color: %1; border: 1px solid %2; }")
.arg(bg, bd));
setStyleSheet(
QStringLiteral("#ListRowCard { background-color: %1; border: 1px solid %2; }").arg(bg, bd));
}
AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
@@ -136,19 +143,25 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
haystack += cfg.tags;
buildSearchHaystack(haystack);
auto *name = new QLabel(cfg.name, this);
auto *name = new QLabel(cfg.name.toHtmlEscaped(), this);
QFont nameFont = name->font();
nameFont.setBold(true);
nameFont.setPixelSize(13);
name->setFont(nameFont);
name->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
name->setTextFormat(Qt::RichText);
m_nameLabel = name;
QLabel *model = nullptr;
if (!cfg.model.isEmpty()) {
model = new QLabel(QStringLiteral("· %1").arg(cfg.model), this);
model = new QLabel(cfg.model.toHtmlEscaped(), this);
model->setFont(CardStyle::monoFont(11));
model->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
model->setMinimumWidth(0);
model->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
model->setTextFormat(Qt::RichText);
m_modelLabel = model;
m_model = cfg.model;
}
Pill *sourcePill = nullptr;
@@ -156,47 +169,21 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
sourcePill = new Pill(Pill::User, Tr::tr("User"), this);
}
auto *description = new QLabel(this);
description->setWordWrap(false);
QFont descFont = description->font();
descFont.setItalic(true);
description->setFont(descFont);
description->setText(cfg.description.isEmpty()
? Tr::tr("No description provided.")
: cfg.description);
description->setMinimumWidth(0);
description->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
description->setTextInteractionFlags(Qt::NoTextInteraction);
QStringList endpointParts;
if (!cfg.endpoint.isEmpty())
endpointParts << cfg.endpoint;
endpointParts << (cfg.enableThinking ? Tr::tr("thinking") : Tr::tr("no-thinking"));
endpointParts << (cfg.enableTools ? Tr::tr("tools") : Tr::tr("no-tools"));
auto *endpoint = new QLabel(endpointParts.join(QStringLiteral(" · ")), this);
endpoint->setFont(CardStyle::monoFont(11));
endpoint->setMinimumWidth(0);
endpoint->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
endpoint->setTextInteractionFlags(Qt::NoTextInteraction);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(6);
headerRow->addWidget(name);
if (sourcePill)
headerRow->addWidget(sourcePill);
if (model)
headerRow->addWidget(model, 1);
else
headerRow->addStretch(1);
if (sourcePill)
headerRow->addWidget(sourcePill);
auto *outer = new QVBoxLayout(this);
outer->setContentsMargins(10, 8, 10, 8);
outer->setSpacing(3);
outer->setContentsMargins(8, 5, 8, 5);
outer->setSpacing(2);
outer->addLayout(headerRow);
outer->addWidget(description);
outer->addWidget(endpoint);
if (!cfg.tags.isEmpty()) {
constexpr int kMaxTagPills = 4;
@@ -207,10 +194,8 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
for (int i = 0; i < std::min(tagCount, kMaxTagPills); ++i)
tagsRow->addWidget(new Pill(Pill::Tag, cfg.tags.at(i), this));
if (tagCount > kMaxTagPills) {
auto *more = new Pill(
Pill::Tag,
QStringLiteral("+%1").arg(tagCount - kMaxTagPills),
this);
auto *more
= new Pill(Pill::Tag, QStringLiteral("+%1").arg(tagCount - kMaxTagPills), this);
more->setToolTip(cfg.tags.mid(kMaxTagPills).join(QStringLiteral(", ")));
tagsRow->addWidget(more);
}
@@ -218,32 +203,45 @@ AgentRowCard::AgentRowCard(const AgentConfig &cfg, QWidget *parent)
outer->addLayout(tagsRow);
}
const auto t = CardStyle::toneFor(CardStyle::isDark(palette()));
QPalette descPal = description->palette();
descPal.setColor(QPalette::WindowText,
QColor(cfg.description.isEmpty() ? t.textFaint : t.textSoft));
description->setPalette(descPal);
QPalette endPal = endpoint->palette();
endPal.setColor(QPalette::WindowText, QColor(t.textFaint));
endpoint->setPalette(endPal);
if (model) {
const auto t = CardStyle::toneFor(CardStyle::isDark(palette()));
QPalette mp = model->palette();
mp.setColor(QPalette::WindowText, QColor(t.textMute));
model->setPalette(mp);
}
QStringList capabilityParts;
if (!cfg.endpoint.isEmpty())
capabilityParts << cfg.endpoint;
capabilityParts << (cfg.enableThinking ? Tr::tr("thinking") : Tr::tr("no-thinking"));
capabilityParts << (cfg.enableTools ? Tr::tr("tools") : Tr::tr("no-tools"));
QString tooltip;
if (!cfg.description.isEmpty())
tooltip += cfg.description + QStringLiteral("\n\n");
if (!cfg.model.isEmpty())
tooltip += Tr::tr("Model: %1\n").arg(cfg.model);
if (!cfg.providerInstance.isEmpty())
tooltip += Tr::tr("Provider instance: %1\n").arg(cfg.providerInstance);
if (!cfg.systemPrompt.isEmpty())
tooltip += Tr::tr("System prompt: %1\n").arg(cfg.systemPrompt);
if (!cfg.endpoint.isEmpty())
tooltip += Tr::tr("Endpoint: %1\n").arg(cfg.endpoint);
tooltip += capabilityParts.join(QStringLiteral(" · "));
if (!cfg.tags.isEmpty())
tooltip += QStringLiteral("\n")
+ Tr::tr("Tags: %1").arg(cfg.tags.join(QStringLiteral(", ")));
setToolTip(tooltip.trimmed());
}
void AgentRowCard::setFilterHighlight(const QString &lowerNeedle)
{
if (m_lowerNeedle == lowerNeedle)
return;
m_lowerNeedle = lowerNeedle;
m_nameLabel->setText(filterHighlightedHtml(itemName(), lowerNeedle));
if (m_modelLabel)
m_modelLabel->setText(filterHighlightedHtml(m_model, lowerNeedle));
}
ProviderSection::ProviderSection(const QString &name, QWidget *parent)
: QWidget(parent)
{
@@ -297,10 +295,12 @@ void ProviderSection::addCard(ListRowCard *card)
int ProviderSection::applyFilter(const QString &needle, const QSet<QString> &activeTags)
{
const QString lowerNeedle = needle.toLower();
int visible = 0;
for (auto *card : m_cards) {
const bool show = card->matches(needle) && card->hasAllTags(activeTags);
card->setVisible(show);
card->setFilterHighlight(lowerNeedle);
if (show)
++visible;
}
@@ -350,9 +350,10 @@ AgentSelectionDialog::AgentSelectionDialog(
m_localConfigs = configs;
m_filter = new QLineEdit(this);
m_filter->setPlaceholderText(
Tr::tr("Filter by name, provider, model, template, description…"));
m_filter->setPlaceholderText(Tr::tr("Filter by name, provider, model, template, description…"));
m_filter->setClearButtonEnabled(true);
m_filter->setToolTip(Tr::tr("Type to filter; Up/Down moves through the list."));
m_filter->installEventFilter(this);
auto *topRow = new QHBoxLayout;
topRow->setContentsMargins(0, 0, 0, 0);
@@ -360,6 +361,7 @@ AgentSelectionDialog::AgentSelectionDialog(
topRow->addWidget(m_filter, 1);
m_tagStrip = new TagFilterStrip(this);
m_tagStrip->setMaxColumns(6);
const bool dark = CardStyle::isDark(palette());
const auto tone = CardStyle::toneFor(dark);
@@ -389,8 +391,7 @@ AgentSelectionDialog::AgentSelectionDialog(
m_scroll->setFrameShape(QFrame::StyledPanel);
m_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
auto *buttons
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
m_okButton = buttons->button(QDialogButtonBox::Ok);
m_okButton->setText(isChange ? Tr::tr("Change") : Tr::tr("Add"));
m_okButton->setEnabled(false);
@@ -442,6 +443,59 @@ void AgentSelectionDialog::setAllExpanded(bool expanded)
section->setExpanded(expanded);
}
bool AgentSelectionDialog::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_filter && event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_Down) {
moveSelection(1);
return true;
}
if (ke->key() == Qt::Key_Up) {
moveSelection(-1);
return true;
}
}
return QDialog::eventFilter(watched, event);
}
QList<ListRowCard *> AgentSelectionDialog::filteredCards() const
{
const QString needle = m_filter ? m_filter->text().trimmed() : QString();
const QSet<QString> activeTags = m_tagStrip ? m_tagStrip->activeTags() : QSet<QString>();
QList<ListRowCard *> out;
for (auto *section : m_sections)
for (auto *card : section->cards())
if (card->matches(needle) && card->hasAllTags(activeTags))
out.append(card);
return out;
}
void AgentSelectionDialog::moveSelection(int delta)
{
const QList<ListRowCard *> cards = filteredCards();
if (cards.isEmpty())
return;
const int cur = m_currentCard ? int(cards.indexOf(m_currentCard)) : -1;
int next = cur < 0 ? (delta > 0 ? 0 : int(cards.size()) - 1) : cur + delta;
next = qBound(0, next, int(cards.size()) - 1);
if (next == cur)
return;
ListRowCard *card = cards.at(next);
for (auto *section : m_sections) {
if (section->cards().contains(card)) {
section->setExpanded(true);
break;
}
}
selectCard(card);
QPointer<ListRowCard> guarded(card);
QTimer::singleShot(0, this, [this, guarded] {
if (guarded)
m_scroll->ensureWidgetVisible(guarded, 0, 40);
});
}
void AgentSelectionDialog::selectCard(ListRowCard *card)
{
if (m_currentCard == card)
@@ -467,8 +521,7 @@ void AgentSelectionDialog::rebuild(const QString &currentName)
if (m_okButton)
m_okButton->setEnabled(false);
const auto &configs
= m_agentFactory ? m_agentFactory->configs() : m_localConfigs;
const auto &configs = m_agentFactory ? m_agentFactory->configs() : m_localConfigs;
auto *content = new QWidget;
auto *contentLayout = new QVBoxLayout(content);
@@ -479,9 +532,8 @@ void AgentSelectionDialog::rebuild(const QString &currentName)
for (const auto &cfg : configs) {
if (cfg.hidden)
continue;
const QString key = cfg.providerInstance.isEmpty()
? Tr::tr("(Unknown provider instance)")
: cfg.providerInstance;
const QString key = cfg.providerInstance.isEmpty() ? Tr::tr("(Unknown provider instance)")
: cfg.providerInstance;
byProvider[key].push_back(&cfg);
}
@@ -491,12 +543,13 @@ void AgentSelectionDialog::rebuild(const QString &currentName)
for (auto it = byProvider.cbegin(); it != byProvider.cend(); ++it) {
auto *section = new ProviderSection(it.key());
auto sortedConfigs = it.value();
std::sort(sortedConfigs.begin(), sortedConfigs.end(),
[](const AgentConfig *a, const AgentConfig *b) { return a->name < b->name; });
std::sort(
sortedConfigs.begin(),
sortedConfigs.end(),
[](const AgentConfig *a, const AgentConfig *b) { return a->name < b->name; });
for (const AgentConfig *cfg : sortedConfigs) {
auto *card = new AgentRowCard(*cfg);
connect(card, &ListRowCard::clicked, this,
[this, card]() { selectCard(card); });
connect(card, &ListRowCard::clicked, this, [this, card]() { selectCard(card); });
connect(card, &ListRowCard::activated, this, [this, card]() {
selectCard(card);
accept();
@@ -554,6 +607,8 @@ void AgentSelectionDialog::applyFilters()
section->setExpanded(visible > 0);
total += visible;
}
if (m_currentCard && (!m_currentCard->matches(needle) || !m_currentCard->hasAllTags(activeTags)))
selectCard(nullptr);
if (m_emptyLabel)
m_emptyLabel->setVisible(total == 0);
if (m_resultCount)

View File

@@ -50,21 +50,28 @@ inline bool isDark(const QPalette &p)
inline Tone toneFor(bool dark)
{
return dark
? Tone{"#333333", "#3a3a3a", "#2a4566", "#3a6fb7", "#4a4a4a",
"#c2c2c2", "#9a9a9a", "#7a7a7a"}
: Tone{"#f6f6f6", "#ececec", "#cfe1f7", "#3a6fb7", "#bdbdbd",
"#3a3a3a", "#5a5a5a", "#8a8a8a"};
? Tone{"#333333", "#3a3a3a", "#2a4566", "#3a6fb7", "#4a4a4a", "#c2c2c2", "#9a9a9a", "#7a7a7a"}
: Tone{
"#f6f6f6",
"#ececec",
"#cfe1f7",
"#3a6fb7",
"#bdbdbd",
"#3a3a3a",
"#5a5a5a",
"#8a8a8a"};
}
inline QFont monoFont(int pixelSize)
{
QFont f;
f.setFamilies({QStringLiteral("SF Mono"),
QStringLiteral("Cascadia Code"),
QStringLiteral("Consolas"),
QStringLiteral("Liberation Mono"),
QStringLiteral("Menlo"),
QStringLiteral("Courier New")});
f.setFamilies(
{QStringLiteral("SF Mono"),
QStringLiteral("Cascadia Code"),
QStringLiteral("Consolas"),
QStringLiteral("Liberation Mono"),
QStringLiteral("Menlo"),
QStringLiteral("Courier New")});
f.setStyleHint(QFont::Monospace);
f.setPixelSize(pixelSize);
return f;
@@ -94,6 +101,7 @@ public:
bool matches(const QString &needle) const;
bool hasAllTags(const QSet<QString> &activeTags) const;
void setSelected(bool selected);
virtual void setFilterHighlight(const QString &lowerNeedle);
signals:
void clicked();
@@ -130,6 +138,13 @@ public:
explicit AgentRowCard(const AgentConfig &cfg, QWidget *parent = nullptr);
QString agentName() const { return itemName(); }
void setFilterHighlight(const QString &lowerNeedle) override;
private:
QLabel *m_nameLabel = nullptr;
QLabel *m_modelLabel = nullptr;
QString m_model;
QString m_lowerNeedle;
};
class ProviderSection : public QWidget
@@ -171,11 +186,16 @@ public:
QString selectedName() const { return m_selectedName; }
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void rebuild(const QString &currentName);
void selectCard(ListRowCard *card);
void applyFilters();
void setAllExpanded(bool expanded);
void moveSelection(int delta);
QList<ListRowCard *> filteredCards() const;
QLineEdit *m_filter = nullptr;
TagFilterStrip *m_tagStrip = nullptr;