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

@@ -4,13 +4,11 @@
#include "AgentDetailPane.hpp"
#include "AgentModelDialog.hpp"
#include "ModelSelector.hpp"
#include "SectionBox.hpp"
#include "SettingsTheme.hpp"
#include "SettingsUiBuilders.hpp"
#include <optional>
#include <utils/theme/theme.h>
#include <AgentFactory.hpp>
@@ -84,6 +82,9 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
m_name->setFont(nf);
m_name->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_sourceBadge = new QLabel(this);
m_sourceBadge->setVisible(false);
m_path = new QLabel(this);
m_path->setFont(monospaceFont(11));
m_path->setTextInteractionFlags(Qt::TextSelectableByMouse);
@@ -91,20 +92,20 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
pp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_path->setPalette(pp);
m_openBtn = new QPushButton(tr("Open in editor"), this);
m_dupBtn = new QPushButton(tr("Customize a copy…"), this);
m_dupBtn->setToolTip(
tr("Create an editable user agent that inherits from this one — "
"override only the fields you want."));
m_openBtn = new QPushButton(tr("Open in editor"), this);
m_deleteBtn = new QPushButton(tr("Delete"), this);
connect(m_openBtn, &QPushButton::clicked, this, [this] {
if (m_current)
emit openInEditorRequested(*m_current);
});
connect(m_dupBtn, &QPushButton::clicked, this, [this] {
if (m_current)
emit customizeRequested(*m_current);
});
connect(m_openBtn, &QPushButton::clicked, this, [this] {
if (m_current)
emit openInEditorRequested(*m_current);
});
connect(m_deleteBtn, &QPushButton::clicked, this, [this] {
if (m_current)
emit deleteRequested(*m_current);
@@ -114,14 +115,15 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
auto *actions = new QHBoxLayout(m_actionsHolder);
actions->setContentsMargins(0, 0, 0, 0);
actions->setSpacing(6);
actions->addWidget(m_openBtn);
actions->addWidget(m_dupBtn);
actions->addWidget(m_openBtn);
actions->addWidget(m_deleteBtn);
auto *titleRow = new QHBoxLayout;
titleRow->setContentsMargins(0, 0, 0, 0);
titleRow->setSpacing(8);
titleRow->addWidget(m_name);
titleRow->addWidget(m_sourceBadge, 0, Qt::AlignVCenter);
titleRow->addStretch(1);
auto *headerLeft = new QVBoxLayout;
@@ -144,41 +146,32 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
m_description->setWordWrap(true);
m_description->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *identity = new SectionBox(tr("Identity"), this);
m_nameValue = makeReadOnlyLine();
m_extendsLabel = new QLabel(tr("Extends:"), this);
m_extendsLabel->setMinimumWidth(96);
m_extendsLabel->setAlignment(Qt::AlignLeft | Qt::AlignTop);
m_extendsValue = makeReadOnlyLine();
m_descriptionEdit = new QPlainTextEdit(this);
m_descriptionEdit->setReadOnly(true);
m_descriptionEdit->setMaximumHeight(56);
m_tagsValue = makeReadOnlyLine();
auto *setup = new SectionBox(tr("Configuration"), this);
auto *idGrid = new QGridLayout;
idGrid->setContentsMargins(0, 0, 0, 0);
idGrid->setHorizontalSpacing(8);
idGrid->setVerticalSpacing(4);
FormBuilder idForm(idGrid);
idForm.row(tr("Name:"), m_nameValue);
{
auto *holder = new QWidget;
holder->setLayout(singleField(m_extendsValue));
const int row = idForm.currentRow();
idGrid->addWidget(m_extendsLabel, row, 0, Qt::AlignTop);
idGrid->addWidget(holder, row, 1);
m_extendsHolder = holder;
idForm = FormBuilder(idGrid, row + 1);
}
idForm.row(tr("Description:"), m_descriptionEdit);
idForm.row(
tr("Tags:"),
m_tagsValue,
tr("Comma-separated. Free-form — used to filter and "
"group the agent list."));
identity->bodyLayout()->addLayout(idGrid);
m_modelSelector = new ModelSelector(this);
connect(m_modelSelector, &ModelSelector::modelSubmitted, this, [this](const QString &model) {
onModelSubmitted(model);
});
m_modelResetBtn = new QPushButton(tr("Reset"), this);
m_modelResetBtn->setToolTip(tr("Remove the model override and restore the agent's default"));
m_modelResetBtn->setVisible(false);
connect(m_modelResetBtn, &QPushButton::clicked, this, [this] { onResetModel(); });
auto *modelHolder = new QWidget(this);
auto *modelRow = new QHBoxLayout(modelHolder);
modelRow->setContentsMargins(0, 0, 0, 0);
modelRow->setSpacing(6);
modelRow->addWidget(m_modelSelector, 1);
modelRow->addWidget(m_modelResetBtn);
m_modelStatus = makeHintLabel(QString(), this);
m_modelStatus->setVisible(false);
connect(m_modelSelector, &ModelSelector::statusChanged, this, [this](const QString &status) {
m_modelStatus->setText(status);
m_modelStatus->setVisible(!status.isEmpty());
});
auto *connection = new SectionBox(tr("Connection"), this);
m_providerCombo = new QComboBox(this);
m_providerCombo->setSizeAdjustPolicy(QComboBox::AdjustToContents);
m_providerCombo->setEnabled(false);
@@ -199,65 +192,6 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
providerRow->addWidget(m_providerCombo, 1);
providerRow->addWidget(m_providerResetBtn);
m_endpointValue = makeReadOnlyLine(true);
m_modelValue = makeReadOnlyLine(true);
m_modelValue->setPlaceholderText(tr("(set a model)"));
m_modelChangeBtn = new QPushButton(tr("Change…"), this);
m_modelChangeBtn->setToolTip(tr("Pick a model from the provider or type one"));
m_modelChangeBtn->setEnabled(false);
connect(m_modelChangeBtn, &QPushButton::clicked, this, [this] { onChangeModel(); });
m_modelResetBtn = new QPushButton(tr("Reset"), this);
m_modelResetBtn->setToolTip(tr("Remove the model override and restore the agent's default"));
m_modelResetBtn->setVisible(false);
connect(m_modelResetBtn, &QPushButton::clicked, this, [this] { onResetModel(); });
auto *modelHolder = new QWidget(this);
auto *modelRow = new QHBoxLayout(modelHolder);
modelRow->setContentsMargins(0, 0, 0, 0);
modelRow->setSpacing(6);
modelRow->addWidget(m_modelValue, 1);
modelRow->addWidget(m_modelChangeBtn);
modelRow->addWidget(m_modelResetBtn);
auto *connGrid = new QGridLayout;
connGrid->setContentsMargins(0, 0, 0, 0);
connGrid->setHorizontalSpacing(8);
connGrid->setVerticalSpacing(4);
FormBuilder(connGrid)
.row(
tr("Provider:"),
providerHolder,
tr("The provider instance this agent uses. URL is "
"inherited from the instance. Switching it is saved as a "
"per-agent override; Reset restores the agent's default."))
.row(
tr("Endpoint:"),
m_endpointValue,
tr("Appended to the provider's URL. Blank uses the "
"provider default."))
.row(
tr("Model:"),
modelHolder,
tr("Model sent to the provider. Click Change… to pick from the "
"provider's available models or type one. The choice is saved "
"as a per-agent override in settings; Reset restores the "
"agent's default."));
connection->bodyLayout()->addLayout(connGrid);
m_effectiveUrl = new QLabel(this);
m_effectiveUrl->setFont(monospaceFont(11));
m_effectiveUrl->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_effectiveUrl->setWordWrap(true);
m_effectiveUrl->setContentsMargins(6, 4, 6, 4);
m_effectiveUrl->setAutoFillBackground(true);
connection->bodyLayout()->addWidget(m_effectiveUrl);
auto *capabilities = new SectionBox(tr("Capabilities"), this);
m_thinkingValue = new QLabel(this);
m_toolsCheck = new QCheckBox(tr("Allow tool calls"), this);
m_toolsCheck->setEnabled(false);
connect(m_toolsCheck, &QCheckBox::clicked, this, [this](bool on) { onToggleTools(on); });
@@ -275,38 +209,46 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
toolsRow->addStretch(1);
toolsRow->addWidget(m_toolsResetBtn);
auto *capGrid = new QGridLayout;
capGrid->setContentsMargins(0, 0, 0, 0);
capGrid->setHorizontalSpacing(8);
capGrid->setVerticalSpacing(4);
FormBuilder(capGrid)
.row(
tr("Thinking:"),
m_thinkingValue,
tr("Whether this agent requests extended thinking. Defined by the "
"agent template."))
.row(
tr("Tools:"),
toolsHolder,
tr("Whether the agent may call tools. Toggle to save a per-agent "
"override in settings; Reset restores the agent's default."));
capabilities->bodyLayout()->addLayout(capGrid);
m_thinkingValue = new QLabel(this);
auto *match = new SectionBox(tr("Match"), this);
auto *matchHint = makeHintLabel(
tr("When a feature slot has multiple bound agents, the first whose "
"match rules satisfy the current context wins."));
auto *setupGrid = new QGridLayout;
setupGrid->setContentsMargins(0, 0, 0, 0);
setupGrid->setHorizontalSpacing(8);
setupGrid->setVerticalSpacing(4);
FormBuilder setupForm(setupGrid);
setupForm.row(
tr("Model:"),
modelHolder,
tr("Click to pick from the provider's models, or type a name and press Enter."));
{
const int row = setupForm.currentRow();
setupGrid->addWidget(m_modelStatus, row, 1);
setupForm = FormBuilder(setupGrid, row + 1);
}
setupForm.row(tr("Provider:"), providerHolder)
.row(tr("Tools:"), toolsHolder)
.row(tr("Thinking:"), m_thinkingValue);
setup->bodyLayout()->addLayout(setupGrid);
setup->bodyLayout()->addWidget(makeHintLabel(
tr("Changes apply immediately and are saved as per-agent overrides in settings; "
"Reset restores the value from the agent file."),
this));
m_extendsValue = makeReadOnlyLine();
m_extendsValue->setPlaceholderText(tr("(none)"));
m_tagsValue = makeReadOnlyLine();
m_tagsValue->setPlaceholderText(tr("(none)"));
m_endpointValue = makeReadOnlyLine(true);
m_endpointValue->setPlaceholderText(tr("(provider default)"));
m_filePatternsValue = makeReadOnlyLine(true);
auto *matchGrid = new QGridLayout;
matchGrid->setContentsMargins(0, 0, 0, 0);
matchGrid->setHorizontalSpacing(8);
matchGrid->setVerticalSpacing(4);
FormBuilder(matchGrid).row(
tr("File patterns:"),
m_filePatternsValue,
tr("Globs, comma-separated. Empty matches every file."));
match->bodyLayout()->addWidget(matchHint);
match->bodyLayout()->addLayout(matchGrid);
m_filePatternsValue->setPlaceholderText(tr("(matches every file)"));
m_effectiveUrl = new QLabel(this);
m_effectiveUrl->setFont(monospaceFont(11));
m_effectiveUrl->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_effectiveUrl->setWordWrap(true);
m_effectiveUrl->setContentsMargins(6, 4, 6, 4);
m_effectiveUrl->setAutoFillBackground(true);
m_rawToggle = new QToolButton(this);
m_rawToggle->setText(tr("▸ Show raw TOML"));
@@ -338,18 +280,49 @@ AgentDetailPane::AgentDetailPane(QWidget *parent)
m_baseRawToggle->setText(on ? tr("▾ Hide base agent TOML") : tr("▸ Show base agent TOML"));
});
m_detailsBody = new QWidget(this);
auto *detailsLayout = new QVBoxLayout(m_detailsBody);
detailsLayout->setContentsMargins(0, 0, 0, 0);
detailsLayout->setSpacing(10);
auto *detailsGrid = new QGridLayout;
detailsGrid->setContentsMargins(0, 0, 0, 0);
detailsGrid->setHorizontalSpacing(8);
detailsGrid->setVerticalSpacing(4);
FormBuilder(detailsGrid)
.row(tr("Extends:"), m_extendsValue)
.row(tr("Tags:"), m_tagsValue, tr("Free-form — used to filter and group the agent list."))
.row(tr("Endpoint:"), m_endpointValue, tr("Appended to the provider's URL."))
.row(
tr("File patterns:"),
m_filePatternsValue,
tr("Globs, comma-separated. When a feature slot has multiple bound "
"agents, the first whose match rules satisfy the context wins."));
detailsLayout->addLayout(detailsGrid);
detailsLayout->addWidget(m_effectiveUrl);
detailsLayout->addWidget(m_rawToggle, 0, Qt::AlignLeft);
detailsLayout->addWidget(m_rawToml);
detailsLayout->addWidget(m_baseRawToggle, 0, Qt::AlignLeft);
detailsLayout->addWidget(m_baseRawToml);
m_detailsBody->setVisible(false);
m_detailsToggle = new QToolButton(this);
m_detailsToggle->setText(tr("▸ More details"));
m_detailsToggle->setCursor(Qt::PointingHandCursor);
m_detailsToggle->setAutoRaise(true);
m_detailsToggle->setCheckable(true);
connect(m_detailsToggle, &QToolButton::toggled, this, [this](bool on) {
m_detailsBody->setVisible(on);
m_detailsToggle->setText(on ? tr("▾ More details") : tr("▸ More details"));
});
m_body = new QWidget(this);
auto *bodyLayout = new QVBoxLayout(m_body);
bodyLayout->setContentsMargins(0, 0, 0, 0);
bodyLayout->setSpacing(10);
bodyLayout->addWidget(identity);
bodyLayout->addWidget(connection);
bodyLayout->addWidget(capabilities);
bodyLayout->addWidget(match);
bodyLayout->addWidget(m_rawToggle, 0, Qt::AlignLeft);
bodyLayout->addWidget(m_rawToml);
bodyLayout->addWidget(m_baseRawToggle, 0, Qt::AlignLeft);
bodyLayout->addWidget(m_baseRawToml);
bodyLayout->addWidget(setup);
bodyLayout->addWidget(m_detailsToggle, 0, Qt::AlignLeft);
bodyLayout->addWidget(m_detailsBody);
auto *root = new QVBoxLayout(this);
root->setContentsMargins(12, 12, 12, 12);
@@ -407,25 +380,19 @@ void AgentDetailPane::populateProviderCombo()
m_providerComboPopulated = true;
}
void AgentDetailPane::onChangeModel()
void AgentDetailPane::onModelSubmitted(const QString &model)
{
if (!m_agentFactory || !m_current)
return;
const QString name = m_current->name;
AgentModelDialog dialog(m_agentFactory, name, m_current->model, this);
if (dialog.exec() != QDialog::Accepted)
return;
const QString model = dialog.selectedModel();
if (model == m_current->model)
return;
QString err;
if (!m_agentFactory->setAgentModelOverride(name, model, &err)) {
const bool ok = m_agentFactory->setAgentModelOverride(name, model, &err);
if (!ok)
QMessageBox::warning(this, tr("Set model"), err);
return;
}
if (const AgentConfig *cfg = m_agentFactory->configByName(name))
setAgent(*cfg);
}
@@ -540,22 +507,11 @@ void AgentDetailPane::setAgent(const AgentConfig &cfg)
setDetailsVisible(true);
m_name->setText(cfg.name);
m_sourceBadge->setText(user ? tr("User") : tr("Bundled"));
m_path->setText(cfg.sourcePath);
m_description->setText(
cfg.description.isEmpty() ? tr("No description provided.") : cfg.description);
m_nameValue->setText(cfg.name);
if (cfg.extendsName.isEmpty()) {
m_extendsLabel->setVisible(false);
m_extendsHolder->setVisible(false);
} else {
m_extendsLabel->setVisible(true);
m_extendsHolder->setVisible(true);
m_extendsValue->setText(cfg.extendsName);
}
m_descriptionEdit->setPlainText(cfg.description);
m_tagsValue->setText(cfg.tags.join(QStringLiteral(", ")));
populateProviderCombo();
if (m_providerComboHasSentinel) {
@@ -589,14 +545,11 @@ void AgentDetailPane::setAgent(const AgentConfig &cfg)
? tr("Overridden in settings. Reset to use the agent's default provider.")
: QString());
m_endpointValue->setText(cfg.endpoint);
m_endpointValue->setPlaceholderText(tr("(provider default)"));
m_modelValue->setText(cfg.model);
m_modelChangeBtn->setEnabled(m_agentFactory != nullptr);
m_modelSelector->setAgent(m_agentFactory, cfg.name, cfg.providerInstance, cfg.model);
const bool hasModelOverride = m_agentFactory
&& !m_agentFactory->agentModelOverride(cfg.name).isEmpty();
m_modelResetBtn->setVisible(hasModelOverride);
m_modelValue->setToolTip(
m_modelSelector->setToolTip(
hasModelOverride ? tr("Overridden in settings. Reset to use the agent's default model.")
: QString());
@@ -613,14 +566,16 @@ void AgentDetailPane::setAgent(const AgentConfig &cfg)
hasToolsOverride ? tr("Overridden in settings. Reset to use the agent's default.")
: QString());
m_extendsValue->setText(cfg.extendsName);
m_tagsValue->setText(cfg.tags.join(QStringLiteral(", ")));
m_endpointValue->setText(cfg.endpoint);
m_filePatternsValue->setText(cfg.match.filePatterns.join(QStringLiteral(", ")));
const QString eff = resolvedUrl + cfg.endpoint;
m_effectiveUrl->setText(
eff.isEmpty() ? tr("# effective request line\n(unknown — provider instance not found)")
: QStringLiteral("# %1\nPOST %2").arg(tr("effective request line"), eff));
m_filePatternsValue->setText(cfg.match.filePatterns.join(QStringLiteral(", ")));
m_filePatternsValue->setPlaceholderText(tr("(matches every file)"));
fillRawToml(m_rawToml, cfg.sourcePath);
const QString basePath = m_agentFactory ? m_agentFactory->sourcePathForName(cfg.extendsName)
@@ -652,11 +607,6 @@ void AgentDetailPane::clear()
m_name->setText(tr("Select an agent"));
m_path->clear();
m_description->setText(tr("Pick an agent from the list to see its details."));
m_nameValue->clear();
m_extendsLabel->setVisible(false);
m_extendsHolder->setVisible(false);
m_descriptionEdit->clear();
m_tagsValue->clear();
if (m_providerComboHasSentinel) {
m_providerCombo->removeItem(0);
m_providerComboHasSentinel = false;
@@ -664,11 +614,8 @@ void AgentDetailPane::clear()
m_providerCombo->setCurrentIndex(-1);
m_providerCombo->setEnabled(false);
m_providerResetBtn->setVisible(false);
m_endpointValue->clear();
m_modelValue->clear();
m_modelChangeBtn->setEnabled(false);
m_modelSelector->clearAgent();
m_modelResetBtn->setVisible(false);
m_effectiveUrl->clear();
m_thinkingValue->clear();
{
QSignalBlocker block(m_toolsCheck);
@@ -676,7 +623,11 @@ void AgentDetailPane::clear()
}
m_toolsCheck->setEnabled(false);
m_toolsResetBtn->setVisible(false);
m_extendsValue->clear();
m_tagsValue->clear();
m_endpointValue->clear();
m_filePatternsValue->clear();
m_effectiveUrl->clear();
m_rawToml->clear();
m_baseRawToml->clear();
m_baseRawToggle->setVisible(false);
@@ -728,6 +679,7 @@ void AgentDetailPane::fillRawToml(QPlainTextEdit *view, const QString &path)
void AgentDetailPane::setDetailsVisible(bool visible)
{
m_headerSep->setVisible(visible);
m_sourceBadge->setVisible(visible);
m_path->setVisible(visible);
m_actionsHolder->setVisible(visible);
m_body->setVisible(visible);
@@ -744,6 +696,8 @@ void AgentDetailPane::applyCodePalette()
m_effectiveUrl->setStyleSheet(
QStringLiteral("QLabel { background:%1; border:1px solid %2; }")
.arg(cssColor(codeBg), cssColor(Utils::creatorColor(Utils::Theme::SplitterColor))));
if (m_current)
styleSourceBadge(m_sourceBadge, m_current->isUserSource());
}
} // namespace QodeAssist::Settings

