276 lines
9.6 KiB
Python
276 lines
9.6 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import ctypes
|
||
|
|
import ctypes.wintypes
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
from PySide6.QtCore import QEvent, QObject, QPoint, Qt, qInstallMessageHandler
|
||
|
|
from PySide6.QtTest import QTest
|
||
|
|
from PySide6.QtWidgets import QApplication, QMessageBox, QWidget
|
||
|
|
|
||
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||
|
|
if str(PROJECT_ROOT) not in sys.path:
|
||
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||
|
|
|
||
|
|
from step_editor.app import DEFAULT_MODEL_PATH, StepEditorWindow, _suppress_vtk_output_window
|
||
|
|
|
||
|
|
|
||
|
|
def _rect_tuple(hwnd: int) -> tuple[int, int, int, int]:
|
||
|
|
rect = ctypes.wintypes.RECT()
|
||
|
|
if not ctypes.windll.user32.GetWindowRect(hwnd, ctypes.byref(rect)):
|
||
|
|
return (0, 0, 0, 0)
|
||
|
|
return (int(rect.left), int(rect.top), int(rect.right), int(rect.bottom))
|
||
|
|
|
||
|
|
|
||
|
|
def _window_text(hwnd: int) -> str:
|
||
|
|
length = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
|
||
|
|
buffer = ctypes.create_unicode_buffer(max(length + 1, 1))
|
||
|
|
ctypes.windll.user32.GetWindowTextW(hwnd, buffer, len(buffer))
|
||
|
|
return buffer.value
|
||
|
|
|
||
|
|
|
||
|
|
def _class_name(hwnd: int) -> str:
|
||
|
|
buffer = ctypes.create_unicode_buffer(256)
|
||
|
|
ctypes.windll.user32.GetClassNameW(hwnd, buffer, len(buffer))
|
||
|
|
return buffer.value
|
||
|
|
|
||
|
|
|
||
|
|
def _visible_native_windows_for_process(pid: int) -> list[dict[str, object]]:
|
||
|
|
windows: list[dict[str, object]] = []
|
||
|
|
|
||
|
|
@ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
|
||
|
|
def callback(hwnd, _lparam):
|
||
|
|
window_pid = ctypes.wintypes.DWORD()
|
||
|
|
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(window_pid))
|
||
|
|
if int(window_pid.value) != pid:
|
||
|
|
return True
|
||
|
|
if not ctypes.windll.user32.IsWindowVisible(hwnd):
|
||
|
|
return True
|
||
|
|
left, top, right, bottom = _rect_tuple(int(hwnd))
|
||
|
|
width = max(0, right - left)
|
||
|
|
height = max(0, bottom - top)
|
||
|
|
if width <= 0 or height <= 0:
|
||
|
|
return True
|
||
|
|
windows.append(
|
||
|
|
{
|
||
|
|
"hwnd": int(hwnd),
|
||
|
|
"class": _class_name(int(hwnd)),
|
||
|
|
"title": _window_text(int(hwnd)),
|
||
|
|
"rect": [left, top, right, bottom],
|
||
|
|
"size": [width, height],
|
||
|
|
}
|
||
|
|
)
|
||
|
|
return True
|
||
|
|
|
||
|
|
ctypes.windll.user32.EnumWindows(callback, 0)
|
||
|
|
return windows
|
||
|
|
|
||
|
|
|
||
|
|
def _widget_text(widget: QWidget) -> str:
|
||
|
|
if isinstance(widget, QMessageBox):
|
||
|
|
return str(widget.text() or "")
|
||
|
|
getter = getattr(widget, "text", None)
|
||
|
|
if callable(getter):
|
||
|
|
try:
|
||
|
|
value = getter()
|
||
|
|
if isinstance(value, str):
|
||
|
|
return value
|
||
|
|
except Exception:
|
||
|
|
return ""
|
||
|
|
return ""
|
||
|
|
|
||
|
|
|
||
|
|
class WindowProbe(QObject):
|
||
|
|
def __init__(self, output_dir: Path) -> None:
|
||
|
|
super().__init__()
|
||
|
|
self.output_dir = output_dir
|
||
|
|
self.events: list[dict[str, object]] = []
|
||
|
|
self.native_samples: list[dict[str, object]] = []
|
||
|
|
self._native_seen: set[tuple[object, ...]] = set()
|
||
|
|
|
||
|
|
def eventFilter(self, watched, event):
|
||
|
|
if isinstance(watched, QWidget) and event.type() in {
|
||
|
|
QEvent.Type.Show,
|
||
|
|
QEvent.Type.Hide,
|
||
|
|
QEvent.Type.WindowActivate,
|
||
|
|
QEvent.Type.ToolTip,
|
||
|
|
}:
|
||
|
|
if watched.isWindow() or event.type() == QEvent.Type.ToolTip:
|
||
|
|
self.events.append(
|
||
|
|
{
|
||
|
|
"event": event.type().name,
|
||
|
|
"qt_class": type(watched).__name__,
|
||
|
|
"object_name": watched.objectName(),
|
||
|
|
"title": watched.windowTitle(),
|
||
|
|
"text": _widget_text(watched),
|
||
|
|
"is_window": watched.isWindow(),
|
||
|
|
"visible": watched.isVisible(),
|
||
|
|
"size": [watched.width(), watched.height()],
|
||
|
|
"flags": int(watched.windowFlags()),
|
||
|
|
}
|
||
|
|
)
|
||
|
|
return False
|
||
|
|
|
||
|
|
def sample_native(self, label: str) -> None:
|
||
|
|
for item in _visible_native_windows_for_process(os.getpid()):
|
||
|
|
key = (item["hwnd"], item["class"], item["title"], tuple(item["rect"]))
|
||
|
|
if key in self._native_seen:
|
||
|
|
continue
|
||
|
|
self._native_seen.add(key)
|
||
|
|
sample = dict(item)
|
||
|
|
sample["label"] = label
|
||
|
|
self.native_samples.append(sample)
|
||
|
|
|
||
|
|
def grab_qt_windows(self, label: str) -> None:
|
||
|
|
app = QApplication.instance()
|
||
|
|
if app is None:
|
||
|
|
return
|
||
|
|
for index, widget in enumerate(app.topLevelWidgets()):
|
||
|
|
if not widget.isVisible():
|
||
|
|
continue
|
||
|
|
name = widget.objectName() or type(widget).__name__
|
||
|
|
safe_name = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in name)
|
||
|
|
target = self.output_dir / f"{label}_qt_{index}_{safe_name}.png"
|
||
|
|
widget.grab().save(str(target))
|
||
|
|
|
||
|
|
def grab_native_windows(self, label: str) -> None:
|
||
|
|
app = QApplication.instance()
|
||
|
|
if app is None:
|
||
|
|
return
|
||
|
|
screen = app.primaryScreen()
|
||
|
|
if screen is None:
|
||
|
|
return
|
||
|
|
for index, item in enumerate(_visible_native_windows_for_process(os.getpid())):
|
||
|
|
hwnd = int(item["hwnd"])
|
||
|
|
safe_class = "".join(ch if ch.isalnum() or ch in "-_" else "_" for ch in str(item["class"]))
|
||
|
|
target = self.output_dir / f"{label}_native_{index}_{safe_class}_{hwnd}.png"
|
||
|
|
screen.grabWindow(hwnd).save(str(target))
|
||
|
|
|
||
|
|
def write_log(self) -> None:
|
||
|
|
payload = {
|
||
|
|
"qt_events": self.events,
|
||
|
|
"native_samples": self.native_samples,
|
||
|
|
}
|
||
|
|
(self.output_dir / "transient_windows_log.json").write_text(
|
||
|
|
json.dumps(payload, ensure_ascii=False, indent=2),
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _pump(app: QApplication, probe: WindowProbe, label: str, ms: int) -> None:
|
||
|
|
deadline = time.monotonic() + ms / 1000.0
|
||
|
|
while time.monotonic() < deadline:
|
||
|
|
app.processEvents()
|
||
|
|
probe.sample_native(label)
|
||
|
|
time.sleep(0.005)
|
||
|
|
|
||
|
|
|
||
|
|
def _find_face_click_point(window: StepEditorWindow) -> QPoint:
|
||
|
|
width = max(1, window.vtk_widget.width())
|
||
|
|
height = max(1, window.vtk_widget.height())
|
||
|
|
fractions = [0.5, 0.42, 0.58, 0.35, 0.65, 0.28, 0.72]
|
||
|
|
for fy in fractions:
|
||
|
|
for fx in fractions:
|
||
|
|
x = int(width * fx)
|
||
|
|
y_qt = int(height * fy)
|
||
|
|
y_vtk = int(height - y_qt)
|
||
|
|
target = window._pick_selection_target("Face", x, y_vtk)
|
||
|
|
if target is not None:
|
||
|
|
return QPoint(x, y_qt)
|
||
|
|
return QPoint(width // 2, height // 2)
|
||
|
|
|
||
|
|
|
||
|
|
def _click_first_property_card(window: StepEditorWindow) -> bool:
|
||
|
|
rows = getattr(window, "property_card_rows", {})
|
||
|
|
if not isinstance(rows, dict):
|
||
|
|
return False
|
||
|
|
for widgets in rows.values():
|
||
|
|
card = widgets.get("card") if isinstance(widgets, dict) else None
|
||
|
|
if isinstance(card, QWidget) and card.isVisible():
|
||
|
|
QTest.mouseClick(card, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, QPoint(20, max(2, card.height() // 2)))
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def main() -> int:
|
||
|
|
parser = argparse.ArgumentParser()
|
||
|
|
parser.add_argument("--output-dir", default="assets/screenshots/transient_probe")
|
||
|
|
parser.add_argument("--model", default=str(DEFAULT_MODEL_PATH))
|
||
|
|
parser.add_argument("--offscreen", action="store_true")
|
||
|
|
args = parser.parse_args()
|
||
|
|
|
||
|
|
if args.offscreen:
|
||
|
|
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||
|
|
|
||
|
|
output_dir = Path(args.output_dir)
|
||
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
qt_messages: list[str] = []
|
||
|
|
|
||
|
|
def message_handler(_mode, _context, message):
|
||
|
|
text = str(message)
|
||
|
|
qt_messages.append(text)
|
||
|
|
print(text, file=sys.stderr)
|
||
|
|
|
||
|
|
qInstallMessageHandler(message_handler)
|
||
|
|
_suppress_vtk_output_window()
|
||
|
|
app = QApplication(["transient-window-probe"])
|
||
|
|
probe = WindowProbe(output_dir)
|
||
|
|
app.installEventFilter(probe)
|
||
|
|
|
||
|
|
window = StepEditorWindow(Path(args.model), background_load=False)
|
||
|
|
window.show()
|
||
|
|
window.resize(1280, 820)
|
||
|
|
_pump(app, probe, "after_show", 300)
|
||
|
|
|
||
|
|
window.load_step(Path(args.model), background=False)
|
||
|
|
_pump(app, probe, "after_load", 800)
|
||
|
|
window.render_window.Render()
|
||
|
|
_pump(app, probe, "after_render", 300)
|
||
|
|
probe.grab_qt_windows("after_render")
|
||
|
|
probe.grab_native_windows("after_render")
|
||
|
|
|
||
|
|
window._set_selection_mode("Face")
|
||
|
|
point = _find_face_click_point(window)
|
||
|
|
QTest.mouseClick(window.vtk_widget, Qt.MouseButton.LeftButton, Qt.KeyboardModifier.NoModifier, point)
|
||
|
|
_pump(app, probe, "after_face_click", 900)
|
||
|
|
probe.grab_qt_windows("after_face_click")
|
||
|
|
probe.grab_native_windows("after_face_click")
|
||
|
|
|
||
|
|
clicked_card = _click_first_property_card(window)
|
||
|
|
_pump(app, probe, "after_property_click", 900)
|
||
|
|
probe.grab_qt_windows("after_property_click")
|
||
|
|
probe.grab_native_windows("after_property_click")
|
||
|
|
|
||
|
|
(output_dir / "probe_summary.json").write_text(
|
||
|
|
json.dumps(
|
||
|
|
{
|
||
|
|
"model": str(Path(args.model)),
|
||
|
|
"face_click_point": [point.x(), point.y()],
|
||
|
|
"selected_kind": getattr(window, "selected_kind", None),
|
||
|
|
"selected_face_id": getattr(window, "selected_face_id", None),
|
||
|
|
"clicked_property_card": clicked_card,
|
||
|
|
"qt_message_count": len(qt_messages),
|
||
|
|
"qt_messages": qt_messages[-50:],
|
||
|
|
},
|
||
|
|
ensure_ascii=False,
|
||
|
|
indent=2,
|
||
|
|
),
|
||
|
|
encoding="utf-8",
|
||
|
|
)
|
||
|
|
probe.write_log()
|
||
|
|
window.close()
|
||
|
|
_pump(app, probe, "after_close", 100)
|
||
|
|
app.quit()
|
||
|
|
print(f"transient window probe wrote {output_dir}")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|