#include "panels/DatasetListPanel.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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); if (special) return QSize(0, 34); // 无副标题(容器节点 项目/GS/TM)用紧凑矮卡,避免标题下大片留白。 const bool hasMeta = idx.data(Qt::DisplayRole).toString().contains(QLatin1Char('\n')); return QSize(0, hasMeta ? 52 : 30); } 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; } // 选中/hover:与对象树(ObjectRowDelegate)完全一致——整行方角填充(非圆角卡)+ 左 2px // accent 竖条贴行左缘满高 + 选中标题加粗。保留双行卡片内容(标题 + 创建时间·类型副标题)。 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) p->fillRect(opt.rect, geopro::app::tokenColor(selected ? "bg/selected" : "bg/hover")); if (selected) p->fillRect(QRect(opt.rect.left(), opt.rect.top(), 2, opt.rect.height()), geopro::app::tokenColor("accent/primary")); // 可勾选项:左侧画复选框(用当前 style 的指示器),文本整体右移。容器节点(项目/GS/TM)不画 // 复选框、名称紧跟展开图标(左留白小,与对象树容器一致;勿为对齐子级而预留空复选框列, // 否则容器名与展开图标间出现大段空白,见用户 #2 反馈)。 const int box = 16; int textLeftPad = 6; const bool checkable = (idx.flags() & Qt::ItemIsUserCheckable); const bool isContainer = idx.data(kDsDdCodeRole).toString() == QStringLiteral("container"); if (checkable) { QRect checkRect(r.left() + 12, r.top() + (r.height() - box) / 2, box, box); const auto cs = static_cast(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; // 复选框右侧留白后再放文本 } else if (isContainer) { // 容器文本左缘对齐子级复选框的左缘(r.left()+12)——使「容器→带框子级」的视觉缩进 = 一个树级 // (14px),与「带框父→带框子」一致,消除带框子级相对容器缩进过大(用户 #6)。只 +12(非整列宽), // 故名称仍紧邻展开图标、无 #2 的大留白。 textLeftPad = 12; } 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, 4, -12, -4); QFont tf = opt.font; tf.setPixelSize(geopro::app::scaledPx(13)); if (selected) tf.setWeight(QFont::DemiBold); // 选中加粗,与对象树一致 p->setFont(tf); p->setPen(geopro::app::tokenColor("text/primary")); if (meta.isEmpty()) { // 无副标题(容器节点):标题垂直居中整卡,不留下半空白。 p->drawText(textR, Qt::AlignLeft | Qt::AlignVCenter, p->fontMetrics().elidedText(title, Qt::ElideRight, textR.width())); } else { 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())); 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(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(ev); if (me->button() == Qt::LeftButton && hit.contains(me->pos())) { toggle(); return true; } } else if (ev->type() == QEvent::KeyPress) { auto* ke = static_cast(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& rows, bool append, const QString& tmObjectId) { if (!tree) return; if (!append) tree->clear(); // id→已在树中的项:!append 时为空;append(分页)时含已加载行,使新行能挂到既有父下。 QHash 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 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& 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(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 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& 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& 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& visibleTypes, const QDate& minDate) { if (!tree) return; for (int i = 0; i < tree->topLevelItemCount(); ++i) applyFilterRec(tree->topLevelItem(i), visibleTypes, minDate); } } // namespace geopro::app