643 lines
35 KiB
Python
643 lines
35 KiB
Python
"""Native Windows Qt clicks through QInventor sketch and extrusion workflows."""
|
||
import json
|
||
import os
|
||
import traceback
|
||
|
||
import FreeCAD as App
|
||
import FreeCADGui as Gui
|
||
from PySide import QtCore, QtGui, QtWidgets
|
||
try:
|
||
from PySide import QtTest
|
||
except ImportError:
|
||
try:
|
||
from PySide6 import QtTest, QtOpenGLWidgets
|
||
except ImportError:
|
||
from PySide2 import QtTest
|
||
|
||
|
||
def run(output):
|
||
import Part
|
||
import qi_commands as commands
|
||
import qi_ui as ui
|
||
import qi_localization as locale
|
||
import qi_model as model
|
||
os.makedirs(output, exist_ok=True)
|
||
report = {'status': 'running', 'passed': 0, 'failed': 0, 'checks': [],
|
||
'input_delivery':'Qt events on native viewport', 'os_pointer_access':False}
|
||
main = Gui.getMainWindow()
|
||
main.resize(1440, 940)
|
||
# A Hidden Win32 startup can leave Qt's visible flag set while the
|
||
# native window has never been exposed. Force a real show transition.
|
||
main.hide()
|
||
main.showNormal()
|
||
main.raise_()
|
||
main.activateWindow()
|
||
assert QtTest.QTest.qWaitForWindowExposed(main.windowHandle(), 5000), 'Native main window is not exposed'
|
||
ribbon = ui.show_ribbon()
|
||
App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher').SetBool('LeaveSketchWithEscape', False)
|
||
App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher').SetBool('ShowDialogOnDistanceConstraint', True)
|
||
App.ParamGet('User parameter:BaseApp/Preferences/Mod/Sketcher').SetBool('ContinuousCreationMode', True)
|
||
created = []
|
||
context = {}
|
||
|
||
def settle(ms=250):
|
||
QtWidgets.QApplication.processEvents()
|
||
QtTest.QTest.qWait(ms)
|
||
|
||
def capture(name):
|
||
settle(350)
|
||
main.grab().save(os.path.join(output, name + '.png'))
|
||
|
||
def mouse_move(widget, point):
|
||
# QtTest.mouseMove may only reposition the desktop cursor. Deliver the
|
||
# complete movement event to FreeCAD's native viewport even when the
|
||
# Windows lock screen prevents the OS cursor from reaching this HWND.
|
||
global_point = widget.mapToGlobal(point)
|
||
QtGui.QCursor.setPos(global_point)
|
||
event = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, QtCore.QPointF(point),
|
||
QtCore.QPointF(global_point), QtCore.Qt.NoButton,
|
||
QtCore.Qt.NoButton, QtCore.Qt.NoModifier)
|
||
QtWidgets.QApplication.sendEvent(widget, event)
|
||
|
||
def click(point):
|
||
canvas = context['canvas']
|
||
mouse_move(canvas, point)
|
||
settle(80)
|
||
QtTest.QTest.mouseClick(canvas, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, point)
|
||
settle(100)
|
||
|
||
def canvas():
|
||
renderers = [w for w in main.findChildren(QtWidgets.QWidget)
|
||
if w.isVisible() and w.metaObject().className() == 'QOpenGLWidget'
|
||
and w.parent() and w.parent().metaObject().className() == 'Gui::View3DInventorViewer']
|
||
assert renderers, 'Native OpenGL drafting canvas not found'
|
||
# Send input to the actual viewport under the pointer. Revit routes
|
||
# renderer events through Quarter; addressing the parent directly
|
||
# bypasses that route. Native hover selection is audited separately.
|
||
renderer = max(renderers, key=lambda w: w.width() * w.height())
|
||
# FreeCAD supplies the widget through QtWidgets; registering the
|
||
# OpenGL binding gives QtTest the concrete viewport wrapper.
|
||
if 'QtOpenGLWidgets' in globals():
|
||
import shiboken6
|
||
renderer = shiboken6.wrapInstance(shiboken6.getCppPointer(renderer)[0], QtOpenGLWidgets.QOpenGLWidget)
|
||
return renderer
|
||
|
||
def check(name, fn):
|
||
try:
|
||
result = fn()
|
||
report['checks'].append({'name': name, 'passed': True, 'result': result})
|
||
report['passed'] += 1
|
||
except Exception:
|
||
report['checks'].append({'name': name, 'passed': False, 'error': traceback.format_exc()})
|
||
report['failed'] += 1
|
||
capture('ribbon-failure-' + str(report['failed']))
|
||
with open(os.path.join(output, 'ribbon-suite.json'), 'w', encoding='utf-8') as stream:
|
||
json.dump(report, stream, ensure_ascii=False, indent=2, default=str)
|
||
|
||
def new_sketch():
|
||
doc = model.new_part('Проверка построения мышью')
|
||
created.append(doc.Name)
|
||
old = ui.choose_plane
|
||
ui.choose_plane = lambda: 'XY'
|
||
try:
|
||
Gui.runCommand('QI_NewSketch')
|
||
finally:
|
||
ui.choose_plane = old
|
||
settle(1400)
|
||
sketch = commands._editing()
|
||
assert sketch and sketch.isDerivedFrom('Sketcher::SketchObject')
|
||
assert sketch.GeometryCount == 0
|
||
assert sketch.ViewObject.Autoconstraints and sketch.ViewObject.AvoidRedundant
|
||
edit_controls = {}
|
||
for name in ('QI_FinishSketch','QI_CancelSketch'):
|
||
button = ribbon.buttons[name][0]
|
||
visible = button.isVisible() and button.visibleRegion().contains(button.rect().center())
|
||
edit_controls[name] = {'visible_without_scrolling':visible, 'caption':button.text()}
|
||
assert visible, 'Primary sketch control is clipped at 1440 px: ' + name
|
||
context.update(doc=doc, sketch=sketch, canvas=canvas())
|
||
return {'native_editor': sketch.TypeId, 'plane': 'XY', 'canvas': [context['canvas'].width(), context['canvas'].height()],
|
||
'camera': Gui.activeDocument().activeView().getCamera(),
|
||
'exposed':main.windowHandle().isExposed(), 'edit_controls':edit_controls,
|
||
'navigation': App.ParamGet('User parameter:BaseApp/Preferences/View').GetString('NavigationStyle')}
|
||
|
||
def rectangle():
|
||
sketch = context['sketch']
|
||
assert sketch.GeometryCount == 0
|
||
button = ribbon.buttons['QI_Rectangle'][0]
|
||
assert button.isEnabled()
|
||
QtTest.QTest.mouseClick(button, QtCore.Qt.LeftButton)
|
||
settle(100)
|
||
widget = context['canvas']
|
||
first = QtCore.QPoint(int(widget.width() * 0.28), int(widget.height() * 0.64))
|
||
second = QtCore.QPoint(int(widget.width() * 0.44), int(widget.height() * 0.36))
|
||
click(first)
|
||
click(second)
|
||
QtTest.QTest.keyClick(widget, QtCore.Qt.Key_Escape)
|
||
settle(150)
|
||
assert sketch.GeometryCount == 4, 'Mouse rectangle created %s geometries' % sketch.GeometryCount
|
||
assert all(g.__class__.__name__ == 'LineSegment' for g in sketch.Geometry)
|
||
assert commands._editing() == sketch, 'Esc left sketch instead of cancelling geometry tool'
|
||
context.update(first=first, second=second)
|
||
capture('ribbon-mouse-rectangle')
|
||
Gui.activeDocument().activeView().getViewer().grabFramebuffer().save(os.path.join(output, 'ribbon-native-framebuffer.png'))
|
||
wire = Part.Wire([g.toShape() for g in sketch.Geometry])
|
||
assert wire.isClosed()
|
||
context['doc'].recompute()
|
||
settle(100)
|
||
return {'geometry_count': sketch.GeometryCount, 'constraints': [c.Type for c in sketch.Constraints], 'closed': wire.isClosed()}
|
||
|
||
def shortcuts():
|
||
native_keys = {key: Gui.Command.listByShortcut(key) for key in ['L','C','D','E','Ctrl+N','Ctrl+O','Ctrl+S']}
|
||
assert not native_keys['L'], native_keys
|
||
assert not native_keys['C'], native_keys
|
||
assert not native_keys['D'], native_keys
|
||
assert not native_keys['E'], native_keys
|
||
assert 'QI_Dimension' in ribbon.shortcuts and ribbon.shortcuts['QI_Dimension'].isEnabled()
|
||
assert 'QI_Pad' in ribbon.shortcuts and not ribbon.shortcuts['QI_Pad'].isEnabled()
|
||
assert ribbon.shortcuts['QI_Dimension'].key().toString() == 'D'
|
||
return {'native_keys': native_keys, 'ribbon_keys': {n:s.key().toString() for n,s in ribbon.shortcuts.items()}}
|
||
|
||
def dimension():
|
||
sketch = context['sketch']
|
||
widget = context['canvas']
|
||
before = len(sketch.Constraints)
|
||
key_signals = []
|
||
ribbon.shortcuts['QI_Dimension'].activated.connect(lambda: key_signals.append('D'))
|
||
ribbon.shortcuts['QI_Dimension'].activatedAmbiguously.connect(lambda: key_signals.append('ambiguous'))
|
||
Gui.Selection.clearSelection()
|
||
horizontal = [(i, g) for i, g in enumerate(sketch.Geometry)
|
||
if abs(g.StartPoint.y - g.EndPoint.y) < 1e-6]
|
||
edge_index, geometry = max(horizontal, key=lambda item: item[1].StartPoint.y)
|
||
expected_edge = 'Edge' + str(edge_index + 1)
|
||
# Project actual geometry after native layout/camera updates. Keeping
|
||
# the point away from the constraint glyph avoids picking that glyph.
|
||
local_point = geometry.StartPoint * .35 + geometry.EndPoint * .65
|
||
world_point = sketch.getGlobalPlacement().multVec(local_point)
|
||
view = Gui.activeDocument().activeView()
|
||
projected = view.getPointOnScreen(world_point)
|
||
physical_size = view.getViewer().getSoRenderManager().getViewportRegion().getViewportSizePixels().getValue()
|
||
ratios = [float(physical_size[0]) / widget.width(), float(physical_size[1]) / widget.height()]
|
||
edge = QtCore.QPoint(round(float(projected[0]) / ratios[0]),
|
||
round(widget.height() - float(projected[1]) / ratios[1]))
|
||
coordinates = {'expected_edge':expected_edge, 'local_point':list(local_point),
|
||
'world_point':list(world_point), 'projected_physical':list(projected),
|
||
'qt_point':[edge.x(), edge.y()], 'qt_size':[widget.width(), widget.height()],
|
||
'physical_size':list(physical_size), 'ratios':ratios,
|
||
'input_delivery':'Qt events on native viewport', 'os_pointer_access':False}
|
||
widget.setFocus(QtCore.Qt.OtherFocusReason)
|
||
main.raise_()
|
||
main.activateWindow()
|
||
window_active = QtTest.QTest.qWaitForWindowActive(main.windowHandle(), 1500)
|
||
settle(100)
|
||
focus_window = QtGui.QGuiApplication.focusWindow()
|
||
coordinates['input_state'] = {
|
||
'wait_for_window_active':window_active, 'main_active':main.isActiveWindow(),
|
||
'focus_window_title':focus_window.title() if focus_window else None,
|
||
'renderer_mouse_tracking':widget.hasMouseTracking(),
|
||
'renderer_focus_policy':str(widget.focusPolicy()), 'renderer_has_focus':widget.hasFocus(),
|
||
'native_parent_mouse_tracking':widget.parentWidget().hasMouseTracking(),
|
||
'edited_object':getattr(commands._editing(),'Name',None),
|
||
'sketch_visible':sketch.ViewObject.Visibility,
|
||
'main_frame':[main.frameGeometry().x(),main.frameGeometry().y(),main.frameGeometry().width(),main.frameGeometry().height()],
|
||
'canvas_global_origin':[widget.mapToGlobal(QtCore.QPoint(0,0)).x(),widget.mapToGlobal(QtCore.QPoint(0,0)).y()],
|
||
'screens':[[s.geometry().x(),s.geometry().y(),s.geometry().width(),s.geometry().height()] for s in QtGui.QGuiApplication.screens()],
|
||
}
|
||
# Direct native Qt motion follows the projected pixel exactly; no
|
||
# selection API or coordinate jitter substitutes for the real pick.
|
||
mouse_move(widget, edge)
|
||
settle(160)
|
||
preselection = Gui.Selection.getPreselection()
|
||
coordinates['preselection_object'] = getattr(preselection,'ObjectName',None)
|
||
coordinates['preselection_subelements'] = list(getattr(preselection,'SubElementNames',[]))
|
||
context['dimension_coordinates'] = coordinates
|
||
assert coordinates['preselection_object'] == sketch.Name and expected_edge in coordinates['preselection_subelements'], coordinates
|
||
# Native Sketcher commits its edge pick on release. Give the viewport
|
||
# an event-loop turn while the one physical gesture is held, instead
|
||
# of QtTest's back-to-back press/release without event processing.
|
||
QtTest.QTest.mousePress(widget, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, edge)
|
||
try:
|
||
settle(80)
|
||
coordinates['selection_after_press'] = [
|
||
(s.Object.Name, list(s.SubElementNames)) for s in Gui.Selection.getSelectionEx()]
|
||
finally:
|
||
QtTest.QTest.mouseRelease(widget, QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, edge)
|
||
selection_deadline = QtCore.QElapsedTimer()
|
||
selection_deadline.start()
|
||
while selection_deadline.elapsed() < 1000:
|
||
settle(40)
|
||
if any(s.Object.Name == sketch.Name and expected_edge in s.SubElementNames
|
||
for s in Gui.Selection.getSelectionEx()):
|
||
break
|
||
selection_before = [(s.Object.Name, list(s.SubElementNames)) for s in Gui.Selection.getSelectionEx()]
|
||
coordinates['selection'] = selection_before
|
||
context['dimension_coordinates'] = coordinates
|
||
assert any(name == sketch.Name and expected_edge in subelements
|
||
for name, subelements in selection_before), coordinates
|
||
context['dimension_selection_method'] = 'native viewport Qt mouse events; actual selection verified'
|
||
# Start the same general dimension tool the user gets with D.
|
||
QtWidgets.QApplication.setActiveWindow(main)
|
||
main.activateWindow()
|
||
widget.setFocus(QtCore.Qt.OtherFocusReason)
|
||
settle(400)
|
||
keyboard_target = QtWidgets.QApplication.focusWidget()
|
||
coordinates['dimension_key_state'] = {
|
||
'active_main_window': QtWidgets.QApplication.activeWindow() == main,
|
||
'renderer_has_focus': widget.hasFocus(),
|
||
'focused_class': keyboard_target.metaObject().className() if keyboard_target else None,
|
||
'focused_object_name': keyboard_target.objectName() if keyboard_target else None,
|
||
'shortcut_enabled': ribbon.shortcuts['QI_Dimension'].isEnabled(),
|
||
'edited_object': getattr(commands._editing(), 'Name', None),
|
||
}
|
||
# FreeCAD may focus the native viewer through the renderer's focus
|
||
# proxy. Keyboard input belongs to that actual Qt focus widget.
|
||
coordinates['dimension_key_state']['focus_in_viewport'] = bool(
|
||
keyboard_target and (keyboard_target == widget or
|
||
keyboard_target.isAncestorOf(widget) or
|
||
widget.isAncestorOf(keyboard_target)))
|
||
assert all(coordinates['dimension_key_state'][key] for key in
|
||
('active_main_window', 'focus_in_viewport', 'shortcut_enabled')), coordinates
|
||
assert coordinates['dimension_key_state']['edited_object'] == sketch.Name, coordinates
|
||
# D consumes the actual mouse-selected native edge, then places the
|
||
# dimension line and opens FreeCAD's own datum editor.
|
||
accepted = []
|
||
attempts = [0]
|
||
timer = QtCore.QTimer(main)
|
||
def enter_value():
|
||
attempts[0] += 1
|
||
for dialog in QtWidgets.QApplication.topLevelWidgets():
|
||
if isinstance(dialog, QtWidgets.QDialog) and dialog.isVisible():
|
||
edits = [w for w in dialog.findChildren(QtWidgets.QLineEdit) if w.isVisible()]
|
||
if edits:
|
||
edits[0].setFocus()
|
||
edits[0].selectAll()
|
||
QtTest.QTest.keyClicks(edits[0], '40 mm')
|
||
QtTest.QTest.keyClick(edits[0], QtCore.Qt.Key_Return)
|
||
accepted.append(dialog.windowTitle())
|
||
timer.stop()
|
||
return
|
||
# A retry handles the native modal datum dialog appearing after
|
||
# the pick event rather than during the original mouse release.
|
||
if attempts[0] >= 15:
|
||
timer.stop()
|
||
timer.timeout.connect(enter_value)
|
||
timer.start(100)
|
||
QtTest.QTest.keyClick(keyboard_target, QtCore.Qt.Key_D)
|
||
settle(150)
|
||
coordinates['dimension_key_signals'] = key_signals
|
||
context['dimension_key_signals'] = key_signals
|
||
locale.localize_surface()
|
||
context['dimension_locale_candidates'] = locale.untranslated_surface()
|
||
capture('ribbon-dimension-tool-started')
|
||
click(QtCore.QPoint(edge.x(), max(10, edge.y() - 50)))
|
||
settle(350)
|
||
timer.stop()
|
||
timer.deleteLater()
|
||
QtTest.QTest.keyClick(widget, QtCore.Qt.Key_Escape)
|
||
settle(150)
|
||
dimensional = [c for c in sketch.Constraints if c.Type in ('Distance','DistanceX','DistanceY') and abs(abs(c.Value)-40)<1e-5]
|
||
assert len(sketch.Constraints) > before and dimensional, {'before':before,'constraints':[(c.Type,c.Value) for c in sketch.Constraints], 'dialogs':accepted, 'key_signals':key_signals, 'selection_before':selection_before, 'coordinates':coordinates}
|
||
assert accepted, 'Native datum dialog was not exercised'
|
||
assert sketch.solve() == 0
|
||
context['value_dialogs'] = accepted
|
||
capture('ribbon-driving-dimension')
|
||
return {'driving_value_mm': 40, 'dialogs': accepted, 'constraints': len(sketch.Constraints), 'selection_method':context['dimension_selection_method'], 'coordinates':coordinates, 'untranslated_candidates':context['dimension_locale_candidates']}
|
||
|
||
def finish_and_extrude():
|
||
sketch = context['sketch']
|
||
Gui.runCommand('QI_FinishSketch')
|
||
settle(500)
|
||
assert commands._editing() is None
|
||
assert not ribbon.shortcuts['QI_Dimension'].isEnabled()
|
||
assert ribbon.shortcuts['QI_Pad'].isEnabled()
|
||
widget = canvas()
|
||
widget.setFocus()
|
||
QtTest.QTest.keyClick(widget, QtCore.Qt.Key_E)
|
||
settle(350)
|
||
import qi_extrude
|
||
active = Gui.Control.activeDialog()
|
||
assert active is not None, 'Extrusion shortcut did not open task panel'
|
||
length = main.findChild(QtWidgets.QDoubleSpinBox, 'QI_ExtrudeLength')
|
||
assert length is not None and length.isVisible()
|
||
length.setValue(12)
|
||
settle(150)
|
||
boxes = [w for w in main.findChildren(QtWidgets.QDialogButtonBox) if w.isVisible() and w.button(QtWidgets.QDialogButtonBox.Ok)]
|
||
assert boxes
|
||
QtTest.QTest.mouseClick(boxes[0].button(QtWidgets.QDialogButtonBox.Ok), QtCore.Qt.LeftButton)
|
||
settle(400)
|
||
assert not Gui.Control.activeDialog()
|
||
bodies = [o for o in context['doc'].Objects if o.isDerivedFrom('PartDesign::Body') and o.Shape.Solids]
|
||
assert bodies and bodies[-1].Shape.isValid()
|
||
assert bodies[-1].Shape.Volume > 0
|
||
capture('ribbon-extruded-part')
|
||
return {'solids': len(bodies[-1].Shape.Solids), 'volume_mm3': bodies[-1].Shape.Volume}
|
||
|
||
def cancel_existing():
|
||
if Gui.Control.activeDialog():
|
||
boxes = [w for w in main.findChildren(QtWidgets.QDialogButtonBox) if w.isVisible() and w.button(QtWidgets.QDialogButtonBox.Cancel)]
|
||
if boxes:
|
||
boxes[0].button(QtWidgets.QDialogButtonBox.Cancel).click()
|
||
settle(100)
|
||
doc = model.new_part('Проверка отмены изменений')
|
||
created.append(doc.Name)
|
||
sketch = model.create_sketch_profile('rectangle', 40, 25, doc=doc)
|
||
geometry, constraints = sketch.GeometryCount, len(sketch.Constraints)
|
||
Gui.activeDocument().setEdit(sketch.Name)
|
||
settle(200)
|
||
sketch.addGeometry(Part.LineSegment(App.Vector(80,0,0), App.Vector(80,20,0)), False)
|
||
assert sketch.GeometryCount == geometry + 1
|
||
Gui.runCommand('QI_CancelSketch')
|
||
settle(250)
|
||
assert commands._editing() is None
|
||
assert sketch.GeometryCount == geometry and len(sketch.Constraints) == constraints
|
||
assert sketch.FullyConstrained
|
||
return {'original_geometry': geometry, 'restored_geometry': sketch.GeometryCount, 'constraints': constraints}
|
||
|
||
def localization():
|
||
locale.localize_surface()
|
||
samples = ['Sketcher', 'Pick point or edge', 'Switch mode', 'QILength', 'MapMode', 'New Assembly', 'Constraint12']
|
||
translated = {s: locale.translate_source(s) for s in samples}
|
||
assert all(re.search('[А-Яа-яЁё]', v) for v in translated.values())
|
||
candidates = locale.untranslated_surface()
|
||
with open(os.path.join(output, 'localization-visible-candidates.json'),'w',encoding='utf-8') as stream:
|
||
json.dump(candidates,stream,ensure_ascii=False,indent=2)
|
||
return {'catalog_sources':len(locale.TRANSLATIONS), 'samples':translated, 'untranslated_candidates':candidates}
|
||
|
||
def body_face_sketch():
|
||
doc = model.new_part('Проверка эскиза на компоненте')
|
||
created.append(doc.Name)
|
||
# This case isolates native container/attachment behavior. A native
|
||
# scoped feature avoids moving a fixture's unrelated document-level
|
||
# parameter controller into another Part scope.
|
||
body = doc.addObject('PartDesign::Body', 'Body')
|
||
body.Label = 'Деталь'
|
||
feature = body.newObject('PartDesign::Feature', 'BaseSolid')
|
||
feature.Label = 'Основание'
|
||
feature.Shape = Part.makeBox(40, 25, 8)
|
||
part = doc.addObject('App::Part', 'Component')
|
||
part.Label = 'Компонент'
|
||
part.addObject(body)
|
||
tip = body.Tip
|
||
doc.recompute()
|
||
Gui.Selection.clearSelection()
|
||
Gui.Selection.addSelection(body, 'Face1')
|
||
Gui.runCommand('QI_NewSketch')
|
||
settle(500)
|
||
sketch = commands._editing()
|
||
assert sketch and sketch.getParentGeoFeatureGroup() == body
|
||
support = sketch.AttachmentSupport
|
||
support_object = support[0][0] if isinstance(support[0], (tuple, list)) else support[0]
|
||
assert support_object == tip, repr(support)
|
||
assert sketch.MapMode == 'FlatFace'
|
||
Gui.runCommand('QI_CancelSketch')
|
||
settle(150)
|
||
assert body.Shape.isValid() and body.Shape.Volume > 0
|
||
return {'parent':body.TypeId, 'support':tip.TypeId, 'map_mode':'FlatFace', 'restored_volume_mm3':body.Shape.Volume}
|
||
|
||
def projection_buttons():
|
||
import qi_projection as projection
|
||
doc = model.new_part('Проверка связанных проекций')
|
||
created.append(doc.Name)
|
||
body = doc.addObject('PartDesign::Body', 'ProjectionBody')
|
||
body.Label = 'Деталь для проекции'
|
||
source = body.newObject('PartDesign::AdditiveBox', 'ProjectionSource')
|
||
source.Label = 'Исходное основание'
|
||
source.Length, source.Width, source.Height = 20, 10, 8
|
||
doc.recompute()
|
||
# The direct kernel fixture needs the same model framing as an
|
||
# opened project or a primitive created through the ribbon.
|
||
settle(250)
|
||
Gui.activeDocument().activeView().fitAll()
|
||
Gui.activeDocument().activeView().redraw()
|
||
settle(350)
|
||
face_name = next('Face%d' % (i + 1) for i, face in enumerate(source.Shape.Faces)
|
||
if face.normalAt(0, 0).z > .99)
|
||
Gui.Selection.clearSelection()
|
||
# Attachment and external-element selection are explicit native
|
||
# Selection API boundaries. The controls themselves receive Qt clicks.
|
||
Gui.Selection.addSelection(source, face_name)
|
||
Gui.runCommand('QI_NewSketch')
|
||
settle(700)
|
||
sketch = commands._editing()
|
||
assert sketch and sketch.getParentGeoFeatureGroup() == body
|
||
assert sketch.MapMode == 'FlatFace' and sketch.GeometryCount == 0
|
||
context['canvas'] = canvas()
|
||
controls = {}
|
||
|
||
def capture_projection(name):
|
||
Gui.activeDocument().activeView().fitAll()
|
||
Gui.activeDocument().activeView().redraw()
|
||
settle(300)
|
||
capture(name)
|
||
|
||
def click_control(name):
|
||
button = ribbon.buttons[name][0]
|
||
ui.select_tab('Эскиз')
|
||
parent = button.parentWidget()
|
||
while parent is not None:
|
||
if isinstance(parent, QtWidgets.QScrollArea):
|
||
parent.ensureWidgetVisible(button)
|
||
break
|
||
parent = parent.parentWidget()
|
||
settle(400)
|
||
assert button.isVisible() and button.isEnabled(), name
|
||
assert button.visibleRegion().contains(button.rect().center()), 'Ribbon button is clipped: ' + name
|
||
assert re.search('[А-Яа-яЁё]', button.text()), button.text()
|
||
assert re.search('[А-Яа-яЁё]', button.toolTip()), button.toolTip()
|
||
controls[name] = {'caption':button.text(), 'tooltip':button.toolTip(),
|
||
'activation':'actual Qt mouse click'}
|
||
QtTest.QTest.mouseClick(button, QtCore.Qt.LeftButton)
|
||
settle(250)
|
||
|
||
try:
|
||
click_control('QI_ProjectGeometry')
|
||
locale.localize_surface()
|
||
candidates = locale.untranslated_surface()
|
||
capture_projection('ribbon-projection-native-tool')
|
||
# The native continuous projection handler consumes AddSelection;
|
||
# it clears any selection that existed when its button was pressed.
|
||
assert not sketch.ExternalGeometry
|
||
Gui.Selection.addSelection(source, face_name)
|
||
settle(350)
|
||
doc.recompute()
|
||
settle(150)
|
||
ids = projection.linked_geometry_ids(sketch)
|
||
assert len(ids) == 4 and len(sketch.ExternalGeometry) == 1
|
||
metadata = projection._metadata(sketch)
|
||
assert all(metadata[gid]['defining'] for gid in ids)
|
||
assert sketch.GeometryCount == 0
|
||
assert len(sketch.Shape.Wires) == 1 and sketch.Shape.Wires[0].isClosed()
|
||
original_area = Part.Face(sketch.Shape.Wires[0]).Area
|
||
assert abs(original_area - 200) < 1e-6
|
||
QtTest.QTest.keyClick(context['canvas'], QtCore.Qt.Key_Escape)
|
||
settle(150)
|
||
assert commands._editing() == sketch
|
||
doc.openTransaction('Изменить длину основания')
|
||
source.Length = 30
|
||
doc.recompute()
|
||
doc.commitTransaction()
|
||
settle(250)
|
||
updated_area = Part.Face(sketch.Shape.Wires[0]).Area
|
||
assert abs(updated_area - 300) < 1e-6
|
||
assert sketch.solve() == 0
|
||
capture_projection('ribbon-associated-projection-regenerated')
|
||
Gui.Selection.clearSelection()
|
||
Gui.Selection.addSelection(sketch, 'ExternalEdge1')
|
||
selected = [(s.Object.Name, list(s.SubElementNames)) for s in Gui.Selection.getSelectionEx()]
|
||
assert any(name == sketch.Name and 'ExternalEdge1' in names for name, names in selected)
|
||
click_control('QI_BreakProjection')
|
||
assert not sketch.ExternalGeometry and not projection.linked_geometry_ids(sketch)
|
||
assert sketch.GeometryCount == 4
|
||
assert all(not sketch.getConstruction(i) for i in range(4))
|
||
assert abs(Part.Face(sketch.Shape.Wires[0]).Area - updated_area) < 1e-6
|
||
capture_projection('ribbon-projection-link-broken')
|
||
doc.undo()
|
||
doc.recompute()
|
||
settle(250)
|
||
assert len(sketch.ExternalGeometry) == 1 and sketch.GeometryCount == 0
|
||
assert len(projection.linked_geometry_ids(sketch)) == 4
|
||
assert abs(Part.Face(sketch.Shape.Wires[0]).Area - updated_area) < 1e-6
|
||
assert sketch.solve() == 0
|
||
assert not candidates, candidates
|
||
capture_projection('ribbon-projection-undo-restored')
|
||
return {'controls':controls, 'source_face':face_name, 'native_geo_ids':ids,
|
||
'source_length_mm':source.Length.Value,
|
||
'original_area_mm2':original_area, 'updated_area_mm2':updated_area,
|
||
'detached_geometry_count':4, 'undo_restored_link':True,
|
||
'selection_method':'native Selection API observer boundary; controls clicked with Qt',
|
||
'break_selection':selected, 'untranslated_candidates':candidates}
|
||
finally:
|
||
Gui.Selection.clearSelection()
|
||
if commands._editing() == sketch:
|
||
Gui.runCommand('QI_FinishSketch')
|
||
settle(150)
|
||
if doc.Name in App.listDocuments():
|
||
App.closeDocument(doc.Name)
|
||
|
||
def native_enum_editor():
|
||
doc = model.new_part('Проверка русского редактора свойств')
|
||
created.append(doc.Name)
|
||
obj = doc.addObject('App::FeaturePython', 'EnumProbe')
|
||
obj.Label = 'Проверка соединения'
|
||
obj.addProperty('App::PropertyEnumeration', 'JointType', 'Проверка', 'Тип соединения')
|
||
obj.JointType = ['Fixed', 'Revolute']
|
||
obj.JointType = 'Fixed'
|
||
doc.recompute()
|
||
Gui.Selection.clearSelection()
|
||
Gui.Selection.addSelection(obj)
|
||
settle(500)
|
||
locale.localize_surface()
|
||
editors = [w for w in main.findChildren(QtWidgets.QAbstractItemView)
|
||
if w.isVisible() and w.metaObject().className().endswith('PropertyEditor')]
|
||
target = None
|
||
for editor in editors:
|
||
tree_model = editor.model()
|
||
def find(parent=QtCore.QModelIndex()):
|
||
for row in range(tree_model.rowCount(parent)):
|
||
label = tree_model.index(row, 0, parent)
|
||
if str(label.data()) in ('Тип соединения', 'Joint Type', 'JointType'):
|
||
return tree_model.index(row, 1, parent)
|
||
found = find(label)
|
||
if found is not None:
|
||
return found
|
||
return None
|
||
index = find()
|
||
if index is not None:
|
||
target = (editor, index)
|
||
break
|
||
assert target, 'Native PropertyEnumeration row not found'
|
||
editor, index = target
|
||
editor.scrollTo(index)
|
||
editor.edit(index)
|
||
settle(200)
|
||
combos = [w for w in editor.findChildren(QtWidgets.QComboBox)
|
||
if w.isVisible() and w.findText('Revolute') >= 0]
|
||
assert combos, 'Native enum editor not created'
|
||
combo = combos[-1]
|
||
assert combo.currentText() == 'Fixed'
|
||
assert combo.property('QILocalizedStyle'), 'Collapsed enum text is not painted through localized style'
|
||
native_index = combo.model().index(combo.findText('Revolute'), combo.modelColumn(), combo.rootModelIndex())
|
||
delegate = combo.itemDelegate()
|
||
assert isinstance(delegate, locale.DisplayDelegate)
|
||
painted = delegate.display_text(native_index)
|
||
assert painted == locale.translate_source('Revolute') and re.search('[А-Яа-яЁё]', painted)
|
||
combo.showPopup()
|
||
settle(100)
|
||
popup = combo.view()
|
||
popup.scrollTo(native_index)
|
||
QtTest.QTest.mouseClick(popup.viewport(), QtCore.Qt.LeftButton, QtCore.Qt.NoModifier, popup.visualRect(native_index).center())
|
||
settle(250)
|
||
QtTest.QTest.keyClick(editor, QtCore.Qt.Key_Return)
|
||
settle(150)
|
||
assert obj.JointType == 'Revolute', 'Translated display corrupted canonical enum setter: '+str(obj.JointType)
|
||
capture('ribbon-russian-enum')
|
||
return {'painted_option':painted,'canonical_value':obj.JointType,'source_item_preserved':'Revolute'}
|
||
|
||
def line_circle_keys():
|
||
results = []
|
||
for key, expected in [(QtCore.Qt.Key_L, 'LineSegment'), (QtCore.Qt.Key_C, 'Circle')]:
|
||
doc = model.new_part('Проверка инструментов клавиатуры')
|
||
created.append(doc.Name)
|
||
original_chooser = ui.choose_plane
|
||
ui.choose_plane = lambda: 'XY'
|
||
try:
|
||
Gui.runCommand('QI_NewSketch')
|
||
finally:
|
||
ui.choose_plane = original_chooser
|
||
settle(1400)
|
||
sketch = commands._editing()
|
||
context['canvas'] = canvas()
|
||
widget = context['canvas']
|
||
QtWidgets.QApplication.setActiveWindow(main)
|
||
main.activateWindow()
|
||
widget.setFocus(QtCore.Qt.OtherFocusReason)
|
||
settle(400)
|
||
command_name = 'QI_Line' if expected == 'LineSegment' else 'QI_Circle'
|
||
signals = []
|
||
ribbon.shortcuts[command_name].activated.connect(lambda: signals.append(True))
|
||
ribbon.shortcuts[command_name].activatedAmbiguously.connect(lambda: signals.append('ambiguous'))
|
||
focus = QtWidgets.QApplication.focusWidget()
|
||
diagnostics = {'native': Gui.Command.listByShortcut('L' if expected == 'LineSegment' else 'C'),
|
||
'focus': str(focus.metaObject().className()) if focus else None,
|
||
'active': QtWidgets.QApplication.activeWindow() == main,
|
||
'enabled_before': ribbon.shortcuts[command_name].isEnabled(),
|
||
'visible': ribbon.isVisible()}
|
||
QtTest.QTest.keyClick(widget, key)
|
||
settle(100)
|
||
click(QtCore.QPoint(int(widget.width()*.28), int(widget.height()*.58)))
|
||
click(QtCore.QPoint(int(widget.width()*.40), int(widget.height()*.45)))
|
||
QtTest.QTest.keyClick(widget, QtCore.Qt.Key_Escape)
|
||
settle(150)
|
||
capture('ribbon-key-test-' + ('L' if expected=='LineSegment' else 'C'))
|
||
assert sketch.GeometryCount == 1, {'key':expected,'geometry':sketch.GeometryCount,'signals':signals,'enabled':ribbon.shortcuts[command_name].isEnabled(), 'diagnostics': diagnostics}
|
||
assert sketch.Geometry[0].__class__.__name__ == expected
|
||
results.append({'key': 'L' if expected=='LineSegment' else 'C', 'native_geometry':expected})
|
||
capture('ribbon-key-' + results[-1]['key'])
|
||
sketch_name = sketch.Name
|
||
Gui.runCommand('QI_CancelSketch')
|
||
settle(150)
|
||
assert doc.getObject(sketch_name) is None and commands._editing() is None
|
||
return results
|
||
|
||
import re
|
||
check('Blank XY sketch enters native edit via QI_NewSketch', new_sketch)
|
||
check('Rectangle drawn through native Qt mouse events creates native geometry', rectangle)
|
||
check('D/E and document shortcuts have one owner across workbenches', shortcuts)
|
||
check('Native Qt mouse events select an edge; D places a driving datum through its dialog', dimension)
|
||
check('Finish sketch exits active tool; E previews and creates real solid', finish_and_extrude)
|
||
check('Cancel existing native sketch restores original geometry and constraints', cancel_existing)
|
||
check('L and C draw actual native line/circle and cancel new sketch removes it', line_circle_keys)
|
||
check('Sketch on Body face inside App::Part remains in Body and attaches to its feature', body_face_sketch)
|
||
check('Projection and Break Link buttons create defining linked geometry, regenerate and undo', projection_buttons)
|
||
check('Russian native enum popup preserves canonical values and commits selected property', native_enum_editor)
|
||
check('Supplemental Russian translations and visible surface audit', localization)
|
||
# Closing documents removes test-only geometry, not user documents.
|
||
for name in created:
|
||
if name in App.listDocuments():
|
||
if App.ActiveDocument and App.ActiveDocument.Name == name and commands._editing():
|
||
Gui.activeDocument().resetEdit()
|
||
App.closeDocument(name)
|
||
report['status'] = 'passed' if report['failed'] == 0 else 'failed'
|
||
with open(os.path.join(output, 'ribbon-suite.json'),'w',encoding='utf-8') as stream:
|
||
json.dump(report,stream,ensure_ascii=False,indent=2,default=str)
|
||
return report
|