View File

@@ -29,6 +29,7 @@ class ProviderInstanceFactory;
namespace QodeAssist::Settings {
class ModelSelector;
class SectionBox;
class AgentDetailPane : public QWidget
@@ -56,7 +57,7 @@ private:
void setDetailsVisible(bool visible);
void applyCodePalette();
void populateProviderCombo();
void onChangeModel();
void onModelSubmitted(const QString &model);
void onResetModel();
void onChangeProvider(int index);
void onResetProvider();
@@ -70,7 +71,11 @@ private:
AgentConfig m_currentStorage;
const AgentConfig *m_current = nullptr;
QPointer<Providers::ProviderInstanceFactory> m_instanceFactory;
QPointer<AgentFactory> m_agentFactory;
QLabel *m_name = nullptr;
QLabel *m_sourceBadge = nullptr;
QLabel *m_path = nullptr;
QFrame *m_headerSep = nullptr;
QWidget *m_actionsHolder = nullptr;
@@ -80,32 +85,25 @@ private:
QPushButton *m_deleteBtn = nullptr;
QLabel *m_description = nullptr;
QLineEdit *m_nameValue = nullptr;
QLabel *m_extendsLabel = nullptr;
QWidget *m_extendsHolder = nullptr;
QLineEdit *m_extendsValue = nullptr;
QPlainTextEdit *m_descriptionEdit = nullptr;
QLineEdit *m_tagsValue = nullptr;
ModelSelector *m_modelSelector = nullptr;
QPushButton *m_modelResetBtn = nullptr;
QLabel *m_modelStatus = nullptr;
QComboBox *m_providerCombo = nullptr;
QPushButton *m_providerResetBtn = nullptr;
QPointer<Providers::ProviderInstanceFactory> m_instanceFactory;
QPointer<AgentFactory> m_agentFactory;
QLineEdit *m_endpointValue = nullptr;
QLineEdit *m_modelValue = nullptr;
QPushButton *m_modelChangeBtn = nullptr;
QPushButton *m_modelResetBtn = nullptr;
QLabel *m_effectiveUrl = nullptr;
QLabel *m_thinkingValue = nullptr;
QCheckBox *m_toolsCheck = nullptr;
QPushButton *m_toolsResetBtn = nullptr;
QLabel *m_thinkingValue = nullptr;
QToolButton *m_detailsToggle = nullptr;
QWidget *m_detailsBody = nullptr;
QLineEdit *m_extendsValue = nullptr;
QLineEdit *m_tagsValue = nullptr;
QLineEdit *m_endpointValue = nullptr;
QLabel *m_effectiveUrl = nullptr;
QLineEdit *m_filePatternsValue = nullptr;
QToolButton *m_rawToggle = nullptr;
QPlainTextEdit *m_rawToml = nullptr;
QToolButton *m_baseRawToggle = nullptr;
QPlainTextEdit *m_baseRawToml = nullptr;
};

View File

