"""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