geopro/src/app/panels/DatasetListPanel.cpp

307 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "panels/DatasetListPanel.hpp"
#include <QAbstractItemView>
#include <QApplication>
#include <QColor>
#include <QHash>
#include <QKeyEvent>
#include <QListWidget>
#include <QListWidgetItem>
#include <QMouseEvent>
#include <QObject>
#include <QPainter>
#include <QPainterPath>
#include <QString>
#include <QStyle>
#include <QStyledItemDelegate>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QTreeWidgetItemIterator>
#include "Theme.hpp"
namespace geopro::app {
namespace {
QString humanSize(long long b) {
if (b < 1024) return QStringLiteral("%1 B").arg(b);
const double kb = b / 1024.0;
if (kb < 1024.0) return QStringLiteral("%1 KB").arg(kb, 0, 'f', 1);
return QStringLiteral("%1 MB").arg(kb / 1024.0, 0, 'f', 1);
}
// 数据/文件列表卡片委托:标题+元信息双行、悬停/选中圆角高亮 + 选中左 2px 强调竖条规范§6.2)。
// 特殊行(加载更多 / 占位提示)退回为居中纯文本,不画卡片。
class DatasetCardDelegate : public QStyledItemDelegate {
public:
using QStyledItemDelegate::QStyledItemDelegate;
QSize sizeHint(const QStyleOptionViewItem&, const QModelIndex& idx) const override {
const bool special =
idx.data(kDsLoadMoreRole).toBool() || !(idx.flags() & Qt::ItemIsSelectable);
return QSize(0, special ? 34 : 52);
}
void paint(QPainter* p, const QStyleOptionViewItem& opt, const QModelIndex& idx) const override {
p->save();
p->setRenderHint(QPainter::Antialiasing, true);
const QString disp = idx.data(Qt::DisplayRole).toString();
// 「加载更多」居中强调色文本hover 时加底)。
if (idx.data(kDsLoadMoreRole).toBool()) {
if (opt.state & QStyle::State_MouseOver) {
QPainterPath bgp;
bgp.addRoundedRect(opt.rect.adjusted(4, 2, -4, -2), 6, 6);
p->fillPath(bgp, geopro::app::tokenColor("bg/hover"));
}
p->setPen(geopro::app::tokenColor("accent/primary"));
p->drawText(opt.rect, Qt::AlignCenter, disp);
p->restore();
return;
}
// 占位提示行(不可选):居中淡色文本。
if (!(idx.flags() & Qt::ItemIsSelectable)) {
p->setPen(geopro::app::tokenColor("text/disabled"));
p->drawText(opt.rect, Qt::AlignCenter, disp);
p->restore();
return;
}
// 卡片
const QRect r = opt.rect.adjusted(4, 2, -4, -2);
const bool selected = opt.state & QStyle::State_Selected;
const bool hover = opt.state & QStyle::State_MouseOver;
if (selected || hover) {
QPainterPath path;
path.addRoundedRect(r, 6, 6);
p->fillPath(path, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover"));
}
if (selected) { // 左 2px 强调竖条规范§6.2
p->fillRect(QRect(r.left(), r.top() + 4, 2, r.height() - 8),
geopro::app::tokenColor("accent/primary"));
}
// 可勾选项:左侧画复选框(用当前 style 的指示器),文本整体右移。
int textLeftPad = 14;
const bool checkable = (idx.flags() & Qt::ItemIsUserCheckable);
if (checkable) {
const int box = 16;
QRect checkRect(r.left() + 12, r.top() + (r.height() - box) / 2, box, box);
const auto cs = static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt());
QStyleOptionViewItem o(opt);
o.rect = checkRect;
o.state &= ~QStyle::State_HasFocus;
o.state |= (cs == Qt::Checked ? QStyle::State_On : QStyle::State_Off);
const QWidget* w = opt.widget;
QStyle* st = w ? w->style() : QApplication::style();
st->drawPrimitive(QStyle::PE_IndicatorItemViewItemCheck, &o, p, w);
textLeftPad = 12 + box + 8; // 复选框右侧留白后再放文本
}
QString title = disp, meta;
const int nl = disp.indexOf(QLatin1Char('\n'));
if (nl >= 0) {
title = disp.left(nl);
meta = disp.mid(nl + 1);
}
const QRect textR = r.adjusted(textLeftPad, 6, -12, -6);
// 标题
QFont tf = opt.font;
tf.setPixelSize(geopro::app::scaledPx(13));
p->setFont(tf);
p->setPen(geopro::app::tokenColor("text/primary"));
const QRect titleR(textR.left(), textR.top(), textR.width(), textR.height() / 2);
p->drawText(titleR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(title, Qt::ElideRight, titleR.width()));
// 元信息
if (!meta.isEmpty()) {
QFont mf = opt.font;
mf.setPixelSize(geopro::app::scaledPx(11));
p->setFont(mf);
p->setPen(geopro::app::tokenColor("text/tertiary"));
const QRect metaR(textR.left(), textR.center().y() + 1, textR.width(),
textR.height() / 2);
p->drawText(metaR, Qt::AlignLeft | Qt::AlignVCenter,
p->fontMetrics().elidedText(meta, Qt::ElideRight, metaR.width()));
}
p->restore();
}
bool editorEvent(QEvent* ev, QAbstractItemModel* model, const QStyleOptionViewItem& opt,
const QModelIndex& idx) override {
if (!(idx.flags() & Qt::ItemIsUserCheckable))
return QStyledItemDelegate::editorEvent(ev, model, opt, idx);
const QRect r = opt.rect.adjusted(4, 2, -4, -2);
const int box = 16;
// 命中区放宽到复选框左侧整段(含一点文本起始),便于点击。
const QRect hit(r.left(), r.top(), 12 + box + 8, r.height());
auto toggle = [&]() {
const auto cur = static_cast<Qt::CheckState>(idx.data(Qt::CheckStateRole).toInt());
model->setData(idx, cur == Qt::Checked ? Qt::Unchecked : Qt::Checked, Qt::CheckStateRole);
};
if (ev->type() == QEvent::MouseButtonRelease) {
auto* me = static_cast<QMouseEvent*>(ev);
if (me->button() == Qt::LeftButton && hit.contains(me->pos())) {
toggle();
return true;
}
} else if (ev->type() == QEvent::KeyPress) {
auto* ke = static_cast<QKeyEvent*>(ev);
if (ke->key() == Qt::Key_Space || ke->key() == Qt::Key_Select) {
toggle();
return true;
}
}
return QStyledItemDelegate::editorEvent(ev, model, opt, idx);
}
};
} // namespace
namespace {
// 建一条数据集树项不挂载列0 文本 = dsName +「创建时间 · 类型名」data 存各角色。
QTreeWidgetItem* makeDatasetItem(const geopro::data::DsRow& d, const QString& tmObjectId) {
QString text = QString::fromStdString(d.dsName);
QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间
if (!d.typeName.empty())
sub += QStringLiteral(" · %1").arg(QString::fromStdString(d.typeName)); // 再跟类型
if (!sub.isEmpty()) text += QStringLiteral("\n%1").arg(sub);
auto* item = new QTreeWidgetItem();
item->setText(0, text);
item->setData(0, kDsIdRole, QString::fromStdString(d.id));
item->setData(0, kDsDdTypeRole, QString::fromStdString(d.ddCode));
item->setData(0, kDsDdCodeRole, QString::fromStdString(d.ddCode));
item->setData(0, kDsNameRole, QString::fromStdString(d.dsName));
item->setData(0, kDsTypeNameRole, QString::fromStdString(d.typeName));
item->setData(0, kDsCreateTimeRole, QString::fromStdString(d.createTime));
item->setData(0, kDsTmObjectIdRole, tmObjectId); // 所属 TM 对象 id白化 structParentId
// 单击 tip显示数据集主要属性名称 / 类型 / 创建时间对齐菜单文档「tip显示ds的主要属性」。
QString tip = QStringLiteral("名称:%1").arg(QString::fromStdString(d.dsName));
if (!d.typeName.empty()) tip += QStringLiteral("\n类型:%1").arg(QString::fromStdString(d.typeName));
if (!d.createTime.empty())
tip += QStringLiteral("\n创建时间:%1").arg(QString::fromStdString(d.createTime));
item->setToolTip(0, tip);
return item;
}
} // namespace
void populateDatasetList(QTreeWidget* tree, const std::vector<geopro::data::DsRow>& rows, bool append,
const QString& tmObjectId) {
if (!tree) return;
if (!append) tree->clear();
// id→已在树中的项!append 时为空append分页时含已加载行使新行能挂到既有父下。
QHash<QString, QTreeWidgetItem*> byId;
for (QTreeWidgetItemIterator it(tree); *it; ++it) {
const QString id = (*it)->data(0, kDsIdRole).toString();
if (!id.isEmpty()) byId.insert(id, *it);
}
// 第一遍:本批全部建项(不挂载)并登记 id使同批内的父子也能互相找到。
std::vector<QTreeWidgetItem*> batch;
batch.reserve(rows.size());
for (const auto& d : rows) {
auto* item = makeDatasetItem(d, tmObjectId);
byId.insert(QString::fromStdString(d.id), item);
batch.push_back(item);
}
// 第二遍:按 parentId 挂载。父在集合内→作其子;否则(父是源文件节点/不在本批)→作树根。
for (std::size_t i = 0; i < rows.size(); ++i) {
const QString pid = QString::fromStdString(rows[i].parentId);
QTreeWidgetItem* parent = pid.isEmpty() ? nullptr : byId.value(pid, nullptr);
if (parent && parent != batch[i])
parent->addChild(batch[i]);
else
tree->addTopLevelItem(batch[i]);
}
// 默认折叠(对齐原版:仅显源数据根行,派生数据收在展开箭头内)。
}
void populateFileList(QListWidget* list, const std::vector<geopro::data::DsRow>& rows, bool append) {
if (!list) return;
if (!append) list->clear();
if (!append && rows.empty()) {
auto* hint = new QListWidgetItem(QStringLiteral("(暂无文件)"), list);
hint->setFlags(Qt::NoItemFlags);
hint->setTextAlignment(Qt::AlignCenter);
return;
}
for (const auto& d : rows) {
const QString fname =
d.fileName.empty() ? QString::fromStdString(d.dsName) : QString::fromStdString(d.fileName);
QString sub = QString::fromStdString(d.createTime); // 名称下先创建时间
sub += QStringLiteral(" · %1").arg(humanSize(d.fileSize)); // 再跟大小
const QString text = fname + QStringLiteral("\n%1").arg(sub);
auto* item = new QListWidgetItem(text, list);
item->setData(kDsIdRole, QString::fromStdString(d.id));
item->setData(kDsFileUrlRole, QString::fromStdString(d.fileUrl));
}
}
void applyDatasetCardDelegate(QAbstractItemView* view) {
if (!view) return;
view->setItemDelegate(new DatasetCardDelegate(view));
view->setMouseTracking(true); // 让委托收到 hover 状态
if (auto* list = qobject_cast<QListWidget*>(view)) list->setSpacing(0); // 卡间距由委托内边距控制
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, view,
[view]() { view->viewport()->update(); });
}
QStringList collectDatasetTypeNames(QTreeWidget* tree) {
QStringList types;
if (!tree) return types;
QSet<QString> seen;
for (QTreeWidgetItemIterator it(tree); *it; ++it) {
if ((*it)->data(0, kDsLoadMoreRole).toBool()) continue;
const QString t = (*it)->data(0, kDsTypeNameRole).toString();
if (t.isEmpty() || seen.contains(t)) continue;
seen.insert(t);
types << t;
}
return types;
}
namespace {
// 解析创建时间字符串为日期(容忍 "yyyy-MM-dd HH:mm:ss" / "yyyy-MM-dd" / "yyyy/MM/dd")。
QDate parseRowDate(const QString& s) {
if (s.isEmpty()) return {};
const QString d = s.left(10);
for (const char* fmt : {"yyyy-MM-dd", "yyyy/MM/dd"}) {
QDate v = QDate::fromString(d, QString::fromLatin1(fmt));
if (v.isValid()) return v;
}
return {};
}
// 递归判定项是否匹配(类型在集合内 且 创建日期 >= minDate
bool rowMatches(QTreeWidgetItem* item, const QSet<QString>& visibleTypes, const QDate& minDate) {
const QString t = item->data(0, kDsTypeNameRole).toString();
if (!t.isEmpty() && !visibleTypes.contains(t)) return false;
if (minDate.isValid()) {
const QDate d = parseRowDate(item->data(0, kDsCreateTimeRole).toString());
if (d.isValid() && d < minDate) return false;
}
return true;
}
// 返回该项(或其任一后代)是否可见;据此 setHidden。父项只要有可见后代即保留。
bool applyFilterRec(QTreeWidgetItem* item, const QSet<QString>& visibleTypes, const QDate& minDate) {
if (item->data(0, kDsLoadMoreRole).toBool()) return true; // 「加载更多」行恒显
bool anyChildVisible = false;
for (int i = 0; i < item->childCount(); ++i)
anyChildVisible |= applyFilterRec(item->child(i), visibleTypes, minDate);
const bool selfMatch = rowMatches(item, visibleTypes, minDate);
const bool visible = selfMatch || anyChildVisible;
item->setHidden(!visible);
return visible;
}
} // namespace
void applyDatasetFilter(QTreeWidget* tree, const QSet<QString>& visibleTypes, const QDate& minDate) {
if (!tree) return;
for (int i = 0; i < tree->topLevelItemCount(); ++i)
applyFilterRec(tree->topLevelItem(i), visibleTypes, minDate);
}
} // namespace geopro::app