@@ -5,8 +5,9 @@
#include "AgentListItem.hpp"
#include "SettingsTheme.hpp"
#include "TagChip.hpp"
#include "SettingsUiBuilders.hpp"
#include <utils/elidinglabel.h>
#include <utils/theme/theme.h>
#include <QEvent>
@@ -16,73 +17,52 @@
#include <QMouseEvent>
#include <QPalette>
#include <QScopedValueRollback>
#include <QVBoxLayout>
namespace QodeAssist::Settings {
AgentListItem::AgentListItem(const AgentConfig &cfg, QWidget *parent)
: QFrame(parent)
, m_name(cfg.name)
, m_model(cfg.model)
{
setObjectName(QStringLiteral("AgentListItem"));
setFrameShape(QFrame::NoFrame);
setAutoFillBackground(true);
setCursor(Qt::PointingHandCursor);
auto *dot = new QLabel(QStringLiteral(""), this);
QFont df = dot->font();
df.setPixelSize(10);
dot->setFont(df);
QPalette dp = dot->palette();
dp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
dot->setPalette(dp);
auto *nameLbl = new QLabel(cfg.name, this);
QFont nf = nameLbl->font();
m_nameLabel = new QLabel(this);
QFont nf = m_nameLabel->font();
nf.setBold(true);
nf.setPixelSize(12);
nameLbl->setFont(nf);
m_nameLabel->setFont(nf);
m_nameLabel->setTextFormat(Qt::RichText);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(6);
headerRow->addWidget(dot, 0, Qt::AlignVCenter);
headerRow->addWidget(nameLbl, 1);
auto *col = new QVBoxLayout;
col->setContentsMargins(0, 0, 0, 0);
col->setSpacing(2);
col->addLayout(headerRow);
m_modelLabel = new QLabel(cfg.model, this);
m_modelLabel = new Utils::ElidingLabel(this);
m_modelLabel->setElideMode(Qt::ElideMiddle);
m_modelLabel->setFont(monospaceFont(11));
m_modelLabel->setContentsMargins(16, 0, 0, 0);
m_modelLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
QPalette mp = m_modelLabel->palette();
mp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_modelLabel->setPalette(mp);
m_modelLabel->setText(cfg.model);
m_modelLabel->setVisible(!cfg.model.isEmpty());
col->addWidget(m_modelLabel);
if (!cfg.tags.isEmpty()) {
auto *tagsHolder = new QWidget(this);
auto *tagsLay = new QHBoxLayout(tagsHolder);
tagsLay->setContentsMargins(16, 2, 0, 0);
tagsLay->setSpacing(3);
for (const QString &t : cfg.tags) {
auto *chip = new TagChip(t, -1, tagsHolder);
connect(chip, &TagChip::clicked, this, &AgentListItem::tagClicked);
m_chips.append(chip);
tagsLay->addWidget(chip);
}
tagsLay->addStretch(1);
col->addWidget(tagsHolder);
}
auto *row = new QHBoxLayout(this);
row->setContentsMargins(8, 4, 8, 4);
row->setSpacing(8);
row->addWidget(m_nameLabel, 0);
row->addWidget(m_modelLabel, 1);
auto *outer = new QVBoxLayout(this);
outer->setContentsMargins(5, 6, 8, 6);
outer->setSpacing(0);
outer->addLayout(col);
QStringList tipLines;
if (!cfg.description.isEmpty())
tipLines << cfg.description;
if (!cfg.model.isEmpty())
tipLines << tr("Model: %1").arg(cfg.model);
if (!cfg.tags.isEmpty())
tipLines << tr("Tags: %1").arg(cfg.tags.join(QStringLiteral(", ")));
setToolTip(tipLines.join(QStringLiteral("\n")));
updateNameText();
applyTheme();
}
@@ -94,20 +74,26 @@ void AgentListItem::setSelected(bool selected)
applyTheme();
}
void AgentListItem::setActiveTags(const QSet<QString> &active)
{
for (auto *chip : m_chips)
chip->setActive(active.contains(chip->tag()));
}
void AgentListItem::setModel(const QString &model)
{
if (!m_modelLabel)
return;
m_model = model;
m_modelLabel->setText(model);
m_modelLabel->setVisible(!model.isEmpty());
}
void AgentListItem::setFilterHighlight(const QString &lowerFilter)
{
if (m_filter == lowerFilter)
return;
m_filter = lowerFilter;
updateNameText();
}
void AgentListItem::updateNameText()
{
m_nameLabel->setText(filterHighlightedHtml(m_name, m_filter));
}
void AgentListItem::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
@@ -133,6 +119,7 @@ void AgentListItem::applyTheme()
"#AgentListItem { background:transparent;"
" border-top:1px solid %1; border-left:3px solid %2; }")
.arg(cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)), accent));
updateNameText();
}
} // namespace QodeAssist::Settings

View File

