434 lines
19 KiB
C++
434 lines
19 KiB
C++
#include "ColorGradientDialog.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
|
||
#include <QColorDialog>
|
||
#include <QComboBox>
|
||
|
||
#include "EmptyAwareComboBox.hpp"
|
||
#include <QDialogButtonBox>
|
||
#include <QDoubleSpinBox>
|
||
#include <QFile>
|
||
#include <QFileDialog>
|
||
#include <QGridLayout>
|
||
#include <QHBoxLayout>
|
||
#include <QIcon>
|
||
#include <QInputDialog>
|
||
#include <QJsonArray>
|
||
#include <QJsonObject>
|
||
#include <QLabel>
|
||
#include <QLineEdit>
|
||
#include <QMessageBox>
|
||
#include <QPainter>
|
||
#include <QPixmap>
|
||
#include <QPointer>
|
||
#include <QPushButton>
|
||
#include <QRegularExpression>
|
||
#include <QSignalBlocker>
|
||
#include <QSlider>
|
||
#include <QVBoxLayout>
|
||
|
||
#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<unsigned char>(r), static_cast<unsigned char>(g),
|
||
static_cast<unsigned char>(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<unsigned char>(c.red()), static_cast<unsigned char>(c.green()),
|
||
static_cast<unsigned char>(c.blue()), static_cast<unsigned char>(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<Stop>& 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<Stop>& init, double minValue,
|
||
double maxValue, double originMin, double originMax,
|
||
std::vector<double> 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<int>(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<int>::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<double>::of(&QDoubleSpinBox::valueChanged), this,
|
||
[this](double) { onMinMaxChanged(); });
|
||
connect(maxSpin_, QOverload<double>::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<GradientEditWidget::Stop> 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<int>(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<Stop> st;
|
||
for (const auto& [pos, c] : clr.stops) st.push_back({pos, c});
|
||
gradient_->setStops(st);
|
||
onSelectionCleared();
|
||
opacity_ = clr.opacity;
|
||
opacitySlider_->setValue(static_cast<int>(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<qint64>(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<ColorGradientDialog> 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<ColorGradientDialog> 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<Stop> 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
|