Files

643 lines
35 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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