@@ -5,8 +5,6 @@
#pragma once
#include <QFrame>
#include <QList>
#include <QSet>
#include <QString>
#include <AgentConfig.hpp>
@@ -15,9 +13,11 @@ QT_BEGIN_NAMESPACE
class QLabel;
QT_END_NAMESPACE
namespace QodeAssist::Settings {
namespace Utils {
class ElidingLabel;
}
class TagChip;
namespace QodeAssist::Settings {
class AgentListItem : public QFrame
{
@@ -27,12 +27,11 @@ public:
QString agentName() const { return m_name; }
void setSelected(bool selected);
void setActiveTags(const QSet<QString> &active);
void setModel(const QString &model);
void setFilterHighlight(const QString &lowerFilter);
signals:
void clicked(const QString &name);
void tagClicked(const QString &tag);
protected:
void mouseReleaseEvent(QMouseEvent *event) override;
@@ -40,12 +39,15 @@ protected:
private:
void applyTheme();
void updateNameText();
QString m_name;
QString m_model;
QString m_filter;
bool m_selected = false;
bool m_inApplyTheme = false;
QLabel *m_modelLabel = nullptr;
QList<TagChip *> m_chips;
QLabel *m_nameLabel = nullptr;
Utils::ElidingLabel *m_modelLabel = nullptr;
};
} // namespace QodeAssist::Settings

View File

@@ -19,6 +19,7 @@
#include <QEvent>
#include <QFont>
#include <QHBoxLayout>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QMap>
@@ -37,8 +38,13 @@ AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
setFrameShape(QFrame::StyledPanel);
m_filterEdit = new QLineEdit(this);
m_filterEdit->setPlaceholderText(tr("Filter agents"));
m_filterEdit->setPlaceholderText(tr("Filter by name, model, provider, tag"));
m_filterEdit->setClearButtonEnabled(true);
m_filterEdit->setToolTip(tr("Type to filter; Up/Down moves through the list."));
m_filterEdit->installEventFilter(this);
setFocusProxy(m_filterEdit);
m_tagStrip = new TagFilterStrip(this);
auto *filterRow = new QHBoxLayout;
filterRow->setContentsMargins(6, 6, 6, 6);
@@ -48,8 +54,6 @@ AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
m_filterHolder->setLayout(filterRow);
m_filterHolder->setAutoFillBackground(true);
m_tagStrip = new TagFilterStrip(this);
m_listScroll = new QScrollArea(this);
m_listScroll->setWidgetResizable(true);
m_listScroll->setFrameShape(QFrame::NoFrame);
@@ -59,19 +63,22 @@ AgentListPane::AgentListPane(AgentFactory *factory, QWidget *parent)
outer->setContentsMargins(0, 0, 0, 0);
outer->setSpacing(0);
outer->addWidget(m_filterHolder);
outer->addWidget(m_tagStrip);
outer->addWidget(m_listScroll, 1);
m_filterDebounce = new QTimer(this);
m_filterDebounce->setSingleShot(true);
m_filterDebounce->setInterval(100);
connect(m_filterDebounce, &QTimer::timeout, this, &AgentListPane::rebuildList);
connect(m_filterEdit, &QLineEdit::textChanged, this,
[this](const QString &) { m_filterDebounce->start(); });
connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &) {
m_filterDebounce->start();
});
connect(m_tagStrip, &TagFilterStrip::activeTagsChanged, this,
[this](const QSet<QString> &) { rebuildList(); },
Qt::QueuedConnection);
connect(
m_tagStrip,
&TagFilterStrip::activeTagsChanged,
this,
[this](const QSet<QString> &) { rebuildList(); },
Qt::QueuedConnection);
if (m_factory) {
connect(m_factory, &AgentFactory::agentModelChanged, this, [this](const QString &name) {
@@ -93,7 +100,7 @@ void AgentListPane::selectByName(const QString &name)
return;
if (m_factory) {
if (const AgentConfig *cfg = m_factory->configByName(name))
m_expandedGroups.insert(groupKey(*cfg));
m_collapsedGroups.remove(groupKey(*cfg));
}
setCurrentNameInternal(name, false);
m_notifyOnRebuild = true;
@@ -121,6 +128,12 @@ void AgentListPane::refresh()
rebuildList();
}
void AgentListPane::focusFilter()
{
m_filterEdit->setFocus(Qt::OtherFocusReason);
m_filterEdit->selectAll();
}
void AgentListPane::changeEvent(QEvent *event)
{
QFrame::changeEvent(event);
@@ -128,6 +141,41 @@ void AgentListPane::changeEvent(QEvent *event)
applyFilterHolderTheme();
}
bool AgentListPane::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_filterEdit && 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 QFrame::eventFilter(watched, event);
}
void AgentListPane::moveSelection(int delta)
{
if (m_rows.isEmpty())
return;
int cur = -1;
for (int i = 0; i < m_rows.size(); ++i) {
if (m_rows.at(i)->agentName() == m_currentName) {
cur = i;
break;
}
}
int next = cur < 0 ? (delta > 0 ? 0 : int(m_rows.size()) - 1) : cur + delta;
next = qBound(0, next, int(m_rows.size()) - 1);
if (next == cur)
return;
setCurrentNameInternal(m_rows.at(next)->agentName(), true);
m_listScroll->ensureWidgetVisible(m_rows.at(next), 0, 40);
}
void AgentListPane::applyFilterHolderTheme()
{
if (!m_filterHolder)
@@ -156,9 +204,14 @@ std::vector<const AgentConfig *> AgentListPane::visibleAgents() const
bool AgentListPane::matchesFilters(const AgentConfig &a, const QString &lowerFilter) const
{
if (!lowerFilter.isEmpty()
&& !(a.name + QLatin1Char(' ') + a.model).toLower().contains(lowerFilter))
return false;
if (!lowerFilter.isEmpty()) {
const QString haystack = (a.name + QLatin1Char(' ') + a.model + QLatin1Char(' ')
+ a.providerInstance + QLatin1Char(' ')
+ a.tags.join(QLatin1Char(' ')))
.toLower();
if (!haystack.contains(lowerFilter))
return false;
}
const QSet<QString> &active = m_tagStrip->activeTags();
for (const QString &t : active)
if (!a.tags.contains(t))
@@ -199,14 +252,8 @@ void AgentListPane::rebuildList()
for (const AgentConfig *cfg : agents) {
auto *item = new AgentListItem(*cfg, content);
item->setSelected(cfg->name == m_currentName);
item->setActiveTags(activeTags);
item->setFilterHighlight(lowerFilter);
connect(item, &AgentListItem::clicked, this, &AgentListPane::onRowClicked);
connect(
item,
&AgentListItem::tagClicked,
this,
[this](const QString &tag) { m_tagStrip->toggleTag(tag); },
Qt::QueuedConnection);
contentLayout->addWidget(item);
newRows.append(item);
}
@@ -231,7 +278,7 @@ void AgentListPane::rebuildList()
const std::vector<const AgentConfig *> &group = byProvider[provider];
const QString key = sectionKey + QLatin1Char('/') + provider;
liveKeys.insert(key);
const bool expanded = filtersActive || m_expandedGroups.contains(key);
const bool expanded = filtersActive || !m_collapsedGroups.contains(key);
auto *header = new CollapsibleHeader(provider, int(group.size()), content);
header->setExpanded(expanded);
@@ -242,8 +289,8 @@ void AgentListPane::rebuildList()
&CollapsibleHeader::toggled,
this,
[this, key] {
if (!m_expandedGroups.remove(key))
m_expandedGroups.insert(key);
if (!m_collapsedGroups.remove(key))
m_collapsedGroups.insert(key);
rebuildList();
},
Qt::QueuedConnection);
@@ -258,7 +305,7 @@ void AgentListPane::rebuildList()
addSection(tr("User"), QStringLiteral("user"), userAgents);
addSection(tr("Bundled"), QStringLiteral("bundled"), bundledAgents);
if (!filtersActive)
m_expandedGroups.intersect(liveKeys);
m_collapsedGroups.intersect(liveKeys);
if (userAgents.empty() && bundledAgents.empty()) {
auto *empty = new QLabel(tr("No agents match these filters."), content);
empty->setAlignment(Qt::AlignCenter);
@@ -273,10 +320,9 @@ void AgentListPane::rebuildList()
m_rows = newRows;
m_listScroll->setWidget(content);
const AgentConfig *current
= m_currentName.isEmpty() || !m_factory
? nullptr
: m_factory->configByName(m_currentName);
const AgentConfig *current = m_currentName.isEmpty() || !m_factory
? nullptr
: m_factory->configByName(m_currentName);
if (!current && !m_rows.isEmpty()) {
const QString fallback = m_rows.front()->agentName();
m_rows.front()->setSelected(true);

View File

@@ -4,11 +4,11 @@
#pragma once
#include <vector>
#include <QFrame>
#include <QList>
#include <QSet>
#include <QString>
#include <vector>
#include <AgentConfig.hpp>
@@ -35,15 +35,19 @@ public:
QString currentName() const { return m_currentName; }
void selectByName(const QString &name);
void refresh();
void focusFilter();
TagFilterStrip *tagStrip() const { return m_tagStrip; }
signals:
void currentAgentChanged(const QString &name);
protected:
void changeEvent(QEvent *event) override;
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void rebuildList();
void moveSelection(int delta);
void applyFilterHolderTheme();
bool matchesFilters(const AgentConfig &a, const QString &lowerFilter) const;
std::vector<const AgentConfig *> visibleAgents() const;
@@ -60,7 +64,7 @@ private:
QScrollArea *m_listScroll = nullptr;
QList<AgentListItem *> m_rows;
QString m_currentName;
QSet<QString> m_expandedGroups;
QSet<QString> m_collapsedGroups;
bool m_notifyOnRebuild = false;
};

View File

@@ -1,139 +0,0 @@
// 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 "AgentModelDialog.hpp"
#include "SettingsTheme.hpp"
#include <utils/theme/theme.h>
#include <Agent.hpp>
#include <AgentFactory.hpp>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QPalette>
#include <QPushButton>
#include <QSignalBlocker>
#include <QVBoxLayout>
namespace QodeAssist::Settings {
AgentModelDialog::AgentModelDialog(
AgentFactory *factory, const QString &agentName, const QString &currentModel, QWidget *parent)
: QDialog(parent)
, m_factory(factory)
, m_agentName(agentName)
{
setWindowTitle(tr("Select Model"));
resize(440, 380);
m_modelEdit = new QLineEdit(currentModel, this);
m_modelEdit->setFont(monospaceFont(11));
m_modelEdit->setPlaceholderText(tr("Type a model name or pick one below"));
m_modelEdit->setClearButtonEnabled(true);
m_fetchBtn = new QPushButton(tr("Fetch available models"), this);
m_fetchBtn->setToolTip(tr("Query the agent's provider for its installed models"));
m_status = new QLabel(this);
m_status->setFont(monospaceFont(10));
QPalette sp = m_status->palette();
sp.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_status->setPalette(sp);
m_list = new QListWidget(this);
m_list->setFont(monospaceFont(11));
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(m_list, &QListWidget::currentTextChanged, this, [this](const QString &text) {
if (!text.isEmpty())
m_modelEdit->setText(text);
});
connect(m_list, &QListWidget::itemDoubleClicked, this, [this](QListWidgetItem *) { accept(); });
connect(m_fetchBtn, &QPushButton::clicked, this, [this] { fetchModels(); });
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *fetchRow = new QHBoxLayout;
fetchRow->setContentsMargins(0, 0, 0, 0);
fetchRow->setSpacing(8);
fetchRow->addWidget(m_fetchBtn);
fetchRow->addWidget(m_status, 1);
auto *root = new QVBoxLayout(this);
root->addWidget(new QLabel(tr("Model:"), this));
root->addWidget(m_modelEdit);
root->addLayout(fetchRow);
root->addWidget(m_list, 1);
root->addWidget(buttons);
fetchModels();
}
QString AgentModelDialog::selectedModel() const
{
return m_modelEdit->text().trimmed();
}
void AgentModelDialog::fetchModels()
{
if (!m_factory)
return;
if (m_watcher && m_watcher->isRunning())
return;
QString err;
Agent *probe = m_factory->create(m_agentName, this, &err);
if (!probe) {
m_status->setText(
err.isEmpty() ? tr("Provider is not available.")
: tr("Provider is not available: %1").arg(err));
return;
}
m_probe = probe;
if (!m_watcher) {
m_watcher = new QFutureWatcher<QList<QString>>(this);
connect(m_watcher, &QFutureWatcher<QList<QString>>::finished, this, [this] {
onModelsFetched();
});
}
m_fetchBtn->setEnabled(false);
m_status->setText(tr("Loading models…"));
m_watcher->setFuture(probe->installedModels());
}
void AgentModelDialog::onModelsFetched()
{
QList<QString> models;
if (m_watcher->future().resultCount() > 0)
models = m_watcher->result();
if (m_probe) {
m_probe->deleteLater();
m_probe.clear();
}
m_fetchBtn->setEnabled(true);
const QString keep = m_modelEdit->text();
{
QSignalBlocker block(m_list);
m_list->clear();
m_list->addItems(models);
}
m_modelEdit->setText(keep);
m_status->setText(
models.isEmpty() ? tr("No models returned — type the model name manually.")
: tr("%n model(s) available.", nullptr, static_cast<int>(models.size())));
}
} // namespace QodeAssist::Settings

View File

@@ -8,24 +8,21 @@
#include "AgentDuplicator.hpp"
#include "AgentListPane.hpp"
#include "SettingsConstants.hpp"
#include "SettingsTheme.hpp"
#include "TagFilterStrip.hpp"
#include <coreplugin/dialogs/ioptionspage.h>
#include <coreplugin/editormanager/editormanager.h>
#include <utils/filepath.h>
#include <utils/theme/theme.h>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFont>
#include <QFontMetrics>
#include <QFrame>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPalette>
#include <QPointer>
#include <QPushButton>
#include <QScrollArea>
@@ -76,13 +73,7 @@ public:
m_reload = new QPushButton(tr("Reload from disk"), this);
m_openUserDir = new QPushButton(tr("Open agents folder"), this);
m_userPathLabel = new QLabel(this);
m_userPathLabel->setFont(monospaceFont(11));
QPalette mutedPal = m_userPathLabel->palette();
mutedPal.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_userPathLabel->setPalette(mutedPal);
m_userPathLabel->setMaximumWidth(260);
m_openUserDir->setToolTip(QodeAssist::AgentFactory::userAgentsDir());
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
@@ -90,7 +81,6 @@ public:
headerRow->addWidget(m_titleLabel);
headerRow->addStretch(1);
headerRow->addWidget(m_reload);
headerRow->addWidget(m_userPathLabel);
headerRow->addWidget(m_openUserDir);
auto *headerSep = new QFrame(this);
@@ -98,6 +88,7 @@ public:
headerSep->setFrameShadow(QFrame::Sunken);
m_listPane = new AgentListPane(m_agentFactory, this);
m_listPane->tagStrip()->setMaxColumns(8);
m_detail = new AgentDetailPane(this);
m_detail->setInstanceFactory(m_agentFactory->instanceFactory());
@@ -119,6 +110,7 @@ public:
root->setSpacing(6);
root->addLayout(headerRow);
root->addWidget(headerSep);
root->addWidget(m_listPane->tagStrip());
root->addWidget(splitter, 1);
connect(m_reload, &QPushButton::clicked, this, &AgentsWidget::reloadFromDisk);
@@ -153,6 +145,8 @@ public:
reloadFromDisk();
QTimer::singleShot(0, this, [this] { m_listPane->focusFilter(); });
if (m_navigator) {
QTimer::singleShot(0, this, [this] {
if (!m_navigator)
@@ -170,18 +164,9 @@ private:
void reloadFromDisk()
{
m_agentFactory->reload();
updateUserPathLabel();
m_listPane->refresh();
}
void updateUserPathLabel()
{
const QString dir = QodeAssist::AgentFactory::userAgentsDir();
m_userPathLabel->setText(
QFontMetrics(m_userPathLabel->font()).elidedText(dir, Qt::ElideLeft, 256));
m_userPathLabel->setToolTip(dir);
}
void openAgentInEditor(const AgentConfig &agent)
{
const QString name = agent.name;
@@ -253,7 +238,6 @@ private:
QLabel *m_titleLabel = nullptr;
QPushButton *m_reload = nullptr;
QPushButton *m_openUserDir = nullptr;
QLabel *m_userPathLabel = nullptr;
AgentListPane *m_listPane = nullptr;
QScrollArea *m_detailScroll = nullptr;

View File

@@ -24,7 +24,7 @@ add_library(QodeAssistSettings STATIC
UpdateDialog.hpp UpdateDialog.cpp
AgentsSettingsPage.hpp AgentsSettingsPage.cpp
AgentDetailPane.hpp AgentDetailPane.cpp
AgentModelDialog.hpp AgentModelDialog.cpp
ModelSelector.hpp ModelSelector.cpp
AgentListItem.hpp AgentListItem.cpp
AgentListPane.hpp AgentListPane.cpp
AgentDuplicator.hpp AgentDuplicator.cpp

View File

@@ -8,6 +8,8 @@
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
#include <coreplugin/idocument.h>
#include <extensionsystem/pluginmanager.h>
#include <extensionsystem/pluginspec.h>
#include <utils/infolabel.h>
#include <utils/layoutbuilder.h>
#include <QFont>
@@ -43,6 +45,16 @@ namespace {
constexpr int kSaveDebounceMs = 300;
QString pluginVersionText()
{
const auto pluginSpecs = ExtensionSystem::PluginManager::plugins();
for (const ExtensionSystem::PluginSpec *spec : pluginSpecs) {
if (spec->name() == QLatin1String("QodeAssist"))
return QStringLiteral("QodeAssist %1").arg(spec->version());
}
return QStringLiteral("QodeAssist");
}
class AgentPipelinesWidget : public QWidget
{
Q_OBJECT
@@ -261,12 +273,13 @@ GeneralSettings::GeneralSettings()
requestTimeout.setSettingsKey(Constants::REQUEST_TIMEOUT);
requestTimeout.setLabelText(Tr::tr("Request timeout (seconds):"));
requestTimeout.setToolTip(Tr::tr(
"Maximum time to wait for the model to send data before a request is aborted. "
"Applies to all requests — chat, code completion, quick refactor and chat compression. "
"The timer resets every time data is received, so this effectively limits the "
"time-to-first-token and any stall between tokens. Increase it for slow or local "
"models that need a long time to start responding. Set to 0 to disable the timeout."));
requestTimeout.setToolTip(
Tr::tr(
"Maximum time to wait for the model to send data before a request is aborted. "
"Applies to all requests — chat, code completion, quick refactor and chat compression. "
"The timer resets every time data is received, so this effectively limits the "
"time-to-first-token and any stall between tokens. Increase it for slow or local "
"models that need a long time to start responding. Set to 0 to disable the timeout."));
requestTimeout.setRange(0, 3600);
requestTimeout.setDefaultValue(120);
@@ -282,9 +295,15 @@ GeneralSettings::GeneralSettings()
setLayouter([this]() {
using namespace Layouting;
auto networkGroup = Group{
title(Tr::tr("Network")),
Column{Row{requestTimeout, Stretch{1}}}};
auto *versionLabel = new QLabel(pluginVersionText());
versionLabel->setEnabled(false);
auto advancedGroup = Group{
title(Tr::tr("Advanced")),
Column{
Row{enableLogging, Stretch{1}},
Row{enableCheckUpdate, Stretch{1}},
Row{requestTimeout, Stretch{1}}}};
auto *supportLabel = new QLabel(Tr::tr("Support the development of QodeAssist:"));
@@ -307,16 +326,13 @@ GeneralSettings::GeneralSettings()
};
return Column{
Row{supportLabel, supportLinks, Stretch{1}, checkUpdate, resetToDefaults},
Space{8},
Row{enableQodeAssist, Stretch{1}},
Row{enableLogging, Stretch{1}},
Row{enableCheckUpdate, Stretch{1}},
Space{8},
networkGroup,
Row{enableQodeAssist, Stretch{1}, versionLabel, checkUpdate, resetToDefaults},
Space{12},
pipelines,
Stretch{1}};
Space{12},
advancedGroup,
Stretch{1},
Row{supportLabel, supportLinks, Stretch{1}}};
});
}

176
settings/ModelSelector.cpp Normal file
View File

@@ -0,0 +1,176 @@
// 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 "ModelSelector.hpp"
#include "SettingsTheme.hpp"
#include <Agent.hpp>
#include <AgentFactory.hpp>
#include <QAbstractItemView>
#include <QCompleter>
#include <QLineEdit>
#include <QSignalBlocker>
#include <QStandardItemModel>
namespace QodeAssist::Settings {
ModelSelector::ModelSelector(QWidget *parent)
: QComboBox(parent)
{
setEditable(true);
setInsertPolicy(QComboBox::NoInsert);
setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLengthWithIcon);
setMinimumContentsLength(24);
setFont(monospaceFont(11));
lineEdit()->setPlaceholderText(tr("(set a model)"));
lineEdit()->setClearButtonEnabled(true);
setEnabled(false);
auto *completer = new QCompleter(model(), this);
completer->setFilterMode(Qt::MatchContains);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setCompletionMode(QCompleter::PopupCompletion);
setCompleter(completer);
connect(this, &QComboBox::activated, this, [this](int) { submitCurrent(); });
connect(lineEdit(), &QLineEdit::returnPressed, this, [this] { submitCurrent(); });
connect(
completer,
QOverload<const QString &>::of(&QCompleter::activated),
this,
[this](const QString &text) {
setEditText(text);
submitCurrent();
},
Qt::QueuedConnection);
}
void ModelSelector::setAgent(
AgentFactory *factory,
const QString &agentName,
const QString &providerInstance,
const QString &model)
{
const bool sameTarget = factory == m_factory && agentName == m_agentName
&& providerInstance == m_providerInstance;
m_factory = factory;
m_agentName = agentName;
m_providerInstance = providerInstance;
m_model = model;
if (!sameTarget) {
m_fetched = false;
{
QSignalBlocker block(this);
clear();
}
emit statusChanged(QString());
}
{
QSignalBlocker block(this);
setCurrentIndex(findText(model));
setEditText(model);
}
setEnabled(m_factory != nullptr);
}
void ModelSelector::clearAgent()
{
m_factory = nullptr;
m_agentName.clear();
m_providerInstance.clear();
m_model.clear();
m_fetched = false;
{
QSignalBlocker block(this);
clear();
clearEditText();
}
setEnabled(false);
emit statusChanged(QString());
}
void ModelSelector::showPopup()
{
if (!m_fetched && m_factory && !(m_watcher && m_watcher->isRunning()))
startFetch();
QComboBox::showPopup();
}
void ModelSelector::startFetch()
{
QString err;
Agent *probe = m_factory->create(m_agentName, this, &err);
if (!probe) {
emit statusChanged(
err.isEmpty() ? tr("Provider is not available.")
: tr("Provider is not available: %1").arg(err));
return;
}
m_probe = probe;
m_fetchAgent = m_agentName;
m_fetchProvider = m_providerInstance;
if (!m_watcher) {
m_watcher = new QFutureWatcher<QList<QString>>(this);
connect(m_watcher, &QFutureWatcher<QList<QString>>::finished, this, [this] { onFetched(); });
}
const QString keep = currentText();
{
QSignalBlocker block(this);
clear();
addItem(tr("Loading models…"));
if (auto *itemModel = qobject_cast<QStandardItemModel *>(model()))
itemModel->item(0)->setEnabled(false);
setCurrentIndex(-1);
setEditText(keep);
}
emit statusChanged(tr("Loading models…"));
m_watcher->setFuture(probe->installedModels());
}
void ModelSelector::onFetched()
{
QList<QString> models;
if (m_watcher->future().resultCount() > 0)
models = m_watcher->result();
if (m_probe) {
m_probe->deleteLater();
m_probe.clear();
}
if (m_fetchAgent != m_agentName || m_fetchProvider != m_providerInstance)
return;
m_fetched = true;
const QString keep = currentText();
const bool popupOpen = view() && view()->isVisible();
{
QSignalBlocker block(this);
clear();
addItems(models);
setCurrentIndex(findText(keep));
setEditText(keep);
}
emit statusChanged(
models.isEmpty() ? tr("No models returned — type the model name manually.")
: tr("%n model(s) available.", nullptr, static_cast<int>(models.size())));
if (popupOpen) {
hidePopup();
QComboBox::showPopup();
}
}
void ModelSelector::submitCurrent()
{
const QString model = currentText().trimmed();
if (model.isEmpty() || model == m_model)
return;
emit modelSubmitted(model);
}
} // namespace QodeAssist::Settings

View File

@@ -4,52 +4,53 @@
#pragma once
#include <QDialog>
#include <QComboBox>
#include <QFutureWatcher>
#include <QList>
#include <QPointer>
#include <QString>
QT_BEGIN_NAMESPACE
class QLabel;
class QLineEdit;
class QListWidget;
class QPushButton;
QT_END_NAMESPACE
namespace QodeAssist {
class AgentFactory;
class Agent;
class AgentFactory;
} // namespace QodeAssist
namespace QodeAssist::Settings {
class AgentModelDialog : public QDialog
class ModelSelector : public QComboBox
{
Q_OBJECT
public:
AgentModelDialog(
explicit ModelSelector(QWidget *parent = nullptr);
void setAgent(
AgentFactory *factory,
const QString &agentName,
const QString &currentModel,
QWidget *parent = nullptr);
const QString &providerInstance,
const QString &model);
void clearAgent();
[[nodiscard]] QString selectedModel() const;
signals:
void modelSubmitted(const QString &model);
void statusChanged(const QString &status);
protected:
void showPopup() override;
private:
void fetchModels();
void onModelsFetched();
AgentFactory *m_factory = nullptr;
QString m_agentName;
QLineEdit *m_modelEdit = nullptr;
QListWidget *m_list = nullptr;
QLabel *m_status = nullptr;
QPushButton *m_fetchBtn = nullptr;
void startFetch();
void onFetched();
void submitCurrent();
QPointer<AgentFactory> m_factory;
QPointer<Agent> m_probe;
QFutureWatcher<QList<QString>> *m_watcher = nullptr;
QString m_agentName;
QString m_providerInstance;
QString m_model;
QString m_fetchAgent;
QString m_fetchProvider;
bool m_fetched = false;
};
} // namespace QodeAssist::Settings

View File

@@ -36,6 +36,9 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
nf.setPixelSize(15);
m_nameLabel->setFont(nf);
m_sourceBadge = new QLabel(this);
m_sourceBadge->setVisible(false);
m_sourcePathLabel = new QLabel(this);
m_sourcePathLabel->setFont(monospaceFont(11));
QPalette spp = m_sourcePathLabel->palette();
@@ -66,11 +69,10 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
setEditing(false);
populate(m_current, m_currentHasStoredKey);
});
connect(m_saveBtn, &QPushButton::clicked, this, [this] {
emit saveRequested(collectEdits());
connect(m_saveBtn, &QPushButton::clicked, this, [this] { emit saveRequested(collectEdits()); });
connect(m_openInEditorBtn, &QPushButton::clicked, this, [this] {
emit openInEditorRequested(m_current.sourcePath);
});
connect(m_openInEditorBtn, &QPushButton::clicked, this,
[this] { emit openInEditorRequested(m_current.sourcePath); });
connect(m_dupBtn, &QPushButton::clicked, this, [this] { emit duplicateRequested(); });
connect(m_deleteBtn, &QPushButton::clicked, this, [this] { emit deleteRequested(); });
@@ -88,6 +90,7 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
titleRow->setContentsMargins(0, 0, 0, 0);
titleRow->setSpacing(8);
titleRow->addWidget(m_nameLabel);
titleRow->addWidget(m_sourceBadge, 0, Qt::AlignVCenter);
titleRow->addStretch(1);
auto *headerLeft = new QVBoxLayout;
@@ -111,7 +114,7 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
m_descriptionLabel->setWordWrap(true);
m_descriptionLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
auto *identitySection = new SectionBox(tr("Identity"), this);
m_identitySection = new SectionBox(tr("Identity"), this);
m_nameEdit = new QLineEdit(this);
m_descriptionEdit = new QPlainTextEdit(this);
m_descriptionEdit->setMaximumHeight(60);
@@ -121,29 +124,12 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
identityGrid->setHorizontalSpacing(8);
identityGrid->setVerticalSpacing(4);
FormBuilder(identityGrid).row(tr("Name:"), m_nameEdit).row(tr("Description:"), m_descriptionEdit);
identitySection->bodyLayout()->addLayout(identityGrid);
m_identitySection->bodyLayout()->addLayout(identityGrid);
m_identitySection->setVisible(false);
auto *endpointSection = new SectionBox(tr("Endpoint"), this);
auto *configSection = new SectionBox(tr("Configuration"), this);
m_urlEdit = new QLineEdit(this);
m_urlEdit->setFont(monospaceFont(11));
auto *endpointGrid = new QGridLayout;
endpointGrid->setContentsMargins(0, 0, 0, 0);
endpointGrid->setHorizontalSpacing(8);
endpointGrid->setVerticalSpacing(4);
FormBuilder(endpointGrid).row(tr("URL:"), m_urlEdit,
tr("Base URL. Agents append their endpoint path "
"(e.g. /chat/completions) to this."));
endpointSection->bodyLayout()->addLayout(endpointGrid);
m_samplePreview = new QLabel(this);
m_samplePreview->setFont(monospaceFont(11));
m_samplePreview->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_samplePreview->setWordWrap(true);
m_samplePreview->setContentsMargins(6, 4, 6, 4);
m_samplePreview->setAutoFillBackground(true);
endpointSection->bodyLayout()->addWidget(m_samplePreview);
auto *credSection = new SectionBox(tr("Credentials"), this);
m_apiKeyEdit = new QLineEdit(this);
m_apiKeyEdit->setEchoMode(QLineEdit::Password);
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
@@ -182,8 +168,7 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
emit apiKeySaveRequested(key);
m_apiKeyEdit->clear();
});
connect(m_apiKeyClearBtn, &QPushButton::clicked, this,
[this] { emit apiKeyClearRequested(); });
connect(m_apiKeyClearBtn, &QPushButton::clicked, this, [this] { emit apiKeyClearRequested(); });
m_keyHint = makeHintLabel(QString{}, this);
auto *keyRow = new QHBoxLayout;
@@ -194,15 +179,24 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
keyRow->addWidget(m_apiKeySaveBtn);
keyRow->addWidget(m_apiKeyClearBtn);
auto *credGrid = new QGridLayout;
credGrid->setContentsMargins(0, 0, 0, 0);
credGrid->setHorizontalSpacing(8);
credGrid->setVerticalSpacing(4);
FormBuilder credForm(credGrid);
credForm.row(tr("API key:"), keyRow);
credGrid->addWidget(m_legacyKeyBtn, credForm.currentRow(), 1, Qt::AlignLeft);
credGrid->addWidget(m_keyHint, credForm.currentRow() + 1, 1);
credSection->bodyLayout()->addLayout(credGrid);
auto *configGrid = new QGridLayout;
configGrid->setContentsMargins(0, 0, 0, 0);
configGrid->setHorizontalSpacing(8);
configGrid->setVerticalSpacing(4);
FormBuilder configForm(configGrid);
configForm.row(tr("API key:"), keyRow);
{
int row = configForm.currentRow();
configGrid->addWidget(m_legacyKeyBtn, row, 1, Qt::AlignLeft);
configGrid->addWidget(m_keyHint, row + 1, 1);
configForm = FormBuilder(configGrid, row + 2);
}
configForm.row(
tr("URL:"),
m_urlEdit,
tr("Base URL. Agents append their endpoint path "
"(e.g. /chat/completions) to this. Editable via Edit…"));
configSection->bodyLayout()->addLayout(configGrid);
m_launchSection = new SectionBox(tr("Launch"), this);
m_launchEmptyHint = new QLabel(this);
@@ -218,12 +212,15 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
m_startBtn = new QPushButton(tr("Start"), this);
m_stopBtn = new QPushButton(tr("Stop"), this);
m_restartBtn = new QPushButton(tr("Restart"), this);
connect(m_startBtn, &QPushButton::clicked, this,
[this] { emit launchStartRequested(m_current.name); });
connect(m_stopBtn, &QPushButton::clicked, this,
[this] { emit launchStopRequested(m_current.name); });
connect(m_restartBtn, &QPushButton::clicked, this,
[this] { emit launchRestartRequested(m_current.name); });
connect(m_startBtn, &QPushButton::clicked, this, [this] {
emit launchStartRequested(m_current.name);
});
connect(m_stopBtn, &QPushButton::clicked, this, [this] {
emit launchStopRequested(m_current.name);
});
connect(m_restartBtn, &QPushButton::clicked, this, [this] {
emit launchRestartRequested(m_current.name);
});
auto *launchBtnRow = new QHBoxLayout;
launchBtnRow->setContentsMargins(0, 0, 0, 0);
launchBtnRow->setSpacing(6);
@@ -283,9 +280,8 @@ ProviderDetailPane::ProviderDetailPane(QWidget *parent)
root->addLayout(headerRow);
root->addWidget(headerSep);
root->addWidget(m_descriptionLabel);
root->addWidget(identitySection);
root->addWidget(endpointSection);
root->addWidget(credSection);
root->addWidget(m_identitySection);
root->addWidget(configSection);
root->addWidget(m_launchSection);
root->addWidget(m_rawToggle, 0, Qt::AlignLeft);
root->addWidget(m_rawToml);
@@ -302,6 +298,9 @@ void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool
const bool needsKey = !inst.apiKeyRef.isEmpty();
m_nameLabel->setText(inst.name);
m_sourceBadge->setText(isUser ? tr("User") : tr("Bundled"));
m_sourceBadge->setVisible(true);
styleSourceBadge(m_sourceBadge, isUser);
m_sourcePathLabel->setText(inst.sourcePath);
m_descriptionLabel->setText(
@@ -323,8 +322,9 @@ void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool
m_keyHint->setText(tr("This provider type does not use a key."));
} else if (hasStoredKey) {
m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it."));
m_keyHint->setText(tr("A key is stored. Type a new key and press Save key to "
"replace it, or Clear to erase it."));
m_keyHint->setText(
tr("A key is stored. Type a new key and press Save key to "
"replace it, or Clear to erase it."));
} else {
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
m_keyHint->setText(tr("No key stored yet. Type a key and press Save key."));
@@ -341,10 +341,6 @@ void ProviderDetailPane::populate(const Providers::ProviderInstance &inst, bool
m_legacyKeyBtn->setVisible(false);
}
m_samplePreview->setText(
QStringLiteral("# sample request line\nPOST %1/<agent endpoint>").arg(inst.url));
applyPreviewPalette();
m_deleteBtn->setEnabled(isUser);
m_dupBtn->setEnabled(true);
m_editBtn->setVisible(isUser);
@@ -360,6 +356,7 @@ void ProviderDetailPane::clear()
{
m_current = {};
m_nameLabel->setText(tr("Select a provider"));
m_sourceBadge->setVisible(false);
m_sourcePathLabel->clear();
m_descriptionLabel->clear();
m_nameEdit->clear();
@@ -373,7 +370,6 @@ void ProviderDetailPane::clear()
m_revealKeyBtn->setEnabled(false);
m_legacyKeyValue.clear();
m_legacyKeyBtn->setVisible(false);
m_samplePreview->clear();
m_rawToml->clear();
m_editBtn->setVisible(false);
m_dupBtn->setEnabled(false);
@@ -390,8 +386,9 @@ void ProviderDetailPane::refreshKeyStatus(bool hasStoredKey)
return;
if (hasStoredKey) {
m_apiKeyEdit->setPlaceholderText(tr("Stored — enter a new key to replace it."));
m_keyHint->setText(tr("A key is stored. Type a new key and press Save key to "
"replace it, or Clear to erase it."));
m_keyHint->setText(
tr("A key is stored. Type a new key and press Save key to "
"replace it, or Clear to erase it."));
} else {
m_apiKeyEdit->setPlaceholderText(tr("Enter API key…"));
m_keyHint->setText(tr("No key stored yet. Type a key and press Save key."));
@@ -412,11 +409,11 @@ void ProviderDetailPane::setLaunchState(
m_launchTerminalToggle->setVisible(hasLaunch);
if (!hasLaunch) {
m_launchEmptyHint->setText(tr(
"No [launch] block. This provider is treated as external — "
"the plugin will not spawn or supervise any process. "
"Add a [launch] block to the TOML to have the plugin manage "
"a local server here."));
m_launchEmptyHint->setText(
tr("No [launch] block. This provider is treated as external — "
"the plugin will not spawn or supervise any process. "
"Add a [launch] block to the TOML to have the plugin manage "
"a local server here."));
m_launchCmdLabel->clear();
m_launchTerminal->clearContents();
return;
@@ -429,22 +426,31 @@ void ProviderDetailPane::setLaunchState(
Utils::creatorColor(Utils::Theme::PanelTextColorMid).name(),
tr("(detached — survives Qt Creator restart)"))
: QString();
m_launchCmdLabel->setText(
QStringLiteral("<b>%1</b> %2%3")
.arg(m_current.launch.command.toHtmlEscaped(),
m_current.launch.args.join(QLatin1Char(' ')).toHtmlEscaped(),
detachedNote));
m_launchCmdLabel->setText(QStringLiteral("<b>%1</b> %2%3")
.arg(
m_current.launch.command.toHtmlEscaped(),
m_current.launch.args.join(QLatin1Char(' ')).toHtmlEscaped(),
detachedNote));
QString statusText;
switch (st) {
case Providers::ProviderLauncher::Idle: statusText = tr("idle"); break;
case Providers::ProviderLauncher::Starting: statusText = tr("starting…"); break;
case Providers::ProviderLauncher::Probing: statusText = tr("probing…"); break;
case Providers::ProviderLauncher::Ready: statusText = tr("ready"); break;
case Providers::ProviderLauncher::Stopping: statusText = tr("stopping…"); break;
case Providers::ProviderLauncher::Idle:
statusText = tr("idle");
break;
case Providers::ProviderLauncher::Starting:
statusText = tr("starting…");
break;
case Providers::ProviderLauncher::Probing:
statusText = tr("probing…");
break;
case Providers::ProviderLauncher::Ready:
statusText = tr("ready");
break;
case Providers::ProviderLauncher::Stopping:
statusText = tr("stopping…");
break;
case Providers::ProviderLauncher::Failed:
statusText = lastError.isEmpty() ? tr("failed")
: tr("failed — %1").arg(lastError);
statusText = lastError.isEmpty() ? tr("failed") : tr("failed — %1").arg(lastError);
break;
}
m_launchStatusPill->setText(statusText);
@@ -473,8 +479,9 @@ void ProviderDetailPane::changeEvent(QEvent *event)
{
QWidget::changeEvent(event);
if (event->type() == QEvent::PaletteChange || event->type() == QEvent::StyleChange) {
applyPreviewPalette();
applyTerminalPalette();
if (!m_current.name.isEmpty())
styleSourceBadge(m_sourceBadge, m_current.isUserSource());
}
}
@@ -491,6 +498,7 @@ void ProviderDetailPane::showRevealedKey(const QString &key)
void ProviderDetailPane::setEditing(bool on)
{
m_editing = on;
m_identitySection->setVisible(on);
m_nameEdit->setReadOnly(!on);
m_descriptionEdit->setReadOnly(!on);
m_urlEdit->setReadOnly(!on);
@@ -510,15 +518,6 @@ Providers::ProviderInstance ProviderDetailPane::collectEdits() const
return out;
}
void ProviderDetailPane::applyPreviewPalette()
{
m_samplePreview->setStyleSheet(
QStringLiteral("QLabel { background:%1; border:1px solid %2; }")
.arg(
cssColor(Utils::creatorColor(Utils::Theme::BackgroundColorNormal)),
cssColor(Utils::creatorColor(Utils::Theme::SplitterColor))));
}
void ProviderDetailPane::applyTerminalPalette()
{
if (!m_launchTerminal)

View File

@@ -57,7 +57,6 @@ protected:
private:
void setEditing(bool on);
Providers::ProviderInstance collectEdits() const;
void applyPreviewPalette();
void applyTerminalPalette();
bool m_editing = false;
@@ -65,6 +64,7 @@ private:
Providers::ProviderInstance m_current;
QLabel *m_nameLabel = nullptr;
QLabel *m_sourceBadge = nullptr;
QLabel *m_sourcePathLabel = nullptr;
QPushButton *m_editBtn = nullptr;
QPushButton *m_openInEditorBtn = nullptr;
@@ -75,11 +75,11 @@ private:
QLabel *m_descriptionLabel = nullptr;
SectionBox *m_identitySection = nullptr;
QLineEdit *m_nameEdit = nullptr;
QLabel *m_protocolLabel = nullptr;
QPlainTextEdit *m_descriptionEdit = nullptr;
QLineEdit *m_urlEdit = nullptr;
QLabel *m_samplePreview = nullptr;
QLineEdit *m_apiKeyEdit = nullptr;
QToolButton *m_revealKeyBtn = nullptr;

View File

@@ -4,21 +4,22 @@
#include "ProviderListItem.hpp"
#include <utils/elidinglabel.h>
#include <utils/theme/theme.h>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QScopedValueRollback>
#include <QVBoxLayout>
#include <QStringList>
#include "ProviderInstance.hpp"
#include "SettingsTheme.hpp"
#include "SettingsUiBuilders.hpp"
namespace QodeAssist::Settings {
ProviderListItem::ProviderListItem(
const Providers::ProviderInstance &inst, QWidget *parent)
ProviderListItem::ProviderListItem(const Providers::ProviderInstance &inst, QWidget *parent)
: QFrame(parent)
, m_name(inst.name)
{
@@ -27,35 +28,45 @@ ProviderListItem::ProviderListItem(
setAutoFillBackground(true);
setCursor(Qt::PointingHandCursor);
auto *headerRow = new QHBoxLayout;
headerRow->setContentsMargins(0, 0, 0, 0);
headerRow->setSpacing(6);
m_statusDot = new QLabel(QStringLiteral(""), this);
QFont df = m_statusDot->font();
df.setPixelSize(11);
m_statusDot->setFont(df);
m_statusDot->setStyleSheet(QStringLiteral("color: %1;").arg(statusColor(Status::Unknown)));
m_nameLabel = new QLabel(inst.name, this);
m_nameLabel = new QLabel(this);
QFont nf = m_nameLabel->font();
nf.setBold(true);
nf.setPixelSize(12);
m_nameLabel->setFont(nf);
headerRow->addWidget(m_statusDot, 0, Qt::AlignVCenter);
headerRow->addWidget(m_nameLabel, 1);
m_nameLabel->setTextFormat(Qt::RichText);
m_urlLabel = new QLabel(inst.url, this);
m_urlLabel = new Utils::ElidingLabel(this);
m_urlLabel->setElideMode(Qt::ElideMiddle);
m_urlLabel->setFont(monospaceFont(10));
m_urlLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
QPalette up = m_urlLabel->palette();
up.setColor(QPalette::WindowText, Utils::creatorColor(Utils::Theme::PanelTextColorMid));
m_urlLabel->setPalette(up);
m_urlLabel->setContentsMargins(17, 0, 0, 0);
m_urlLabel->setText(inst.url);
m_urlLabel->setVisible(!inst.url.isEmpty());
auto *outer = new QVBoxLayout(this);
outer->setContentsMargins(5, 6, 8, 6);
outer->setSpacing(2);
outer->addLayout(headerRow);
outer->addWidget(m_urlLabel);
auto *row = new QHBoxLayout(this);
row->setContentsMargins(8, 4, 8, 4);
row->setSpacing(6);
row->addWidget(m_statusDot, 0, Qt::AlignVCenter);
row->addWidget(m_nameLabel, 0);
row->addWidget(m_urlLabel, 1);
QStringList tipLines;
if (!inst.description.isEmpty())
tipLines << inst.description;
tipLines << tr("API: %1").arg(inst.clientApi);
if (!inst.url.isEmpty())
tipLines << tr("URL: %1").arg(inst.url);
setToolTip(tipLines.join(QStringLiteral("\n")));
updateNameText();
applyTheme();
}
@@ -73,6 +84,19 @@ void ProviderListItem::setSelected(bool s)
applyTheme();
}
void ProviderListItem::setFilterHighlight(const QString &lowerFilter)
{
if (m_filter == lowerFilter)
return;
m_filter = lowerFilter;
updateNameText();
}
void ProviderListItem::updateNameText()
{
m_nameLabel->setText(filterHighlightedHtml(m_name, m_filter));
}
void ProviderListItem::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
@@ -111,6 +135,7 @@ void ProviderListItem::applyTheme()
"#ProvListItem { background:transparent;"
" border-top:1px solid %1; border-left:3px solid %2; }")
.arg(cssColor(Utils::creatorColor(Utils::Theme::SplitterColor)), accent));
updateNameText();
}
} // namespace QodeAssist::Settings

View File

@@ -9,6 +9,10 @@
class QLabel;
class QMouseEvent;
namespace Utils {
class ElidingLabel;
}
namespace QodeAssist::Providers {
struct ProviderInstance;
}
@@ -21,11 +25,11 @@ class ProviderListItem : public QFrame
public:
enum class Status : int { Unknown, Ok, Fail };
explicit ProviderListItem(
const Providers::ProviderInstance &inst, QWidget *parent = nullptr);
explicit ProviderListItem(const Providers::ProviderInstance &inst, QWidget *parent = nullptr);
void setStatus(Status s);
void setSelected(bool s);
void setFilterHighlight(const QString &lowerFilter);
QString providerName() const { return m_name; }
signals:
@@ -38,14 +42,16 @@ protected:
private:
static QString statusColor(Status s);
void applyTheme();
void updateNameText();
QString m_name;
QString m_filter;
Status m_status = Status::Unknown;
bool m_selected = false;
bool m_inApplyTheme = false;
QLabel *m_statusDot = nullptr;
QLabel *m_nameLabel = nullptr;
QLabel *m_urlLabel = nullptr;
Utils::ElidingLabel *m_urlLabel = nullptr;
};
} // namespace QodeAssist::Settings

View File

@@ -20,6 +20,7 @@
#include <QFrame>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QKeyEvent>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
@@ -103,7 +104,10 @@ public:
headerSep->setFrameShadow(QFrame::Sunken);
m_filterEdit = new QLineEdit(this);
m_filterEdit->setPlaceholderText(tr("Filter providers"));
m_filterEdit->setPlaceholderText(tr("Filter by name, API, URL"));
m_filterEdit->setClearButtonEnabled(true);
m_filterEdit->setToolTip(tr("Type to filter; Up/Down moves through the list."));
m_filterEdit->installEventFilter(this);
m_listScroll = new QScrollArea(this);
m_listScroll->setWidgetResizable(true);
@@ -127,55 +131,88 @@ public:
leftLay->addWidget(m_listScroll, 1);
m_detailPane = new ProviderDetailPane(this);
connect(m_detailPane, &ProviderDetailPane::saveRequested,
this, &ProvidersPageWidget::onSaveEdited);
connect(m_detailPane, &ProviderDetailPane::duplicateRequested,
this, &ProvidersPageWidget::onDuplicateClicked);
connect(m_detailPane, &ProviderDetailPane::deleteRequested,
this, &ProvidersPageWidget::onRemoveClicked);
connect(m_detailPane, &ProviderDetailPane::apiKeySaveRequested,
this, &ProvidersPageWidget::onApiKeySave);
connect(m_detailPane, &ProviderDetailPane::apiKeyClearRequested,
this, &ProvidersPageWidget::onApiKeyClear);
connect(
m_detailPane,
&ProviderDetailPane::saveRequested,
this,
&ProvidersPageWidget::onSaveEdited);
connect(
m_detailPane,
&ProviderDetailPane::duplicateRequested,
this,
&ProvidersPageWidget::onDuplicateClicked);
connect(
m_detailPane,
&ProviderDetailPane::deleteRequested,
this,
&ProvidersPageWidget::onRemoveClicked);
connect(
m_detailPane,
&ProviderDetailPane::apiKeySaveRequested,
this,
&ProvidersPageWidget::onApiKeySave);
connect(
m_detailPane,
&ProviderDetailPane::apiKeyClearRequested,
this,
&ProvidersPageWidget::onApiKeyClear);
connect(
m_detailPane,
&ProviderDetailPane::apiKeyRevealRequested,
this,
&ProvidersPageWidget::onApiKeyReveal);
connect(m_detailPane, &ProviderDetailPane::launchStartRequested,
this, &ProvidersPageWidget::onLaunchStart);
connect(m_detailPane, &ProviderDetailPane::launchStopRequested,
this, &ProvidersPageWidget::onLaunchStop);
connect(m_detailPane, &ProviderDetailPane::launchRestartRequested,
this, &ProvidersPageWidget::onLaunchRestart);
connect(m_detailPane, &ProviderDetailPane::openInEditorRequested,
this, [this](const QString &path) {
if (path.isEmpty() || path.startsWith(QLatin1String(":/"))) {
QMessageBox::information(
this, tr("Open in editor"),
tr("Bundled providers are read-only. "
"Use Duplicate to create an editable user copy first."));
return;
}
Core::EditorManager::openEditor(Utils::FilePath::fromString(path));
});
connect(
m_detailPane,
&ProviderDetailPane::launchStartRequested,
this,
&ProvidersPageWidget::onLaunchStart);
connect(
m_detailPane,
&ProviderDetailPane::launchStopRequested,
this,
&ProvidersPageWidget::onLaunchStop);
connect(
m_detailPane,
&ProviderDetailPane::launchRestartRequested,
this,
&ProvidersPageWidget::onLaunchRestart);
connect(
m_detailPane,
&ProviderDetailPane::openInEditorRequested,
this,
[this](const QString &path) {
if (path.isEmpty() || path.startsWith(QLatin1String(":/"))) {
QMessageBox::information(
this,
tr("Open in editor"),
tr("Bundled providers are read-only. "
"Use Duplicate to create an editable user copy first."));
return;
}
Core::EditorManager::openEditor(Utils::FilePath::fromString(path));
});
if (m_launcher) {
connect(m_launcher.data(), &Providers::ProviderLauncher::stateChanged,
this, [this](const QString &name,
Providers::ProviderLauncher::State newState) {
if (name == m_currentName)
refreshDetailLaunch();
const ProviderListItem::Status status = rowStatusFromState(newState);
for (auto *row : m_rows) {
if (row->providerName() == name)
row->setStatus(status);
}
});
connect(m_launcher.data(), &Providers::ProviderLauncher::bytesReceived,
this, [this](const QString &name, const QByteArray &chunk) {
if (name == m_currentName)
m_detailPane->appendLaunchBytes(chunk);
});
connect(
m_launcher.data(),
&Providers::ProviderLauncher::stateChanged,
this,
[this](const QString &name, Providers::ProviderLauncher::State newState) {
if (name == m_currentName)
refreshDetailLaunch();
const ProviderListItem::Status status = rowStatusFromState(newState);
for (auto *row : m_rows) {
if (row->providerName() == name)
row->setStatus(status);
}
});
connect(
m_launcher.data(),
&Providers::ProviderLauncher::bytesReceived,
this,
[this](const QString &name, const QByteArray &chunk) {
if (name == m_currentName)
m_detailPane->appendLaunchBytes(chunk);
});
}
m_detailScroll = new QScrollArea(this);
m_detailScroll->setWidgetResizable(true);
@@ -199,26 +236,34 @@ public:
m_filterDebounce = new QTimer(this);
m_filterDebounce->setSingleShot(true);
m_filterDebounce->setInterval(100);
connect(m_filterDebounce, &QTimer::timeout, this,
&ProvidersPageWidget::rebuildList);
connect(m_filterEdit, &QLineEdit::textChanged, this,
[this](const QString &) { m_filterDebounce->start(); });
connect(m_filterDebounce, &QTimer::timeout, this, &ProvidersPageWidget::rebuildList);
connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &) {
m_filterDebounce->start();
});
if (m_factory) {
connect(m_factory.data(),
&Providers::ProviderInstanceFactory::instancesReloaded,
this, &ProvidersPageWidget::rebuildList);
connect(
m_factory.data(),
&Providers::ProviderInstanceFactory::instancesReloaded,
this,
&ProvidersPageWidget::rebuildList);
}
if (m_navigator) {
connect(m_navigator.data(),
&ProvidersPageNavigator::selectInstanceRequested,
this, &ProvidersPageWidget::selectInstance);
connect(
m_navigator.data(),
&ProvidersPageNavigator::selectInstanceRequested,
this,
&ProvidersPageWidget::selectInstance);
}
rebuildList();
const QString pending
= m_navigator ? m_navigator->takePendingSelection() : QString{};
QTimer::singleShot(0, this, [this] {
m_filterEdit->setFocus(Qt::OtherFocusReason);
m_filterEdit->selectAll();
});
const QString pending = m_navigator ? m_navigator->takePendingSelection() : QString{};
if (!pending.isEmpty())
selectInstance(pending);
else if (m_factory && !m_factory->instances().empty())
@@ -227,6 +272,23 @@ public:
void apply() final {}
protected:
bool eventFilter(QObject *watched, QEvent *event) override
{
if (watched == m_filterEdit && 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 Core::IOptionsPageWidget::eventFilter(watched, event);
}
private slots:
void rebuildList()
{
@@ -245,9 +307,9 @@ private slots:
auto matches = [&](const Providers::ProviderInstance &inst) {
if (filter.isEmpty())
return true;
return inst.name.toLower().contains(filter)
|| inst.clientApi.toLower().contains(filter)
|| inst.url.toLower().contains(filter);
return inst.name.toLower().contains(filter) || inst.clientApi.toLower().contains(filter)
|| inst.url.toLower().contains(filter)
|| inst.description.toLower().contains(filter);
};
auto addSection = [&](const QString &title, bool userSection) {
@@ -262,17 +324,18 @@ private slots:
continue;
sorted.push_back(&inst);
}
std::sort(sorted.begin(), sorted.end(),
[](const Providers::ProviderInstance *a,
const Providers::ProviderInstance *b) {
return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
});
std::sort(
sorted.begin(),
sorted.end(),
[](const Providers::ProviderInstance *a, const Providers::ProviderInstance *b) {
return a->name.compare(b->name, Qt::CaseInsensitive) < 0;
});
int shown = 0;
for (const auto *inst : sorted) {
auto *row = new ProviderListItem(*inst, m_listContent);
connect(row, &ProviderListItem::clicked,
this, &ProvidersPageWidget::selectInstance);
row->setFilterHighlight(filter);
connect(row, &ProviderListItem::clicked, this, &ProvidersPageWidget::selectInstance);
if (m_launcher)
row->setStatus(rowStatusFromLauncher(inst->name));
m_rows.append(row);
@@ -281,8 +344,7 @@ private slots:
}
if (shown == 0) {
auto *empty = new QLabel(
userSection ? tr("No user instances yet.")
: tr("No bundled instances loaded."),
userSection ? tr("No user instances yet.") : tr("No bundled instances loaded."),
m_listContent);
empty->setContentsMargins(10, 6, 10, 6);
QPalette ep = empty->palette();
@@ -322,21 +384,25 @@ private slots:
{
if (!m_factory || m_currentName.isEmpty())
return;
const Providers::ProviderInstance *srcPtr
= m_factory->instanceByName(m_currentName);
const Providers::ProviderInstance *srcPtr = m_factory->instanceByName(m_currentName);
if (!srcPtr)
return;
const Providers::ProviderInstance srcCopy = *srcPtr;
bool ok = false;
const QString name = QInputDialog::getText(
this, tr("Duplicate provider"),
tr("Name for the new provider:"), QLineEdit::Normal,
QStringLiteral("%1 (copy)").arg(srcCopy.name), &ok);
this,
tr("Duplicate provider"),
tr("Name for the new provider:"),
QLineEdit::Normal,
QStringLiteral("%1 (copy)").arg(srcCopy.name),
&ok);
if (!ok || name.trimmed().isEmpty())
return;
if (m_factory->instanceByName(name.trimmed())) {
QMessageBox::warning(this, tr("Duplicate provider"),
tr("An instance named '%1' already exists.").arg(name.trimmed()));
QMessageBox::warning(
this,
tr("Duplicate provider"),
tr("An instance named '%1' already exists.").arg(name.trimmed()));
return;
}
Providers::ProviderInstance copy = srcCopy;
@@ -346,7 +412,8 @@ private slots:
copy.overridesBundled = false;
QString writeErr;
if (Providers::ProviderInstanceWriter::writeToUserDir(
copy, /*previousPath=*/QString{}, &writeErr).isEmpty()) {
copy, /*previousPath=*/QString{}, &writeErr)
.isEmpty()) {
QMessageBox::warning(this, tr("Duplicate provider"), writeErr);
return;
}
@@ -370,8 +437,7 @@ private slots:
{
if (!m_factory || m_currentName.isEmpty())
return;
const Providers::ProviderInstance *instPtr
= m_factory->instanceByName(m_currentName);
const Providers::ProviderInstance *instPtr = m_factory->instanceByName(m_currentName);
if (!instPtr || !instPtr->isUserSource())
return;
@@ -391,8 +457,8 @@ private slots:
if (QMessageBox::question(this, tr("Delete provider"), question) != QMessageBox::Yes)
return;
if (!QFile::remove(sourcePath)) {
QMessageBox::warning(this, tr("Delete provider"),
tr("Failed to delete file:\n%1").arg(sourcePath));
QMessageBox::warning(
this, tr("Delete provider"), tr("Failed to delete file:\n%1").arg(sourcePath));
return;
}
if (m_secrets && !apiKeyRef.isEmpty())
@@ -418,8 +484,8 @@ private slots:
if (e.apiKeyRef.isEmpty() || (nameChanged && e.apiKeyRef == priorRef))
e.apiKeyRef = QStringLiteral("qodeassist/providers/%1").arg(e.name);
const QString validation = Providers::ProviderInstance::validate(
e, m_factory->knownClientApis());
const QString validation
= Providers::ProviderInstance::validate(e, m_factory->knownClientApis());
if (!validation.isEmpty()) {
QMessageBox::warning(this, tr("Save"), validation);
return;
@@ -427,8 +493,8 @@ private slots:
if (nameChanged) {
const auto *clash = m_factory->instanceByName(e.name);
if (clash) {
QMessageBox::warning(this, tr("Save"),
tr("An instance named '%1' already exists.").arg(e.name));
QMessageBox::warning(
this, tr("Save"), tr("An instance named '%1' already exists.").arg(e.name));
return;
}
const QStringList referencing = agentsReferencing(priorName);
@@ -449,20 +515,21 @@ private slots:
}
const QString softWarning = Providers::ProviderInstance::warnings(e);
if (!softWarning.isEmpty()) {
if (QMessageBox::warning(this, tr("Save"),
softWarning + QStringLiteral("\n\n")
+ tr("Save anyway?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No)
if (QMessageBox::warning(
this,
tr("Save"),
softWarning + QStringLiteral("\n\n") + tr("Save anyway?"),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No)
!= QMessageBox::Yes)
return;
}
const QString previousPath
= (prior && prior->isUserSource()) ? prior->sourcePath : QString{};
const QString previousPath = (prior && prior->isUserSource()) ? prior->sourcePath
: QString{};
QString writeErr;
const QString writtenPath = Providers::ProviderInstanceWriter::writeToUserDir(
e, previousPath, &writeErr);
const QString writtenPath
= Providers::ProviderInstanceWriter::writeToUserDir(e, previousPath, &writeErr);
if (writtenPath.isEmpty()) {
QMessageBox::warning(this, tr("Save"), writeErr);
return;
@@ -472,7 +539,8 @@ private slots:
!= QFileInfo(previousPath).absoluteFilePath()) {
if (!QFile::remove(previousPath)) {
QMessageBox::warning(
this, tr("Save"),
this,
tr("Save"),
tr("Saved to:\n%1\n\nbut could not remove the old file:\n%2\n\n"
"Two provider files now describe this instance — delete the "
"old file manually to avoid a duplicate-name error.")
@@ -515,15 +583,13 @@ private slots:
{
if (!m_factory || !m_secrets || m_currentName.isEmpty())
return;
const Providers::ProviderInstance *instPtr
= m_factory->instanceByName(m_currentName);
const Providers::ProviderInstance *instPtr = m_factory->instanceByName(m_currentName);
if (!instPtr || instPtr->apiKeyRef.isEmpty())
return;
const QString instName = instPtr->name;
const QString apiKeyRef = instPtr->apiKeyRef;
if (QMessageBox::question(
this, tr("Clear API key"),
tr("Erase the stored API key for '%1'?").arg(instName))
this, tr("Clear API key"), tr("Erase the stored API key for '%1'?").arg(instName))
!= QMessageBox::Yes)
return;
m_secrets->eraseKey(apiKeyRef);
@@ -558,6 +624,25 @@ private slots:
}
private:
void moveSelection(int delta)
{
if (m_rows.isEmpty())
return;
int cur = -1;
for (int i = 0; i < m_rows.size(); ++i) {
if (m_rows.at(i)->providerName() == m_currentName) {
cur = i;
break;
}
}
int next = cur < 0 ? (delta > 0 ? 0 : int(m_rows.size()) - 1) : cur + delta;
next = qBound(0, next, int(m_rows.size()) - 1);
if (next == cur)
return;
selectInstance(m_rows.at(next)->providerName());
m_listScroll->ensureWidgetVisible(m_rows.at(next), 0, 40);
}
void populateDetail(const QString &name)
{
if (!m_factory)
@@ -567,14 +652,13 @@ private:
m_detailPane->clear();
return;
}
const bool hasStoredKey
= m_secrets && !inst->apiKeyRef.isEmpty() && m_secrets->hasKey(inst->apiKeyRef);
const bool hasStoredKey = m_secrets && !inst->apiKeyRef.isEmpty()
&& m_secrets->hasKey(inst->apiKeyRef);
m_detailPane->populate(*inst, hasStoredKey);
if (m_launcher) {
m_detailPane->setLaunchState(
m_launcher->state(inst->name),
m_launcher->lastError(inst->name));
m_detailPane
->setLaunchState(m_launcher->state(inst->name), m_launcher->lastError(inst->name));
m_detailPane->resetLaunchTerminal(m_launcher->scrollback(inst->name));
} else {
m_detailPane->setLaunchState(Providers::ProviderLauncher::Idle, {});
@@ -607,13 +691,11 @@ private:
{
if (!m_launcher || m_currentName.isEmpty())
return;
m_detailPane->setLaunchState(
m_launcher->state(m_currentName),
m_launcher->lastError(m_currentName));
m_detailPane
->setLaunchState(m_launcher->state(m_currentName), m_launcher->lastError(m_currentName));
}
static ProviderListItem::Status rowStatusFromState(
Providers::ProviderLauncher::State state)
static ProviderListItem::Status rowStatusFromState(Providers::ProviderLauncher::State state)
{
switch (state) {
case Providers::ProviderLauncher::Ready:

View File

@@ -28,6 +28,49 @@ void applyMutedSmallCaps(QLabel *label)
label->setPalette(p);
}
QString filterHighlightedHtml(const QString &text, const QString &lowerFilter)
{
if (lowerFilter.isEmpty())
return text.toHtmlEscaped();
QColor mark = Utils::creatorColor(Utils::Theme::TextColorLink);
mark.setAlphaF(0.30);
const QString markCss
= QStringLiteral("background-color:%1;border-radius:2px;").arg(cssColor(mark));
const QString lowerText = text.toLower();
QString out;
int pos = 0;
while (true) {
const int hit = lowerText.indexOf(lowerFilter, pos);
if (hit < 0) {
out += text.mid(pos).toHtmlEscaped();
break;
}
out += text.mid(pos, hit - pos).toHtmlEscaped();
out += QStringLiteral("<span style=\"%1\">%2</span>")
.arg(markCss, text.mid(hit, lowerFilter.size()).toHtmlEscaped());
pos = hit + int(lowerFilter.size());
}
return out;
}
void styleSourceBadge(QLabel *label, bool user)
{
QFont f = label->font();
f.setBold(true);
f.setPixelSize(10);
label->setFont(f);
const QColor accent = Utils::creatorColor(
user ? Utils::Theme::TextColorLink : Utils::Theme::PanelTextColorMid);
QColor bg = accent;
bg.setAlphaF(0.12);
QColor border = accent;
border.setAlphaF(0.45);
label->setStyleSheet(QStringLiteral(
"QLabel { color:%1; background:%2; border:1px solid %3;"
" border-radius:8px; padding:1px 8px; }")
.arg(cssColor(accent), cssColor(bg), cssColor(border)));
}
QLabel *makeSectionHeader(const QString &title, QWidget *parent)
{
auto *header = new QLabel(title.toUpper(), parent);

View File

@@ -16,6 +16,10 @@ namespace QodeAssist::Settings {
void applyMutedSmallCaps(QLabel *label);
void styleSourceBadge(QLabel *label, bool user);
QString filterHighlightedHtml(const QString &text, const QString &lowerFilter);
QLabel *makeSectionHeader(const QString &title, QWidget *parent);
QLabel *makeHintLabel(const QString &text, QWidget *parent = nullptr);

View File

@@ -58,6 +58,15 @@ void TagFilterStrip::setAvailableTags(
emit activeTagsChanged(m_activeTags);
}
void TagFilterStrip::setMaxColumns(int columns)
{
const int clamped = qMax(1, columns);
if (clamped == m_maxColumns)
return;
m_maxColumns = clamped;
rebuild();
}
void TagFilterStrip::setVisibleCounts(const QMap<QString, int> &countsByTag)
{
for (auto it = m_chipByTag.cbegin(); it != m_chipByTag.cend(); ++it)
@@ -161,14 +170,12 @@ void TagFilterStrip::rebuild()
sorted.reserve(m_counts.size());
for (auto it = m_counts.cbegin(); it != m_counts.cend(); ++it)
sorted.emplace_back(it.key(), it.value());
std::sort(sorted.begin(), sorted.end(),
[](const auto &a, const auto &b) {
if (a.second != b.second)
return a.second > b.second;
return a.first.localeAwareCompare(b.first) < 0;
});
std::sort(sorted.begin(), sorted.end(), [](const auto &a, const auto &b) {
if (a.second != b.second)
return a.second > b.second;
return a.first.localeAwareCompare(b.first) < 0;
});
constexpr int kMaxColumns = 3;
auto *gridHost = new QWidget(this);
auto *grid = new QGridLayout(gridHost);
grid->setContentsMargins(0, 0, 0, 0);
@@ -181,7 +188,7 @@ void TagFilterStrip::rebuild()
connect(chip, &TagChip::clicked, this, &TagFilterStrip::toggleTag);
grid->addWidget(chip, gridRow, col, Qt::AlignLeft);
m_chipByTag.insert(tag, chip);
if (++col >= kMaxColumns) {
if (++col >= m_maxColumns) {
col = 0;
++gridRow;
}

View File

@@ -26,6 +26,7 @@ public:
void setAvailableTags(const QMap<QString, int> &countsByTag);
void setAvailableTags(const QMap<QString, int> &countsByTag, const QSet<QString> &activeTags);
void setVisibleCounts(const QMap<QString, int> &countsByTag);
void setMaxColumns(int columns);
const QSet<QString> &activeTags() const { return m_activeTags; }
void toggleTag(const QString &tag);
@@ -42,6 +43,7 @@ private:
QMap<QString, int> m_counts;
QSet<QString> m_activeTags;
int m_maxColumns = 3;
QVBoxLayout *m_layout = nullptr;
QHash<QString, TagChip *> m_chipByTag;
QLabel *m_clearLink = nullptr;