feat: 完善一级关系编辑 UI 与视图体验

This commit is contained in:
2026-08-06 18:15:14 +08:00
parent 57d75541c3
commit eef9efcc1e
29 changed files with 3907 additions and 178 deletions
+755 -17
View File
@@ -8,11 +8,18 @@ from PySide6.QtCore import Qt, QThread, QTimer, Slot
from PySide6.QtWidgets import (
QApplication,
QFileDialog,
QFrame,
QHBoxLayout,
QLabel,
QLineEdit,
QMessageBox,
QPushButton,
QSizePolicy,
QTableWidgetItem,
QTreeWidgetItem,
QToolTip,
QVBoxLayout,
QWidget,
)
from PySide6.QtGui import QColor
@@ -25,13 +32,43 @@ from .workers import EditWorker, LoadWorker, ScanWorker
PROPERTY_VALUE_TOLERANCE = 1e-9
PROPERTY_LABEL_COLUMN = 0
PROPERTY_CURRENT_COLUMN = 1
PROPERTY_SCOPE_COLUMN = 2
PROPERTY_TARGET_COLUMN = 3
PROPERTY_ACTION_COLUMN = 4
PROPERTY_TABLE_MIN_COLUMN_WIDTHS = (54, 118, 48, 66, 74)
PROPERTY_TABLE_PREFERRED_COLUMN_WIDTHS = (68, 168, 58, 92, 82)
PROPERTY_COMMAND_ORDER = ("offset", "move", "scale", "rotate", "feature", "diagnostics")
PROPERTY_COMMAND_LABELS = {
"offset": "偏移",
"move": "移动",
"scale": "缩放",
"rotate": "旋转",
"feature": "特征",
"diagnostics": "诊断",
}
PROPERTY_COMMAND_SUBTITLES = {
"offset": "沿法线调面",
"move": "移动中心/坐标",
"scale": "改尺寸/半径",
"rotate": "绕轴旋转",
"feature": "孔槽/圆角",
"diagnostics": "识别与关系",
}
PROPERTY_COMMAND_HELP = {
"offset": "沿 Face 法线调整位置,适合平面推拉、偏移或切除深度类修改。",
"move": "移动当前 Face、Edge、特征或 Solid 的中心/轴心,不直接修改尺寸。",
"scale": "修改面积、宽度、高度、半径、直径、长度等尺寸参数。",
"rotate": "设置旋转轴或旋转角度,适合 Part/Solid 的整体姿态调整。",
"feature": "执行孔、槽、圆角、倒角、封堵等离散特征命令。",
"diagnostics": "查看识别依据、一级拓扑关系和当前不支持修改的原因。",
}
FEATURE_EDIT_SEMANTICS_KEYS = {
"cad_modeling_form",
"cad_recommended_operation",
"edge_first_level_topology",
"face_first_level_topology",
"cylindrical_feature_first_level_topology",
"slot_edit_semantics",
@@ -44,6 +81,57 @@ FEATURE_EDIT_SEMANTICS_KEYS = {
}
def _property_table_column_widths(available_width: int) -> tuple[int, int, int, int, int]:
"""Prefer current value and action visibility over a wide parameter-name column."""
column_count = len(PROPERTY_TABLE_MIN_COLUMN_WIDTHS)
available = max(int(available_width or 0), column_count * 44)
minimum = PROPERTY_TABLE_MIN_COLUMN_WIDTHS
preferred = PROPERTY_TABLE_PREFERRED_COLUMN_WIDTHS
min_total = sum(minimum)
preferred_total = sum(preferred)
if available >= preferred_total:
widths = [int(value) for value in preferred]
extra = available - preferred_total
weights = (0.05, 0.62, 0.04, 0.19, 0.10)
for index, weight in enumerate(weights):
addition = int(extra * weight)
widths[index] += addition
widths[1] += available - sum(widths)
return tuple(widths) # type: ignore[return-value]
if available >= min_total:
scale = (available - min_total) / max(preferred_total - min_total, 1)
widths = [
int(round(min_width + (pref_width - min_width) * scale))
for min_width, pref_width in zip(minimum, preferred)
]
widths[1] += available - sum(widths)
return tuple(widths) # type: ignore[return-value]
floors = (48, 96, 44, 56, 70)
widths = [int(value) for value in minimum]
deficit = min_total - available
for index in (0, 2, 3, 1, 4):
if deficit <= 0:
break
reducible = max(0, widths[index] - floors[index])
reduction = min(reducible, deficit)
widths[index] -= reduction
deficit -= reduction
if deficit > 0:
widths[1] = max(44, widths[1] - deficit)
widths[1] += available - sum(widths)
return tuple(max(44, width) for width in widths) # type: ignore[return-value]
def _compact_property_card_text(text: str, limit: int = 220) -> str:
compact = " ".join(str(text or "").replace("\r", "\n").split())
if len(compact) <= limit:
return compact
return f"{compact[: max(0, limit - 1)].rstrip()}"
def _feature_dimension_keys(action_info: dict[str, object]) -> tuple[str, ...]:
"""Return the independent, user-facing dimensions for a feature candidate."""
surface = str(action_info.get("surface", "") or "")
@@ -895,6 +983,13 @@ class WindowStateMixin:
"查看当前选中对象的属性;可修改的行可以输入目标值。",
"请先选择一个对象。",
)
if hasattr(self, "property_card_scroll"):
self._set_control_state(
self.property_card_scroll,
has_selection and has_current_info,
"查看当前选中对象的参数卡片;可修改的卡片可以输入目标值。",
"请先选择一个对象。",
)
self._update_edit_action_states(has_model)
def _update_edit_action_states(self, has_model: bool) -> None:
@@ -1321,14 +1416,475 @@ class WindowStateMixin:
return
self.property_editor_specs = []
self.property_table_expanded = False
self.property_editor_selected_row = None
self.property_command_active_key = ""
was_blocked = self.property_table.blockSignals(True)
try:
self.property_table.clearSpans()
self.property_table.setRowCount(0)
finally:
self.property_table.blockSignals(was_blocked)
self._clear_property_cards()
self._clear_property_command_bar()
self._resize_property_table_height()
self._update_property_apply_state(False)
def _clear_property_cards(self) -> None:
self.property_card_rows = {}
layout = getattr(self, "property_card_layout", None)
if layout is None:
return
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.hide()
widget.setParent(None)
widget.deleteLater()
def _clear_property_command_bar(self) -> None:
self.property_command_buttons = {}
if hasattr(self, "property_command_summary_label"):
self.property_command_summary_label.setText("未选择可编辑对象")
self.property_command_summary_label.setToolTip("")
if hasattr(self, "property_command_help_label"):
self.property_command_help_label.setText("")
self.property_command_help_label.setToolTip("")
self.property_command_help_label.setVisible(False)
layout = getattr(self, "property_command_layout", None)
if layout is not None:
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.hide()
widget.setParent(None)
widget.deleteLater()
if hasattr(self, "property_command_bar"):
self.property_command_bar.setVisible(False)
if hasattr(self, "property_command_summary_label"):
self.property_command_summary_label.setVisible(False)
def _is_actionable_property_row(self, row: int, spec: dict[str, object]) -> bool:
effective_spec = self._effective_property_spec(spec, row=row)
return bool(effective_spec.get("editable") and effective_spec.get("enabled") and effective_spec.get("action"))
def _actionable_property_rows(self) -> list[tuple[int, dict[str, object]]]:
specs = list(getattr(self, "property_editor_specs", []) or [])
return [(row, spec) for row, spec in enumerate(specs) if self._is_actionable_property_row(row, spec)]
def _property_command_summary_text(self) -> tuple[str, str]:
specs = list(getattr(self, "property_editor_specs", []) or [])
topology_text = ""
for spec in specs:
key = str(spec.get("key", "") or "")
current_text = str(spec.get("current_text", "") or "").strip()
if key in {
"face_first_level_topology",
"edge_first_level_topology",
"cylindrical_feature_first_level_topology",
} and current_text:
topology_text = _compact_property_card_text(current_text, 86)
actionable = self._actionable_property_rows()
labels = [str(spec.get("label", "") or "").strip() for _row, spec in actionable]
labels = [label for label in labels if label]
if labels:
summary = f"可修改项 {len(labels)} 个:{''.join(labels[:4])}"
if topology_text:
summary = f"{summary} | {topology_text}"
else:
summary = topology_text or "当前对象没有稳定可修改项;请查看诊断信息。"
tooltip_parts = [str(spec.get("current_text", "") or "") for spec in specs if bool(spec.get("pin_top"))]
return summary, "\n\n".join(part for part in tooltip_parts if part)
def _rebuild_property_command_bar(self) -> None:
layout = getattr(self, "property_command_layout", None)
if layout is None:
return
self.property_command_buttons = {}
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
self.property_command_active_key = ""
summary, tooltip = self._property_command_summary_text()
if hasattr(self, "property_command_summary_label"):
self.property_command_summary_label.setText(summary)
self.property_command_summary_label.setToolTip(tooltip or summary)
self.property_command_summary_label.setVisible(True)
if hasattr(self, "property_command_help_label"):
self.property_command_help_label.setText("")
self.property_command_help_label.setToolTip("")
self.property_command_help_label.setVisible(False)
if hasattr(self, "property_command_bar"):
self.property_command_bar.setVisible(False)
def _select_property_command(self, command_key: str) -> None:
if not command_key:
return
def _visible_property_card_rows(self) -> list[tuple[int, dict[str, object]]]:
specs = list(getattr(self, "property_editor_specs", []) or [])
if not specs:
return []
if bool(getattr(self, "property_table_expanded", False)):
return [(row, spec) for row, spec in enumerate(specs)]
visible = self._actionable_property_rows()
if not visible:
visible = [
(row, spec)
for row, spec in enumerate(specs)
if bool(spec.get("editable") or spec.get("action") or spec.get("pin_top"))
]
if not visible:
visible = [(row, spec) for row, spec in enumerate(specs)]
selected_row = getattr(self, "property_editor_selected_row", None)
if selected_row is not None:
selected_items = [(row, spec) for row, spec in enumerate(specs) if row == selected_row]
if selected_items:
visible = selected_items + [(row, spec) for row, spec in visible if row != selected_row]
collapsed_rows = max(int(getattr(self, "property_table_collapsed_rows", 6) or 6), 6)
return visible[:collapsed_rows]
def _rebuild_property_cards(self) -> None:
layout = getattr(self, "property_card_layout", None)
if layout is None:
return
self._clear_property_cards()
visible_rows = self._visible_property_card_rows()
visible_row_ids = {row for row, _spec in visible_rows}
selected_row = getattr(self, "property_editor_selected_row", None)
if selected_row not in visible_row_ids:
self.property_editor_selected_row = None
for row, spec in visible_rows:
card = self._build_property_card(row, spec)
layout.addWidget(card)
def _select_property_card_row(self, row: int) -> None:
specs = getattr(self, "property_editor_specs", [])
if row < 0 or row >= len(specs):
return
if getattr(self, "property_editor_selected_row", None) == row:
return
self.property_editor_selected_row = row
self._rebuild_property_cards()
self._resize_property_table_height()
self._update_property_apply_state()
self._scroll_property_card_row_to_top(row)
def _toggle_property_card_row(self, row: int) -> None:
QToolTip.hideText()
specs = getattr(self, "property_editor_specs", [])
if row < 0 or row >= len(specs):
return
if getattr(self, "property_editor_selected_row", None) == row:
self.property_editor_selected_row = None
self._rebuild_property_cards()
self._resize_property_table_height()
self._update_property_apply_state()
self._scroll_property_cards_to_top()
return
self._select_property_card_row(row)
def _scroll_property_cards_to_top(self) -> None:
scroll = getattr(self, "property_card_scroll", None)
if scroll is None:
return
bar = scroll.verticalScrollBar()
if bar is None:
return
bar.setValue(0)
QTimer.singleShot(0, lambda target_bar=bar: target_bar.setValue(0))
def _scroll_property_card_row_to_top(self, row: int) -> None:
scroll = getattr(self, "property_card_scroll", None)
card = self._property_card_widgets(row).get("card")
if scroll is None or not isinstance(card, QWidget):
return
bar = scroll.verticalScrollBar()
if bar is None:
return
def apply_scroll() -> None:
bar.setValue(max(0, int(card.y()) - 4))
apply_scroll()
QTimer.singleShot(0, apply_scroll)
def _property_card_widgets(self, row: int) -> dict[str, object]:
rows = getattr(self, "property_card_rows", {})
if isinstance(rows, dict):
widgets = rows.get(row, {})
if isinstance(widgets, dict):
return widgets
return {}
def _build_property_card(self, row: int, spec: dict[str, object]) -> QFrame:
effective_spec = self._effective_property_spec(spec, row=row)
editable = bool(effective_spec.get("editable") and effective_spec.get("enabled"))
value_type = str(effective_spec.get("value_type", "number"))
input_editable = editable and value_type != "command"
span_value_columns = bool(effective_spec.get("span_value_columns")) and not editable
selected = row == getattr(self, "property_editor_selected_row", None)
card = QFrame()
card.setObjectName("propertyCard")
card.setProperty("editable", bool(editable))
card.setProperty("pinTop", bool(effective_spec.get("pin_top")))
card.setProperty("selected", bool(selected))
card.setProperty("compact", bool(not selected))
card.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
card.setCursor(Qt.CursorShape.PointingHandCursor)
card.mousePressEvent = lambda _event, target_row=row: self._toggle_property_card_row(target_row)
card_layout = QVBoxLayout(card)
card_layout.setContentsMargins(7 if not selected else 8, 2 if not selected else 7, 7 if not selected else 8, 2 if not selected else 7)
card_layout.setSpacing(0 if not selected else 5)
diagnostics_expanded = bool(getattr(self, "property_table_expanded", False))
header = QHBoxLayout()
header.setContentsMargins(0, 0, 0, 0)
header.setSpacing(8 if not selected else 4)
title = QLabel(str(effective_spec.get("label", "")))
title.setObjectName("propertyCardTitle")
title.setMinimumWidth(0)
title.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Fixed)
if selected or span_value_columns:
title.setToolTip(str(effective_spec.get("disabled_tip") or effective_spec.get("enabled_tip") or ""))
header.addWidget(title, 1)
compact_detail_below_header = False
if not selected and not span_value_columns:
current_text = str(effective_spec.get("current_text", ""))
detail_parts = [current_text] if current_text else []
scope_text = str(effective_spec.get("scope_text") or effective_spec.get("scope_label") or "").strip()
target_text = self._property_target_text(row) or str(effective_spec.get("target_text", ""))
if editable and value_type != "command":
validation_error = (
"" if not target_text.strip() else self._property_target_validation_error(effective_spec, target_text)
)
row_changed = self._property_target_changed(effective_spec, target_text)
if validation_error:
scope_text = "目标无效"
elif row_changed:
scope_text = f"目标 {target_text}"
elif editable and value_type == "command":
scope_text = str(effective_spec.get("button_text") or scope_text or "可执行").strip()
if scope_text:
detail_parts.append(scope_text)
compact_detail_text = _compact_property_card_text(" · ".join(detail_parts), 150)
compact_multiline = len(str(effective_spec.get("label", ""))) >= 14 or len(compact_detail_text) >= 30
title.setWordWrap(compact_multiline)
if compact_multiline:
title.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
card_layout.setSpacing(1)
compact_detail = QLabel(_compact_property_card_text(" · ".join(detail_parts), 150 if compact_multiline else 96))
compact_detail.setObjectName("propertyCardCompactValue")
compact_detail.setAlignment(
(Qt.AlignmentFlag.AlignLeft if compact_multiline else Qt.AlignmentFlag.AlignRight)
| Qt.AlignmentFlag.AlignVCenter
)
compact_detail.setMinimumWidth(0)
compact_detail.setSizePolicy(
QSizePolicy.Policy.Expanding if compact_multiline else QSizePolicy.Policy.Fixed,
QSizePolicy.Policy.Fixed,
)
if compact_multiline:
compact_detail_below_header = True
else:
header.addWidget(compact_detail, 0)
row_widgets_placeholder = compact_detail
status_text = "可修改" if editable else str(effective_spec.get("status_text", ""))
else:
status_text = str(effective_spec.get("status_text", ""))
status_label = QLabel(str(effective_spec.get("status_text", "")), card)
status_label.setObjectName("propertyCardMetaLabel")
status_label.setText(status_text)
status_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
header.addWidget(status_label, 0)
card_layout.addLayout(header)
status_label.setVisible(bool(status_text))
if compact_detail_below_header:
card_layout.addWidget(row_widgets_placeholder)
row_widgets: dict[str, object] = {
"card": card,
"status_label": status_label,
}
if not selected and not span_value_columns:
row_widgets["current_value"] = row_widgets_placeholder
self.property_card_rows[row] = row_widgets
card.style().unpolish(card)
card.style().polish(card)
return card
if span_value_columns:
current_text = str(effective_spec.get("current_text", ""))
show_full_span_text = bool(selected or diagnostics_expanded)
span_limit = 520 if show_full_span_text else 96
value = QLabel(_compact_property_card_text(current_text, span_limit))
value.setObjectName("propertyCardValue")
value.setWordWrap(show_full_span_text)
value.setToolTip(str(effective_spec.get("disabled_tip") or current_text))
card_layout.addWidget(value)
hint_text = _compact_property_card_text(str(effective_spec.get("disabled_tip") or ""), 150)
if selected and hint_text and hint_text != value.text():
hint = QLabel(hint_text)
hint.setObjectName("propertyCardHint")
hint.setWordWrap(True)
card_layout.addWidget(hint)
row_widgets["hint_label"] = hint
self.property_card_rows[row] = row_widgets
card.style().unpolish(card)
card.style().polish(card)
return card
current_row = QHBoxLayout()
current_row.setContentsMargins(0, 0, 0, 0)
current_label = QLabel("当前值")
current_label.setObjectName("propertyCardMetaLabel")
current_row.addWidget(current_label, 0)
current_text = str(effective_spec.get("current_text", ""))
current_value = QLabel(_compact_property_card_text(current_text, 220 if selected else 118))
current_value.setObjectName("propertyCardValue")
current_value.setWordWrap(bool(selected))
current_value.setToolTip(current_text)
current_row.addWidget(current_value, 1)
card_layout.addLayout(current_row)
if not selected:
compact_parts: list[str] = []
scope_text = str(effective_spec.get("scope_text") or effective_spec.get("scope_label") or "").strip()
if scope_text:
compact_parts.append(scope_text)
target_text = self._property_target_text(row) or str(effective_spec.get("target_text", ""))
if editable and value_type != "command":
validation_error = (
"" if not target_text.strip() else self._property_target_validation_error(effective_spec, target_text)
)
row_changed = self._property_target_changed(effective_spec, target_text)
if validation_error:
compact_parts.append("目标无效")
elif row_changed:
compact_parts.append(f"目标 {target_text}")
elif editable and value_type == "command":
button_text = str(effective_spec.get("button_text") or "可执行").strip()
if button_text:
compact_parts.append(button_text)
status_text = str(effective_spec.get("status_text") or "").strip()
if status_text and not compact_parts:
compact_parts.append(status_text)
if compact_parts:
compact_meta = QLabel(_compact_property_card_text(" · ".join(compact_parts), 118))
compact_meta.setObjectName("propertyCardCompactMeta")
compact_meta.setWordWrap(True)
compact_meta.setToolTip(" · ".join(compact_parts))
card_layout.addWidget(compact_meta)
row_widgets["compact_meta"] = compact_meta
self.property_card_rows[row] = row_widgets
card.style().unpolish(card)
card.style().polish(card)
return card
if input_editable:
target_row = QHBoxLayout()
target_row.setContentsMargins(0, 0, 0, 0)
target_label = QLabel("目标值")
target_label.setObjectName("propertyCardMetaLabel")
target_row.addWidget(target_label, 0)
editor = QLineEdit(self._property_target_text(row) or str(effective_spec.get("target_text", "")))
editor.setObjectName("propertyCardTargetEditor")
editor.setToolTip(self._property_target_tooltip(effective_spec, editable=True))
editor.setPlaceholderText("输入目标值")
editor.setCursor(Qt.CursorShape.IBeamCursor)
editor.textChanged.connect(lambda _text="", _row=row: self._on_property_card_target_changed(_row))
editor.returnPressed.connect(lambda target_row=row: self.apply_property_row_edit(target_row))
target_row.addWidget(editor, 1)
card_layout.addLayout(target_row)
row_widgets["target_editor"] = editor
modes = spec.get("scope_modes")
if isinstance(modes, dict) and modes:
scope_row = QHBoxLayout()
scope_row.setContentsMargins(0, 0, 0, 0)
scope_label = QLabel("建模意图")
scope_label.setObjectName("propertyCardMetaLabel")
scope_row.addWidget(scope_label, 0)
combo = self._make_property_scope_combo(row, spec)
scope_row.addWidget(combo, 1)
card_layout.addLayout(scope_row)
row_widgets["scope_combo"] = combo
hint_text = self._property_card_hint_text(effective_spec, editable=editable)
hint = QLabel(hint_text, card)
hint.setObjectName("propertyCardHint")
hint.setWordWrap(True)
hint.setToolTip(str(effective_spec.get("enabled_tip") or effective_spec.get("disabled_tip") or hint_text))
card_layout.addWidget(hint)
hint.setVisible(bool(hint_text))
row_widgets["hint_label"] = hint
if editable and effective_spec.get("action"):
button = QPushButton("未改动")
button.setObjectName("propertyRowEditButton")
button.setCursor(Qt.CursorShape.PointingHandCursor)
button.setProperty("changed", False)
button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
button.clicked.connect(lambda _checked=False, target_row=row: self.apply_property_row_edit(target_row))
card_layout.addWidget(button)
status_label.setVisible(False)
row_widgets["action_button"] = button
self.property_card_rows[row] = row_widgets
card.style().unpolish(card)
card.style().polish(card)
return card
def _property_card_hint_text(self, spec: dict[str, object], *, editable: bool) -> str:
parts: list[str] = []
if editable:
scope_label = str(spec.get("scope_label") or "").strip()
if scope_label:
parts.append(f"建模意图:{scope_label}")
tip = str(spec.get("enabled_tip") or "").strip()
if tip:
parts.append(tip)
range_hint = self._property_range_hint(spec)
if range_hint:
parts.append(range_hint)
else:
tip = str(spec.get("disabled_tip") or spec.get("enabled_tip") or "").strip()
status = str(spec.get("status_text") or "").strip()
if tip:
parts.append(tip)
elif status:
parts.append(status)
return _compact_property_card_text(" ".join(part for part in parts if part), 150)
def _make_property_scope_combo(self, row: int, spec: dict[str, object]) -> NoWheelComboBox:
combo = NoWheelComboBox()
combo.setObjectName("propertyScopeCombo")
modes = spec.get("scope_modes")
default_scope = self._property_scope_value(row, spec) or self._property_scope_default(spec)
selected_index = 0
if isinstance(modes, dict):
for index, (scope_key, mode) in enumerate(modes.items()):
if not isinstance(mode, dict):
continue
label = str(mode.get("label") or scope_key)
if not bool(mode.get("enabled", True)):
label = f"{label}(不可用)"
combo.addItem(label, scope_key)
tip = str(mode.get("enabled_tip") or mode.get("disabled_tip") or "").strip()
if tip:
combo.setItemData(index, tip, Qt.ItemDataRole.ToolTipRole)
if scope_key == default_scope:
selected_index = index
combo.setCurrentIndex(selected_index)
combo.setToolTip(self._property_scope_tooltip(spec, default_scope))
combo.currentIndexChanged.connect(lambda _index=0, target_row=row: self._on_property_scope_changed(target_row))
return combo
def _refresh_property_editor(self) -> None:
if not hasattr(self, "property_table"):
return
@@ -1352,9 +1908,12 @@ class WindowStateMixin:
specs = self._sort_property_specs_for_display(self._property_editor_specs(info, action_info))
self.property_editor_specs = specs
self.property_table_expanded = False
self.property_editor_selected_row = None
self.property_command_active_key = ""
self.property_editor_updating = True
was_blocked = self.property_table.blockSignals(True)
try:
self.property_table.clearSpans()
self.property_table.setRowCount(len(specs))
for row, spec in enumerate(specs):
effective_spec = self._effective_property_spec(spec)
@@ -1363,12 +1922,16 @@ class WindowStateMixin:
input_editable = editable and value_type != "command"
label_item = self._property_table_item(str(effective_spec.get("label", "")), editable=False)
current_item = self._property_table_item(str(effective_spec.get("current_text", "")), editable=False)
scope_item = self._property_table_item(str(effective_spec.get("scope_text", "")), editable=False)
target_item = self._property_table_item(
"" if input_editable else str(effective_spec.get("target_text", "")),
span_value_columns = bool(effective_spec.get("span_value_columns")) and not editable
scope_item = self._property_table_item(
"" if span_value_columns else str(effective_spec.get("scope_text", "")),
editable=False,
)
action_text = "" if editable else str(effective_spec.get("status_text", ""))
target_item = self._property_table_item(
"" if input_editable or span_value_columns else str(effective_spec.get("target_text", "")),
editable=False,
)
action_text = "" if editable or span_value_columns else str(effective_spec.get("status_text", ""))
action_item = self._property_table_item(action_text, editable=False)
target_item.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
row_items = (label_item, current_item, scope_item, target_item, action_item)
@@ -1376,6 +1939,9 @@ class WindowStateMixin:
for column, item in enumerate(row_items):
item.setToolTip(item.toolTip() or item.text())
self.property_table.setItem(row, column, item)
if span_value_columns:
current_item.setToolTip(str(effective_spec.get("disabled_tip") or current_item.text()))
self.property_table.setSpan(row, PROPERTY_CURRENT_COLUMN, 1, 4)
self.property_table.setRowHeight(row, 28 if editable else 24)
if spec.get("scope_modes"):
self._set_property_scope_editor(row, spec)
@@ -1390,10 +1956,13 @@ class WindowStateMixin:
else:
self.property_table.removeCellWidget(row, PROPERTY_TARGET_COLUMN)
self.property_table.removeCellWidget(row, PROPERTY_ACTION_COLUMN)
self._rebuild_property_command_bar()
self._rebuild_property_cards()
self._resize_property_table_height()
finally:
self.property_table.blockSignals(was_blocked)
self.property_editor_updating = False
self._resize_property_table_columns()
self._update_property_apply_state()
def _sort_property_specs_for_display(self, specs: list[dict[str, object]]) -> list[dict[str, object]]:
@@ -1479,6 +2048,11 @@ class WindowStateMixin:
modes = spec.get("scope_modes")
if not isinstance(modes, dict) or not modes:
return ""
card_widget = self._property_card_widgets(row).get("scope_combo")
if isinstance(card_widget, NoWheelComboBox):
value = card_widget.currentData()
if value in modes:
return str(value)
widget = self.property_table.cellWidget(row, PROPERTY_SCOPE_COLUMN) if hasattr(self, "property_table") else None
if isinstance(widget, NoWheelComboBox):
value = widget.currentData()
@@ -1578,8 +2152,30 @@ class WindowStateMixin:
effective_spec = self._effective_property_spec(specs[row], row=row)
editable = bool(effective_spec.get("editable") and effective_spec.get("enabled"))
input_editable = editable and str(effective_spec.get("value_type", "number")) != "command"
card_widgets = self._property_card_widgets(row)
card_scope_widget = card_widgets.get("scope_combo")
if isinstance(card_scope_widget, NoWheelComboBox):
selected_scope = card_scope_widget.currentData()
card_scope_widget.setToolTip(self._property_scope_tooltip(specs[row], self._property_scope_value(row, specs[row])))
card_target_widget = card_widgets.get("target_editor")
if isinstance(card_target_widget, QLineEdit):
card_target_widget.setEnabled(input_editable)
card_target_widget.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
hint_label = card_widgets.get("hint_label")
if isinstance(hint_label, QLabel):
hint_text = self._property_card_hint_text(effective_spec, editable=editable)
hint_label.setText(hint_text)
hint_label.setVisible(bool(hint_text))
scope_widget = self.property_table.cellWidget(row, PROPERTY_SCOPE_COLUMN)
if isinstance(scope_widget, NoWheelComboBox):
if "selected_scope" in locals():
target_index = scope_widget.findData(selected_scope)
if target_index >= 0 and target_index != scope_widget.currentIndex():
was_blocked = scope_widget.blockSignals(True)
try:
scope_widget.setCurrentIndex(target_index)
finally:
scope_widget.blockSignals(was_blocked)
scope_widget.setToolTip(self._property_scope_tooltip(specs[row], self._property_scope_value(row, specs[row])))
target_widget = self.property_table.cellWidget(row, PROPERTY_TARGET_COLUMN)
if isinstance(target_widget, QLineEdit):
@@ -1593,6 +2189,19 @@ class WindowStateMixin:
action_item.setText(str(effective_spec.get("status_text", "")))
self._update_property_apply_state()
def _on_property_card_target_changed(self, row: int) -> None:
card_widget = self._property_card_widgets(row).get("target_editor")
table_widget = self.property_table.cellWidget(row, PROPERTY_TARGET_COLUMN) if hasattr(self, "property_table") else None
if isinstance(card_widget, QLineEdit) and isinstance(table_widget, QLineEdit):
text = card_widget.text()
if table_widget.text() != text:
was_blocked = table_widget.blockSignals(True)
try:
table_widget.setText(text)
finally:
table_widget.blockSignals(was_blocked)
self._update_property_apply_state()
def _property_target_tooltip(self, spec: dict[str, object], *, editable: bool) -> str:
tip_key = "enabled_tip" if editable else "disabled_tip"
parts = [str(spec.get(tip_key, "")).strip()]
@@ -1625,7 +2234,7 @@ class WindowStateMixin:
def _resize_property_table_height(self) -> None:
if not hasattr(self, "property_table"):
return
row_count = self.property_table.rowCount()
row_count = len(getattr(self, "property_editor_specs", []) or [])
collapsed_rows = int(getattr(self, "property_table_collapsed_rows", 6) or 6)
expanded = bool(getattr(self, "property_table_expanded", False))
visible_rows = row_count if expanded else min(row_count, collapsed_rows)
@@ -1642,19 +2251,54 @@ class WindowStateMixin:
if expanded or not has_hidden_rows
else Qt.ScrollBarPolicy.ScrollBarAsNeeded
)
if hasattr(self, "property_card_scroll"):
card_visible_rows = self._visible_property_card_rows()
card_heights: list[int] = []
for row, _spec in card_visible_rows:
card = self._property_card_widgets(row).get("card")
if isinstance(card, QFrame):
card_heights.append(max(int(card.sizeHint().height()), 40))
if card_heights:
content_height = sum(card_heights) + max(0, len(card_heights) - 1) * 4 + 10
else:
content_height = 96
card_height = content_height
card_height = min(card_height, 560 if expanded else 360)
card_height = max(96, card_height)
if hasattr(self, "property_card_container"):
self.property_card_container.setMinimumHeight(content_height)
self.property_card_scroll.setMinimumHeight(card_height)
self.property_card_scroll.setMaximumHeight(card_height)
self.property_card_scroll.setVerticalScrollBarPolicy(
Qt.ScrollBarPolicy.ScrollBarAsNeeded if content_height > card_height else Qt.ScrollBarPolicy.ScrollBarAlwaysOff
)
if hasattr(self, "property_expand_button"):
self.property_expand_button.setVisible(has_hidden_rows)
if expanded:
self.property_expand_button.setText(f"收起{collapsed_rows}")
self.property_expand_button.setToolTip("收起参数列表,只保留最常用的前几项")
self.property_expand_button.setText(f"收起诊断信息,保留{collapsed_rows}")
self.property_expand_button.setToolTip("收起只读诊断信息,保留可编辑参数和关键说明")
else:
self.property_expand_button.setText(f"展开全部参数 ({row_count} 项)")
self.property_expand_button.setToolTip("展开完整参数列表;参数化建模按钮会继续留在下方。")
self.property_expand_button.setText(f"更多诊断信息 ({row_count} 项)")
self.property_expand_button.setToolTip("展开完整参数、识别依据和一级关系诊断;参数化建模按钮会继续留在下方。")
self._resize_property_table_columns()
def _resize_property_table_columns(self) -> None:
if not hasattr(self, "property_table"):
return
table = self.property_table
viewport_width = int(table.viewport().width()) if table.viewport() is not None else int(table.width())
if viewport_width <= 0:
return
widths = _property_table_column_widths(viewport_width)
for column, width in enumerate(widths):
table.setColumnWidth(column, width)
def toggle_property_table_expanded(self) -> None:
if not hasattr(self, "property_table"):
return
self.property_table_expanded = not bool(getattr(self, "property_table_expanded", False))
self._rebuild_property_command_bar()
self._rebuild_property_cards()
self._resize_property_table_height()
def _property_table_item(self, text: str, *, editable: bool) -> QTableWidgetItem:
@@ -1764,9 +2408,34 @@ class WindowStateMixin:
action_info: dict[str, object],
) -> list[dict[str, object]]:
root_rows = self._feature_property_specs(root_specs, action_info)
context_rows: list[dict[str, object]] = []
context_note = str(action_info.get("feature_context_note") or "").strip()
if context_note:
level_text = str(action_info.get("feature_detection_level") or "").strip()
associated_count = _int_or_none(action_info.get("associated_feature_count"))
count_text = "" if associated_count is None else f";关联特征 {associated_count}"
context_rows.append(
{
"key": "feature_context_note",
"label": "关联探测",
"current_text": context_note,
"current_raw": context_note,
"target_text": "",
"editable": False,
"enabled": False,
"status_text": "说明",
"scope_text": f"{level_text}{count_text}".strip(""),
"disabled_tip": (
"这里说明当前特征探测级别和已经找到的一级/二级关联特征;"
"有关联尺寸时,会作为带“关联 Face”的可修改行显示在同一张参数表里。"
),
"pin_top": True,
"span_value_columns": True,
}
)
associated = action_info.get("associated_feature_infos")
if not isinstance(associated, (list, tuple)) or not associated:
return root_rows
return context_rows + root_rows
root_dimensions = [dict(spec) for spec in root_rows if spec.get("parameter_role") == "dimension"]
root_explanations = [dict(spec) for spec in root_rows if spec.get("parameter_role") != "dimension"]
@@ -1789,7 +2458,7 @@ class WindowStateMixin:
related["association_index"] = index
related_rows.append(related)
return root_dimensions + related_rows + root_explanations
return context_rows + root_dimensions + related_rows + root_explanations
def _ordered_property_info_items(self, info: dict[str, object]) -> list[tuple[str, object]]:
items = self._ordered_info_items(info)
@@ -2266,6 +2935,7 @@ class WindowStateMixin:
"status_text": "说明",
"disabled_tip": tip or text,
"pin_top": pin_top,
"span_value_columns": True,
}
)
@@ -2624,6 +3294,36 @@ class WindowStateMixin:
)
if has_edge:
topology_depth = _int_or_none(action_info.get("topology_relation_depth"))
if topology_depth == 1:
selected_edge_count = _int_or_none(action_info.get("selected_edge_count")) or 1
vertex_count = _int_or_none(action_info.get("first_level_vertex_count")) or 0
adjacent_edge_count = _int_or_none(action_info.get("first_level_adjacent_edge_count")) or 0
adjacent_face_count = _int_or_none(action_info.get("first_level_adjacent_face_count")) or 0
included_edge_count = _int_or_none(action_info.get("first_level_edge_count")) or 0
topology_note = str(action_info.get("first_level_topology_note") or "").strip()
fact_summary = str(action_info.get("first_level_fact_summary") or "").strip()
ignored_note = str(action_info.get("topology_ignored_relation_note") or "").strip()
topology_tip = "\n".join(
item
for item in (
topology_note,
fact_summary,
ignored_note,
"当前阶段只把被选 Edge、端点 Vertex、共享端点相邻 Edge 和直接包含该 Edge 的 Face 作为一级关系;不会自动递归传播到二级、三级关系。",
)
if item
)
add_readonly_spec(
key="edge_first_level_topology",
label="一级关系",
text=(
f"当前 Edge {selected_edge_count} 条;端点 Vertex {vertex_count} 个;"
f"共享端点相邻 Edge {adjacent_edge_count} 条;直接相邻 Face {adjacent_face_count} 个;"
f"一级范围 Edge {included_edge_count} 条。"
),
tip=topology_tip,
)
add_readonly_spec(
key="edge_edit_semantics",
label="建模意图",
@@ -4702,18 +5402,46 @@ class WindowStateMixin:
)
for row, spec in enumerate(getattr(self, "property_editor_specs", [])):
effective_spec = self._effective_property_spec(spec, row=row)
target_widget = self.property_table.cellWidget(row, PROPERTY_TARGET_COLUMN)
card_widgets = self._property_card_widgets(row)
target_widget = card_widgets.get("target_editor")
if not isinstance(target_widget, QLineEdit):
target_widget = self.property_table.cellWidget(row, PROPERTY_TARGET_COLUMN)
if isinstance(target_widget, QLineEdit):
editable = bool(effective_spec.get("editable") and effective_spec.get("enabled"))
input_editable = editable and str(effective_spec.get("value_type", "number")) != "command"
target_widget.setEnabled(input_editable)
target_widget.setToolTip(self._property_target_tooltip(effective_spec, editable=editable))
widget = self.property_table.cellWidget(row, PROPERTY_ACTION_COLUMN)
hint_label = card_widgets.get("hint_label")
if isinstance(hint_label, QLabel):
hint_text = self._property_card_hint_text(
effective_spec,
editable=bool(effective_spec.get("editable") and effective_spec.get("enabled")),
)
hint_label.setText(hint_text)
hint_label.setVisible(bool(hint_text))
card = card_widgets.get("card")
card_changed = False
card_invalid = False
if str(effective_spec.get("value_type", "number")) != "command":
text = self._property_target_text(row)
card_changed = self._property_target_changed(effective_spec, text)
card_invalid = bool(text.strip() and self._property_target_validation_error(effective_spec, text))
if isinstance(card, QFrame):
card_state = (bool(card_changed and not card_invalid), bool(card_invalid))
if getattr(card, "_geom_param_card_state", None) != card_state:
card.setProperty("changed", card_state[0])
card.setProperty("invalid", card_state[1])
card.style().unpolish(card)
card.style().polish(card)
setattr(card, "_geom_param_card_state", card_state)
widget = card_widgets.get("action_button")
if not isinstance(widget, QPushButton):
widget = self.property_table.cellWidget(row, PROPERTY_ACTION_COLUMN)
if isinstance(widget, QPushButton):
validation_error = ""
if str(effective_spec.get("value_type", "number")) == "command":
row_changed = True
widget.setText(str(effective_spec.get("button_text") or "执行"))
widget.setText(str(effective_spec.get("button_text") or "执行当前命令"))
enabled = bool(has_model and effective_spec.get("enabled") and effective_spec.get("action"))
tooltip = f"执行“{effective_spec.get('label', '当前操作')}”。"
else:
@@ -4722,11 +5450,11 @@ class WindowStateMixin:
empty_target = not bool(text.strip())
validation_error = "" if empty_target else self._property_target_validation_error(effective_spec, text)
if empty_target:
widget.setText("输入")
widget.setText("输入目标值后应用")
elif validation_error:
widget.setText("无效")
widget.setText("目标无效")
else:
widget.setText("应用" if row_changed else "未改动")
widget.setText("应用当前命令" if row_changed else "修改目标值后应用")
enabled = bool(
has_model
and effective_spec.get("enabled")
@@ -4753,6 +5481,10 @@ class WindowStateMixin:
tooltip = f"{tooltip}\n\n{range_hint}"
widget.setProperty("changed", bool(row_changed and not validation_error))
widget.setProperty("invalid", bool(validation_error))
card = card_widgets.get("card")
if isinstance(card, QFrame):
card.setProperty("changed", bool(row_changed and not validation_error))
card.setProperty("invalid", bool(validation_error))
button_state = (
widget.text(),
bool(enabled),
@@ -4766,6 +5498,9 @@ class WindowStateMixin:
widget.style().unpolish(widget)
widget.style().polish(widget)
widget.setEnabled(enabled)
if isinstance(card, QFrame):
card.style().unpolish(card)
card.style().polish(card)
setattr(widget, "_geom_param_button_state", button_state)
if not hasattr(self, "apply_property_button"):
return
@@ -4807,6 +5542,9 @@ class WindowStateMixin:
def _property_target_text(self, row: int) -> str:
if not hasattr(self, "property_table"):
return ""
card_widget = self._property_card_widgets(row).get("target_editor")
if isinstance(card_widget, QLineEdit):
return card_widget.text().strip()
widget = self.property_table.cellWidget(row, PROPERTY_TARGET_COLUMN)
if isinstance(widget, QLineEdit):
return widget.text().strip()