#include "ColorGradientDialog.hpp" #include #include #include #include #include "EmptyAwareComboBox.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ColorScaleIO.hpp" #include "FormKit.hpp" #include "Theme.hpp" #include "repo/IColorTemplateRepository.hpp" namespace geopro::app { namespace { using Stop = GradientEditWidget::Stop; using geopro::core::Rgba; Rgba hx(unsigned r, unsigned g, unsigned b) { return Rgba{static_cast(r), static_cast(g), static_cast(b), 255}; } QColor toQ(const Rgba& c) { return QColor(c.r, c.g, c.b, c.a); } Rgba fromQ(const QColor& c) { return Rgba{static_cast(c.red()), static_cast(c.green()), static_cast(c.blue()), static_cast(c.alpha())}; } // 解析后端色阶 color 字符串("#RRGGBB" / "rgb(r,g,b)" / "rgba(r,g,b,a)")→ Rgba。 Rgba parseColorString(const QString& s) { const QString t = s.trimmed(); if (t.startsWith(QLatin1Char('#')) && t.size() >= 7) { bool ok = false; const unsigned r = t.mid(1, 2).toUInt(&ok, 16); const unsigned g = t.mid(3, 2).toUInt(&ok, 16); const unsigned b = t.mid(5, 2).toUInt(&ok, 16); return hx(r, g, b); } static const QRegularExpression re( QStringLiteral("rgba?\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)")); const auto m = re.match(t); if (m.hasMatch()) return hx(m.captured(1).toUInt(), m.captured(2).toUInt(), m.captured(3).toUInt()); return hx(0, 0, 0); } // 生成配色方案预览色条(下拉用),复刻 generateColorPreview。 QPixmap previewPixmap(const std::vector& stops, int w = 100, int h = 16) { QPixmap pm(w, h); pm.fill(Qt::white); if (stops.size() >= 2) { QPainter p(&pm); QLinearGradient grad(0, 0, w, 0); for (const auto& s : stops) grad.setColorAt(std::clamp(s.pos, 0.0, 1.0), toQ(s.color)); p.fillRect(QRect(0, 0, w, h), grad); } return pm; } } // namespace ColorGradientDialog::ColorGradientDialog(const std::vector& init, double minValue, double maxValue, double originMin, double originMax, std::vector samples, double opacity, geopro::data::IColorTemplateRepository* tplRepo, QString projectId, QWidget* parent) : QDialog(parent), originMin_(originMin), originMax_(originMax), opacity_(opacity), tplRepo_(tplRepo), projectId_(std::move(projectId)) { setWindowTitle(QStringLiteral("色阶编辑器")); setModal(true); auto* root = new QVBoxLayout(this); // ── 顶部两列 grid(grid-template-columns: 50% 50%) ────────────────────── auto* grid = new QGridLayout(); grid->setHorizontalSpacing(geopro::app::space::kLg); // formkit 标准列距 grid->setVerticalSpacing(geopro::app::space::kMd); // formkit 标准行距 int rowIdx = 0; // 配色方案(下拉带预览色条)。 schemeCombo_ = new EmptyAwareComboBox(this); schemeCombo_->setIconSize(QSize(100, 16)); { auto* cell = new QHBoxLayout(); cell->addWidget(formkit::editLabel(QStringLiteral("配色方案:"))); cell->addWidget(schemeCombo_, 1); grid->addLayout(cell, rowIdx, 0); } // 分布方式(disabled, 默认线性)+ 反向。 { auto* cell = new QHBoxLayout(); cell->addWidget(formkit::editLabel(QStringLiteral("分布方式:"))); auto* distCombo = new EmptyAwareComboBox(this); distCombo->addItem(QStringLiteral("线性"), QStringLiteral("linear")); distCombo->addItem(QStringLiteral("对数"), QStringLiteral("log")); distCombo->setCurrentIndex(0); distCombo->setEnabled(false); cell->addWidget(distCombo, 1); auto* reverseBtn = new QPushButton(QStringLiteral("反转"), this); cell->addWidget(reverseBtn); connect(reverseBtn, &QPushButton::clicked, this, [this] { gradient_->reverse(); }); grid->addLayout(cell, rowIdx++, 1); } // 数值范围(只读:originMin~originMax,各保留 6 位)。 rangeEdit_ = new QLineEdit(this); rangeEdit_->setReadOnly(true); rangeEdit_->setText(QStringLiteral("%1~%2") .arg(QString::number(originMin_, 'f', 6)) .arg(QString::number(originMax_, 'f', 6))); { auto* cell = new QHBoxLayout(); cell->addWidget(formkit::editLabel(QStringLiteral("数值范围:"))); cell->addWidget(rangeEdit_, 1); grid->addLayout(cell, rowIdx, 0); } // 最小值/最大值(可编辑)。 { auto* cell = new QHBoxLayout(); cell->addWidget(formkit::editLabel(QStringLiteral("最小值:"))); minSpin_ = new QDoubleSpinBox(this); minSpin_->setDecimals(6); minSpin_->setRange(-1e12, 1e12); minSpin_->setValue(minValue); cell->addWidget(minSpin_, 1); cell->addWidget(formkit::editLabel(QStringLiteral("最大值:"))); maxSpin_ = new QDoubleSpinBox(this); maxSpin_->setDecimals(6); maxSpin_->setRange(-1e12, 1e12); maxSpin_->setValue(maxValue); cell->addWidget(maxSpin_, 1); grid->addLayout(cell, rowIdx++, 1); } // 当前数据。 curDataLabel_ = new QLabel(QStringLiteral("-"), this); { auto* cell = new QHBoxLayout(); cell->addWidget(formkit::editLabel(QStringLiteral("当前数据值:"))); cell->addWidget(curDataLabel_, 1); grid->addLayout(cell, rowIdx, 0); } // 当前位置。 curPosLabel_ = new QLabel(QStringLiteral("-"), this); { auto* cell = new QHBoxLayout(); cell->addWidget(formkit::editLabel(QStringLiteral("当前数据位置:"))); cell->addWidget(curPosLabel_, 1); grid->addLayout(cell, rowIdx++, 1); } // 当前颜色(色块按钮,仅选中手柄时可用)。 curColorBtn_ = new QPushButton(this); curColorBtn_->setFixedSize(48, 22); curColorBtn_->setEnabled(false); { auto* cell = new QHBoxLayout(); cell->addWidget(formkit::editLabel(QStringLiteral("当前颜色:"))); cell->addWidget(curColorBtn_); cell->addStretch(1); grid->addLayout(cell, rowIdx++, 0); } root->addLayout(grid); // ── 渐变画布 ─────────────────────────────────────────────────────────── gradient_ = new GradientEditWidget(this); gradient_->setMinimumHeight(400); gradient_->setMinMax(minValue, maxValue); gradient_->setSamples(std::move(samples)); if (init.size() >= 2) gradient_->setStops(init); root->addWidget(gradient_); // ── 整体透明度滑块(0~1, step 0.01) ─────────────────────────────────── { auto* opRow = new QHBoxLayout(); opRow->addWidget(new QLabel(QStringLiteral("整体透明度:"))); opacitySlider_ = new QSlider(Qt::Horizontal, this); opacitySlider_->setRange(0, 100); opacitySlider_->setValue(static_cast(opacity_ * 100 + 0.5)); opacityLabel_ = new QLabel(QString::number(opacity_, 'f', 2), this); opRow->addWidget(opacitySlider_, 1); opRow->addWidget(opacityLabel_); root->addLayout(opRow); } // ── 底部按钮:左 导入/导出/新建色阶;右 取消/应用 ────────────────────── { auto* btns = new QDialogButtonBox(this); auto* importBtn = btns->addButton(QStringLiteral("导入"), QDialogButtonBox::ActionRole); auto* exportBtn = btns->addButton(QStringLiteral("导出"), QDialogButtonBox::ActionRole); newSchemeBtn_ = btns->addButton(QStringLiteral("新建色阶"), QDialogButtonBox::ActionRole); newSchemeBtn_->setEnabled(tplRepo_ != nullptr && !projectId_.isEmpty()); btns->addButton(QStringLiteral("取消"), QDialogButtonBox::RejectRole); btns->addButton(QStringLiteral("应用"), QDialogButtonBox::AcceptRole); root->addWidget(btns); connect(importBtn, &QPushButton::clicked, this, &ColorGradientDialog::importClr); connect(exportBtn, &QPushButton::clicked, this, &ColorGradientDialog::exportClr); connect(newSchemeBtn_, &QPushButton::clicked, this, &ColorGradientDialog::newScheme); connect(btns, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(btns, &QDialogButtonBox::rejected, this, &QDialog::reject); } // ── 信号连接 ─────────────────────────────────────────────────────────── connect(schemeCombo_, QOverload::of(&QComboBox::activated), this, [this](int i) { applyScheme(i); }); connect(gradient_, &GradientEditWidget::handleSelected, this, &ColorGradientDialog::onHandleSelected); connect(gradient_, &GradientEditWidget::selectionCleared, this, &ColorGradientDialog::onSelectionCleared); connect(curColorBtn_, &QPushButton::clicked, this, &ColorGradientDialog::pickCurrentColor); connect(minSpin_, QOverload::of(&QDoubleSpinBox::valueChanged), this, [this](double) { onMinMaxChanged(); }); connect(maxSpin_, QOverload::of(&QDoubleSpinBox::valueChanged), this, [this](double) { onMinMaxChanged(); }); connect(opacitySlider_, &QSlider::valueChanged, this, [this](int v) { opacity_ = v / 100.0; opacityLabel_->setText(QString::number(opacity_, 'f', 2)); }); // 配色方案:内置预设打底,再异步拉取后端 .clr 列表(若有 api)。 buildBuiltinSchemes(); reloadSchemeCombo(); if (tplRepo_ != nullptr && !projectId_.isEmpty()) queryClrSchemes(); } std::vector ColorGradientDialog::stops() const { return gradient_->stops(); } // ── 配色方案 ───────────────────────────────────────────────────────────────── void ColorGradientDialog::buildBuiltinSchemes() { schemes_.clear(); // 默认 GMT 17 档(与 colorEditor.vue defaultColorScale 一致)。 schemes_.push_back( {QStringLiteral("默认 (GMT)"), {{0.0, hx(0x00, 0x00, 0xAA)}, {0.0625, hx(0x00, 0x00, 0xD3)}, {0.125, hx(0x00, 0x00, 0xFF)}, {0.1875, hx(0x00, 0x80, 0xFF)}, {0.25, hx(0x00, 0xFF, 0xFF)}, {0.3125, hx(0x00, 0xC0, 0x80)}, {0.375, hx(0x00, 0xFF, 0x00)}, {0.4375, hx(0x00, 0x80, 0x00)}, {0.5, hx(0x80, 0xC0, 0x00)}, {0.5625, hx(0xFF, 0xFF, 0x00)}, {0.625, hx(0xBF, 0x80, 0x00)}, {0.6875, hx(0xFF, 0x80, 0x00)}, {0.75, hx(0xFF, 0x00, 0x00)}, {0.8125, hx(0xD3, 0x00, 0x00)}, {0.875, hx(0x84, 0x00, 0x40)}, {0.9375, hx(0x60, 0x00, 0x45)}, {1.0, hx(0x30, 0x00, 0x30)}}}); schemes_.push_back({QStringLiteral("彩虹"), {{0.0, hx(0, 0, 255)}, {0.25, hx(0, 255, 255)}, {0.5, hx(0, 255, 0)}, {0.75, hx(255, 255, 0)}, {1.0, hx(255, 0, 0)}}}); schemes_.push_back( {QStringLiteral("蓝白红"), {{0.0, hx(0, 0, 255)}, {0.5, hx(255, 255, 255)}, {1.0, hx(255, 0, 0)}}}); schemes_.push_back({QStringLiteral("灰度"), {{0.0, hx(0, 0, 0)}, {1.0, hx(255, 255, 255)}}}); } void ColorGradientDialog::reloadSchemeCombo() { const QSignalBlocker block(schemeCombo_); schemeCombo_->clear(); for (const auto& s : schemes_) schemeCombo_->addItem(QIcon(previewPixmap(s.stops)), s.name); } void ColorGradientDialog::applyScheme(int index) { if (index < 0 || index >= static_cast(schemes_.size())) return; gradient_->setStops(schemes_[index].stops); onSelectionCleared(); } // ── 当前手柄读出 ───────────────────────────────────────────────────────────── void ColorGradientDialog::onHandleSelected(const QString& colorHex, const QString& valueText, const QString& percentText) { curDataLabel_->setText(valueText); curPosLabel_->setText(percentText); curColor_ = parseColorString(colorHex); curColorBtn_->setEnabled(true); curColorBtn_->setStyleSheet( QStringLiteral("background-color: %1;").arg(toQ(curColor_).name())); } void ColorGradientDialog::onSelectionCleared() { curDataLabel_->setText(QStringLiteral("-")); curPosLabel_->setText(QStringLiteral("-")); curColorBtn_->setEnabled(false); curColorBtn_->setStyleSheet(QString()); } void ColorGradientDialog::onMinMaxChanged() { gradient_->setMinMax(minSpin_->value(), maxSpin_->value()); } void ColorGradientDialog::pickCurrentColor() { if (!gradient_->hasSelection()) return; const QColor picked = QColorDialog::getColor(toQ(curColor_), this, QStringLiteral("当前颜色")); if (!picked.isValid()) return; curColor_ = fromQ(picked); curColor_.a = 255; gradient_->setSelectedColor(curColor_); curColorBtn_->setStyleSheet( QStringLiteral("background-color: %1;").arg(toQ(curColor_).name())); } // ── .clr 导入/导出(复刻 importColorLevel / explortColorLevel) ─────────────── void ColorGradientDialog::importClr() { const QString path = QFileDialog::getOpenFileName(this, QStringLiteral("导入 .clr"), {}, QStringLiteral("色阶文件 (*.clr)")); if (path.isEmpty()) return; QFile f(path); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::warning(this, QStringLiteral("导入"), QStringLiteral("无法打开文件。")); return; } const ClrData clr = parseClr(f.readAll().toStdString()); if (clr.stops.size() < 2) { QMessageBox::warning(this, QStringLiteral("导入"), QStringLiteral("文件格式不正确或色阶不足。")); return; } std::vector st; for (const auto& [pos, c] : clr.stops) st.push_back({pos, c}); gradient_->setStops(st); onSelectionCleared(); opacity_ = clr.opacity; opacitySlider_->setValue(static_cast(opacity_ * 100 + 0.5)); } void ColorGradientDialog::exportClr() { const QString path = QFileDialog::getSaveFileName(this, QStringLiteral("导出 .clr"), QStringLiteral("色阶配置.clr"), QStringLiteral("色阶文件 (*.clr)")); if (path.isEmpty()) return; ClrData clr; clr.opacity = opacity_; for (const auto& s : gradient_->stops()) clr.stops.emplace_back(s.pos, s.color); QFile f(path); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, QStringLiteral("导出"), QStringLiteral("无法写入文件。")); return; } const std::string out = generateClr(clr); if (f.write(out.c_str(), static_cast(out.size())) < 0) QMessageBox::warning(this, QStringLiteral("导出"), QStringLiteral("写入失败。")); } // ── 后端接线 ───────────────────────────────────────────────────────────────── void ColorGradientDialog::newScheme() { if (tplRepo_ == nullptr || projectId_.isEmpty()) return; bool ok = false; const QString name = QInputDialog::getText(this, QStringLiteral("新建色阶"), QStringLiteral("色阶名称:"), QLineEdit::Normal, QStringLiteral("默认色阶"), &ok); if (!ok || name.trimmed().isEmpty()) return; // 领域装配(colorscale 串/位置)留在对话框;仓储只负责传输。 QJsonArray scale; for (const auto& s : gradient_->stops()) scale.append(QJsonObject{{QStringLiteral("pos"), s.pos}, {QStringLiteral("color"), toQ(s.color).name().toUpper()}, {QStringLiteral("colorId"), QString()}}); const QJsonObject properties{{QStringLiteral("name"), name}, {QStringLiteral("colorscale"), scale}}; QPointer self(this); tplRepo_->newClrScheme(projectId_, properties, [self](bool ok, QString) { if (!self) return; if (ok) { QMessageBox::information(self, QStringLiteral("新建色阶"), QStringLiteral("保存成功。")); self->queryClrSchemes(); // 刷新下拉 } else { QMessageBox::warning(self, QStringLiteral("新建色阶"), QStringLiteral("保存失败。")); } }); } void ColorGradientDialog::queryClrSchemes() { if (tplRepo_ == nullptr || projectId_.isEmpty()) return; QPointer self(this); tplRepo_->listClrSchemes(projectId_, [self](bool ok, QJsonArray arr, QString) { if (!self || !ok) return; if (arr.isEmpty()) return; // 领域解析(properties.name/colorscale)留在对话框。 self->buildBuiltinSchemes(); // 重置为内置,再追加后端 for (const auto& v : arr) { const QJsonObject props = v.toObject().value(QStringLiteral("properties")).toObject(); const QString name = props.value(QStringLiteral("name")).toString(); const QJsonArray cs = props.value(QStringLiteral("colorscale")).toArray(); if (name.isEmpty() || cs.size() < 2) continue; std::vector st; for (const auto& c : cs) { const QJsonObject o = c.toObject(); st.push_back({o.value(QStringLiteral("pos")).toDouble(), parseColorString(o.value(QStringLiteral("color")).toString())}); } if (st.size() >= 2) self->schemes_.push_back({name, std::move(st)}); } self->reloadSchemeCombo(); }); } } // namespace geopro::app