From 7094424820440e528a9b5f54cf90655d39c827ee Mon Sep 17 00:00:00 2001 From: Billy Huddleston Date: Thu, 8 Jan 2026 19:28:32 -0500 Subject: [PATCH] CAM: Refactor Machine Editor UI, replace QToolBox with tabs Major refactor of the Machine Editor to use QTabWidget for section navigation. Added tabbed spindle management with add/remove functionality, split machine configuration into Output Options, G-Code Blocks, and Processing Options tabs. Updated preferences UI to use tabs instead of QToolBox. src/Mod/CAM/Gui/Resources/preferences/PathJob.ui: - Replace QToolBox with QTabWidget for preferences tabs src/Mod/CAM/Path/Dressup/Gui/Preferences.py: - Use QWidget with vertical layout instead of QToolBox for dressup preferences src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py: - Refactor to use QTabWidget for editor sections - Implement tabbed spindle management with add/remove - Split configuration into Output Options, G-Code Blocks, and Processing Options tabs - Update post processor selection logic src/Mod/CAM/Path/Main/Gui/PreferencesJob.py: - Update to use tabWidget instead of toolBox src/Mod/CAM/Path/Tool/assets/ui/preferences.py: - Use QWidget and direct layout instead of QToolBox for asset preferences --- generate_machine_box.py | 164 +++++++++ .../CAM/Gui/Resources/preferences/PathJob.ui | 38 +-- src/Mod/CAM/Path/Dressup/Gui/Preferences.py | 9 +- .../Path/Machine/ui/editor/machine_editor.py | 314 ++++++++++++------ src/Mod/CAM/Path/Main/Gui/PreferencesJob.py | 2 +- .../CAM/Path/Tool/assets/ui/preferences.py | 68 ++-- 6 files changed, 424 insertions(+), 171 deletions(-) create mode 100644 generate_machine_box.py diff --git a/generate_machine_box.py b/generate_machine_box.py new file mode 100644 index 0000000000..47584c57b7 --- /dev/null +++ b/generate_machine_box.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +""" +FreeCAD Macro: Generate Machine Boundary Box + +This macro creates a wireframe box representing the working envelope +of a CNC machine based on its configuration. + +COORDINATE SYSTEM: +- Uses MACHINE coordinates (absolute travel limits of the machine) +- Not work coordinates (relative to workpiece) +- Shows the full extent the machine can move in X, Y, Z directions + +Author: Generated for FreeCAD CAM +""" + +import FreeCAD +import Part +import Path +from Path.Machine.models.machine import MachineFactory +import os + +def get_machine_file(): + """Prompt user to select a machine configuration file.""" + # Get available machine files + machines = MachineFactory.list_configuration_files() + machine_names = [name for name, path in machines if path is not None] + + if not machine_names: + FreeCAD.Console.PrintError("No machine configuration files found.\n") + return None + + # For now, use the first machine. In a real macro, you'd use a dialog + # to let the user choose + selected_name = machine_names[0] # Default to first + selected_path = None + for name, path in machines: + if name == selected_name and path: + selected_path = path + break + + if not selected_path: + FreeCAD.Console.PrintError("Could not find selected machine file.\n") + return None + + return selected_path + +def create_machine_boundary_box(machine_file, color=(1.0, 0.0, 0.0), line_width=2.0, draw_style="Dashed"): + """Create a wireframe box showing machine boundaries. + + Args: + machine_file: Path to the machine configuration file + color: RGB tuple for wire color (default: red) + line_width: Width of the wires (default: 2.0) + draw_style: "Solid", "Dashed", or "Dotted" (default: "Dashed") + """ + + try: + # Load the machine configuration + machine = MachineFactory.load_configuration(machine_file) + FreeCAD.Console.PrintMessage(f"Loaded machine: {machine.name}\n") + + # Get axis limits + x_min = y_min = z_min = float('inf') + x_max = y_max = z_max = float('-inf') + + # Find min/max for linear axes + for axis_name, axis_obj in machine.linear_axes.items(): + if axis_name.upper() == 'X': + x_min = min(x_min, axis_obj.min_limit) + x_max = max(x_max, axis_obj.max_limit) + elif axis_name.upper() == 'Y': + y_min = min(y_min, axis_obj.min_limit) + y_max = max(y_max, axis_obj.max_limit) + elif axis_name.upper() == 'Z': + z_min = min(z_min, axis_obj.min_limit) + z_max = max(z_max, axis_obj.max_limit) + + # Check if we have valid limits + if x_min == float('inf') or y_min == float('inf') or z_min == float('inf'): + FreeCAD.Console.PrintError("Machine does not have X, Y, Z linear axes defined.\n") + return None + + FreeCAD.Console.PrintMessage(f"Machine boundaries: X({x_min:.3f}, {x_max:.3f}), Y({y_min:.3f}, {y_max:.3f}), Z({z_min:.3f}, {z_max:.3f})\n") + FreeCAD.Console.PrintMessage("Note: These are MACHINE coordinates showing the absolute travel limits.\n") + FreeCAD.Console.PrintMessage("Work coordinates would be relative to the workpiece origin.\n") + + # Create the 8 corner points of the box + p1 = FreeCAD.Vector(x_min, y_min, z_min) + p2 = FreeCAD.Vector(x_max, y_min, z_min) + p3 = FreeCAD.Vector(x_max, y_max, z_min) + p4 = FreeCAD.Vector(x_min, y_max, z_min) + p5 = FreeCAD.Vector(x_min, y_min, z_max) + p6 = FreeCAD.Vector(x_max, y_min, z_max) + p7 = FreeCAD.Vector(x_max, y_max, z_max) + p8 = FreeCAD.Vector(x_min, y_max, z_max) + + # Create edges (12 edges for wireframe box) + edges = [ + Part.makeLine(p1, p2), # bottom face + Part.makeLine(p2, p3), + Part.makeLine(p3, p4), + Part.makeLine(p4, p1), + Part.makeLine(p5, p6), # top face + Part.makeLine(p6, p7), + Part.makeLine(p7, p8), + Part.makeLine(p8, p5), + Part.makeLine(p1, p5), # vertical edges + Part.makeLine(p2, p6), + Part.makeLine(p3, p7), + Part.makeLine(p4, p8), + ] + + # Create a compound of all edges (wireframe) + compound = Part.makeCompound(edges) + + # Create a new document if none exists + if not FreeCAD.ActiveDocument: + FreeCAD.newDocument("MachineBoundary") + + # Create the shape in the document + obj = FreeCAD.ActiveDocument.addObject("Part::Feature", f"MachineBoundary_{machine.name.replace(' ', '_')}") + obj.Shape = compound + obj.Label = f"Machine Boundary: {machine.name}" + + # Set visual properties + obj.ViewObject.ShapeColor = color + obj.ViewObject.LineWidth = line_width + obj.ViewObject.DrawStyle = draw_style + + FreeCAD.ActiveDocument.recompute() + + FreeCAD.Console.PrintMessage(f"Created machine boundary box for {machine.name}\n") + return obj + + except Exception as e: + FreeCAD.Console.PrintError(f"Error creating machine boundary box: {str(e)}\n") + return None + +def main(): + """Main macro function.""" + FreeCAD.Console.PrintMessage("FreeCAD Macro: Generate Machine Boundary Box\n") + + # Get machine file + machine_file = get_machine_file() + if not machine_file: + return + + # Create the boundary box with customizable appearance + # You can change these parameters: + # color: (R, G, B) tuple, e.g., (1.0, 0.0, 0.0) for red, (0.0, 1.0, 0.0) for green + # line_width: thickness of the wires + # draw_style: "Solid", "Dashed", or "Dotted" + obj = create_machine_boundary_box(machine_file, + color=(1.0, 0.0, 0.0), # Red + line_width=2.0, + draw_style="Dashed") # Broken/dashed lines + if obj: + FreeCAD.Console.PrintMessage("Macro completed successfully.\n") + else: + FreeCAD.Console.PrintError("Macro failed.\n") + +# Run the macro +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/Mod/CAM/Gui/Resources/preferences/PathJob.ui b/src/Mod/CAM/Gui/Resources/preferences/PathJob.ui index 73482d95e4..d4646a2db1 100644 --- a/src/Mod/CAM/Gui/Resources/preferences/PathJob.ui +++ b/src/Mod/CAM/Gui/Resources/preferences/PathJob.ui @@ -15,20 +15,12 @@ - + 0 - - - - 0 - 0 - 695 - 308 - - - + + General @@ -118,16 +110,8 @@ If left empty no template will be preselected. - - - - 0 - 0 - 695 - 480 - - - + + Post processor @@ -334,16 +318,8 @@ See the file save policy below on how to deal with name conflicts. - - - - 0 - 0 - 674 - 619 - - - + + Setup diff --git a/src/Mod/CAM/Path/Dressup/Gui/Preferences.py b/src/Mod/CAM/Path/Dressup/Gui/Preferences.py index 0d42921161..eb3e6630dc 100644 --- a/src/Mod/CAM/Path/Dressup/Gui/Preferences.py +++ b/src/Mod/CAM/Path/Dressup/Gui/Preferences.py @@ -36,15 +36,14 @@ def RegisterDressup(dressup): class DressupPreferencesPage: def __init__(self, parent=None): - self.form = QtGui.QToolBox() + self.form = QtGui.QWidget() self.form.setWindowTitle(translate("CAM_PreferencesPathDressup", "Dressups")) + + layout = QtGui.QVBoxLayout(self.form) pages = [] for dressup in _dressups: page = dressup.preferencesPage() - if hasattr(page, "icon") and page.icon: - self.form.addItem(page.form, page.icon, page.label) - else: - self.form.addItem(page.form, page.label) + layout.addWidget(page.form) pages.append(page) self.pages = pages diff --git a/src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py b/src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py index 95bf2d8bda..a9f171f415 100644 --- a/src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py +++ b/src/Mod/CAM/Path/Machine/ui/editor/machine_editor.py @@ -302,13 +302,12 @@ class MachineEditorDialog(QtGui.QDialog): def __init__(self, machine_filename: Optional[str] = None, parent=None): super().__init__(parent) - self.setWindowTitle(translate("CAM_MachineEditor", "Machine Editor")) self.setMinimumSize(700, 900) self.resize(700, 900) self.current_units = "metric" - # Initialize machine object first (needed by setup_post_tab) + # Initialize machine object first (needed by setup methods) self.filename = machine_filename self.machine = None # Store the Machine object @@ -317,6 +316,16 @@ class MachineEditorDialog(QtGui.QDialog): else: self.machine = Machine(name="New Machine") + # Set window title with machine name + title = translate("CAM_MachineEditor", "Machine Editor") + if self.machine and self.machine.name: + title += f" - {self.machine.name}" + self.setWindowTitle(title) + + # Initialize widget and processor caches + self.post_widgets = {} + self.processor = {} + self.layout = QtGui.QVBoxLayout(self) # Tab widget for sections @@ -328,15 +337,33 @@ class MachineEditorDialog(QtGui.QDialog): self.tabs.addTab(self.machine_tab, translate("CAM_MachineEditor", "Machine")) self.setup_machine_tab() - # Post tab - self.post_tab = QtGui.QWidget() - self.tabs.addTab(self.post_tab, translate("CAM_MachineEditor", "Post Processor")) - self.setup_post_tab() + # Output Options tab + self.output_tab = QtGui.QWidget() + self.tabs.addTab(self.output_tab, translate("CAM_MachineEditor", "Output Options")) + self.setup_output_tab() + + # G-Code Blocks tab + self.blocks_tab = QtGui.QWidget() + self.tabs.addTab(self.blocks_tab, translate("CAM_MachineEditor", "G-Code Blocks")) + self.setup_blocks_tab() + + # Processing Options tab + self.processing_tab = QtGui.QWidget() + self.tabs.addTab(self.processing_tab, translate("CAM_MachineEditor", "Processing Options")) + self.setup_processing_tab() # Check experimental flag for machine post processor param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/CAM") self.enable_machine_postprocessor = param.GetBool("EnableMachinePostprocessor", True) - self.tabs.setTabVisible(self.tabs.indexOf(self.post_tab), self.enable_machine_postprocessor) + self.tabs.setTabVisible( + self.tabs.indexOf(self.output_tab), self.enable_machine_postprocessor + ) + self.tabs.setTabVisible( + self.tabs.indexOf(self.blocks_tab), self.enable_machine_postprocessor + ) + self.tabs.setTabVisible( + self.tabs.indexOf(self.processing_tab), self.enable_machine_postprocessor + ) # Text editor (initially hidden) self.text_editor = CodeEditor() @@ -376,6 +403,9 @@ class MachineEditorDialog(QtGui.QDialog): # Populate GUI from machine object self.populate_from_machine(self.machine) + # Update spindle button state + self._update_spindle_button_state() + # Set focus and select the name field for new machines if not machine_filename: self.name_edit.setFocus() @@ -496,6 +526,11 @@ class MachineEditorDialog(QtGui.QDialog): """Update machine name when text changes.""" if self.machine: self.machine.name = text + # Update window title with new name + title = translate("CAM_MachineEditor", "Machine Editor") + if self.machine.name: + title += f" - {self.machine.name}" + self.setWindowTitle(title) def _on_rotary_sequence_changed(self, axis_name, value): """Update rotary axis sequence.""" @@ -525,6 +560,52 @@ class MachineEditorDialog(QtGui.QDialog): spindle = self.machine.spindles[spindle_index] setattr(spindle, field_name, value) + def _add_spindle(self): + """Add a new spindle to the machine.""" + if self.machine and len(self.machine.spindles) < 9: + new_index = len(self.machine.spindles) + 1 + new_spindle = Spindle( + name=f"Spindle {new_index}", + id=f"spindle{new_index}", + max_power_kw=3.0, + max_rpm=24000, + min_rpm=6000, + tool_change="manual", + ) + self.machine.spindles.append(new_spindle) + self.update_spindles() + self._update_spindle_button_state() + # Set focus to the new tab + self.spindles_tabs.setCurrentIndex(len(self.machine.spindles) - 1) + + def _remove_spindle(self, index): + """Remove a spindle from the machine with confirmation. + + Args: + index: Index of the tab/spindle to remove + """ + if not self.machine or len(self.machine.spindles) <= 1: + return # Don't allow removing the last spindle + + spindle = self.machine.spindles[index] + reply = QtGui.QMessageBox.question( + self, + translate("CAM_MachineEditor", "Remove Spindle"), + translate("CAM_MachineEditor", f"Remove '{spindle.name}'? This cannot be undone."), + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, + QtGui.QMessageBox.No, + ) + + if reply == QtGui.QMessageBox.Yes: + self.machine.spindles.pop(index) + self.update_spindles() + self._update_spindle_button_state() + + def _update_spindle_button_state(self): + """Enable/disable the add spindle button based on count.""" + if self.machine: + self.add_spindle_button.setEnabled(len(self.machine.spindles) < 9) + def _on_manufacturer_changed(self, text): """Update manufacturer when text changes.""" if self.machine: @@ -639,13 +720,30 @@ class MachineEditorDialog(QtGui.QDialog): self.type_combo.currentIndexChanged.connect(self._on_type_changed) layout.addRow(translate("CAM_MachineEditor", "Type"), self.type_combo) - self.spindle_count_combo = QtGui.QComboBox() - for i in range(1, 10): # 1 to 9 spindles - self.spindle_count_combo.addItem(str(i), i) - self.spindle_count_combo.currentIndexChanged.connect(self.update_spindles) - layout.addRow( - translate("CAM_MachineEditor", "Number of spindles"), self.spindle_count_combo + # Post Processor Selection + self.post_processor_combo = QtGui.QComboBox() + postProcessors = Path.Preferences.allEnabledPostProcessors([""]) + for post in postProcessors: + self.post_processor_combo.addItem(post) + self.post_processor_combo.currentIndexChanged.connect(self.updatePostProcessorTooltip) + self.post_processor_combo.currentIndexChanged.connect( + lambda: self._update_machine_field( + "postprocessor_file_name", self.post_processor_combo.currentText() + ) ) + self.postProcessorDefaultTooltip = translate("CAM_MachineEditor", "Select a post processor") + self.post_processor_combo.setToolTip(self.postProcessorDefaultTooltip) + layout.addRow(translate("CAM_MachineEditor", "Post Processor"), self.post_processor_combo) + + # self.post_processor_args_edit = QtGui.QLineEdit() + # self.post_processor_args_edit.textChanged.connect( + # lambda text: self._update_machine_field("postprocessor_args", text) + # ) + # self.postProcessorArgsDefaultTooltip = translate( + # "CAM_MachineEditor", "Additional arguments" + # ) + # self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip) + # layout.addRow(translate("CAM_MachineEditor", "Arguments"), self.post_processor_args_edit) # Axes group self.axes_group = QtGui.QGroupBox(translate("CAM_MachineEditor", "Axes")) @@ -656,7 +754,25 @@ class MachineEditorDialog(QtGui.QDialog): # Spindles group self.spindles_group = QtGui.QGroupBox(translate("CAM_MachineEditor", "Spindles")) spindles_layout = QtGui.QVBoxLayout(self.spindles_group) + self.spindles_tabs = QtGui.QTabWidget() + self.spindles_tabs.setTabsClosable(True) + self.spindles_tabs.tabCloseRequested.connect(self._remove_spindle) + + # Add + button to the tab bar corner, vertically centered + corner_container = QtGui.QWidget() + corner_container_layout = QtGui.QVBoxLayout(corner_container) + corner_container_layout.setContentsMargins(0, 0, 0, 0) + corner_container_layout.setSpacing(0) + corner_container_layout.addStretch() + self.add_spindle_button = QtGui.QPushButton("+") + self.add_spindle_button.setToolTip(translate("CAM_MachineEditor", "Add Spindle")) + self.add_spindle_button.clicked.connect(self._add_spindle) + self.add_spindle_button.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + corner_container_layout.addWidget(self.add_spindle_button, 0, QtCore.Qt.AlignCenter) + corner_container_layout.addStretch() + self.spindles_tabs.setCornerWidget(corner_container, QtCore.Qt.TopRightCorner) + spindles_layout.addWidget(self.spindles_tabs) layout.addRow(self.spindles_group) @@ -701,8 +817,6 @@ class MachineEditorDialog(QtGui.QDialog): self.axes_group.setVisible(False) return - axes_form = QtGui.QFormLayout() - # Get axes directly from machine object linear_axes = list(self.machine.linear_axes.keys()) if self.machine else [] rotary_axes = list(self.machine.rotary_axes.keys()) if self.machine else [] @@ -852,7 +966,10 @@ class MachineEditorDialog(QtGui.QDialog): axis_grid.addWidget(QtGui.QLabel("Prefer+"), 1, 4, QtCore.Qt.AlignRight) axis_grid.addWidget(prefer_positive, 1, 5) - rotary_layout.addRow(f"{axis}", axis_grid) + axis_label = QtGui.QLabel(f"{axis}") + axis_label.setMinimumWidth(30) # Prevent layout shift when axis names change + rotary_layout.addRow(axis_label, axis_grid) + self.axis_edits[axis] = { "min": min_edit, "max": max_edit, @@ -874,26 +991,6 @@ class MachineEditorDialog(QtGui.QDialog): input fields for name, ID, power, speed, and tool holder. Updates Machine.spindles directly. """ - # Update machine spindles with current edits before rebuilding UI - if hasattr(self, "spindle_edits") and self.machine: - # Resize spindles list to match current edits - while len(self.machine.spindles) < len(self.spindle_edits): - self.machine.spindles.append(Spindle(name="")) - while len(self.machine.spindles) > len(self.spindle_edits): - self.machine.spindles.pop() - - # Update each spindle from UI - for i, edits in enumerate(self.spindle_edits): - spindle = self.machine.spindles[i] - spindle.name = edits["name"].text() - spindle.id = edits["id"].text() - spindle.max_power_kw = edits["max_power_kw"].value() - spindle.max_rpm = edits["max_rpm"].value() - spindle.min_rpm = edits["min_rpm"].value() - spindle.tool_change = edits["tool_change"].itemData( - edits["tool_change"].currentIndex() - ) - # Clear existing spindle tabs - this properly disconnects signals while self.spindles_tabs.count() > 0: tab = self.spindles_tabs.widget(0) @@ -903,9 +1000,9 @@ class MachineEditorDialog(QtGui.QDialog): self.spindles_tabs.removeTab(0) self.spindle_edits = [] - count = self.spindle_count_combo.itemData(self.spindle_count_combo.currentIndex()) + count = len(self.machine.spindles) if self.machine else 1 - # Ensure machine has enough spindles + # Ensure machine has at least 1 spindle if self.machine: while len(self.machine.spindles) < count: self.machine.spindles.append( @@ -996,8 +1093,8 @@ class MachineEditorDialog(QtGui.QDialog): } ) - def setup_post_tab(self): - """Set up the post processor configuration tab dynamically from Machine dataclass.""" + def setup_output_tab(self): + """Set up the output options configuration tab.""" # Use scroll area for all the options scroll = QtGui.QScrollArea() scroll.setWidgetResizable(True) @@ -1005,61 +1102,55 @@ class MachineEditorDialog(QtGui.QDialog): layout = QtGui.QVBoxLayout(scroll_widget) scroll.setWidget(scroll_widget) - main_layout = QtGui.QVBoxLayout(self.post_tab) + main_layout = QtGui.QVBoxLayout(self.output_tab) main_layout.addWidget(scroll) - # Store widgets for later population - self.post_widgets = {} - - # === Post Processor Selection (special handling for combo box) === - pp_group = QtGui.QGroupBox("Post Processor Selection") - pp_layout = QtGui.QFormLayout(pp_group) - - self.post_processor_combo = QtGui.QComboBox() - postProcessors = Path.Preferences.allEnabledPostProcessors([""]) - for post in postProcessors: - self.post_processor_combo.addItem(post) - self.post_processor_combo.currentIndexChanged.connect(self.updatePostProcessorTooltip) - self.post_processor_combo.currentIndexChanged.connect( - lambda: self._update_machine_field( - "postprocessor_file_name", self.post_processor_combo.currentText() - ) - ) - self.postProcessorDefaultTooltip = translate("CAM_MachineEditor", "Select a post processor") - self.post_processor_combo.setToolTip(self.postProcessorDefaultTooltip) - pp_layout.addRow("Post Processor", self.post_processor_combo) - self.post_widgets["postprocessor_file_name"] = self.post_processor_combo - - self.post_processor_args_edit = QtGui.QLineEdit() - self.post_processor_args_edit.textChanged.connect( - lambda text: self._update_machine_field("postprocessor_args", text) - ) - self.postProcessorArgsDefaultTooltip = translate( - "CAM_MachineEditor", "Additional arguments" - ) - self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip) - pp_layout.addRow("Arguments", self.post_processor_args_edit) - self.post_widgets["postprocessor_args"] = self.post_processor_args_edit - - layout.addWidget(pp_group) - - # === Dynamically generate groups for nested dataclasses === + # === Output Options === if self.machine: - # Output Options output_group, output_widgets = DataclassGUIGenerator.create_group_for_dataclass( self.machine.output, "Output Options" ) layout.addWidget(output_group) self._connect_widgets_to_machine(output_widgets, "output") - # G-Code Blocks + layout.addStretch() + + def setup_blocks_tab(self): + """Set up the G-Code blocks configuration tab.""" + # Use scroll area for all the options + scroll = QtGui.QScrollArea() + scroll.setWidgetResizable(True) + scroll_widget = QtGui.QWidget() + layout = QtGui.QVBoxLayout(scroll_widget) + scroll.setWidget(scroll_widget) + + main_layout = QtGui.QVBoxLayout(self.blocks_tab) + main_layout.addWidget(scroll) + + # === G-Code Blocks === + if self.machine: blocks_group, blocks_widgets = DataclassGUIGenerator.create_group_for_dataclass( self.machine.blocks, "G-Code Blocks" ) layout.addWidget(blocks_group) self._connect_widgets_to_machine(blocks_widgets, "blocks") - # Processing Options + layout.addStretch() + + def setup_processing_tab(self): + """Set up the processing options configuration tab.""" + # Use scroll area for all the options + scroll = QtGui.QScrollArea() + scroll.setWidgetResizable(True) + scroll_widget = QtGui.QWidget() + layout = QtGui.QVBoxLayout(scroll_widget) + scroll.setWidget(scroll_widget) + + main_layout = QtGui.QVBoxLayout(self.processing_tab) + main_layout.addWidget(scroll) + + # === Processing Options === + if self.machine: processing_group, processing_widgets = DataclassGUIGenerator.create_group_for_dataclass( self.machine.processing, "Processing Options" ) @@ -1068,9 +1159,6 @@ class MachineEditorDialog(QtGui.QDialog): layout.addStretch() - # Cache for post processors - self.processor = {} - def _connect_widgets_to_machine(self, widgets: Dict[str, QtGui.QWidget], parent_path: str): """Connect widgets to update Machine object fields. @@ -1267,13 +1355,13 @@ class MachineEditorDialog(QtGui.QDialog): self.post_processor_combo, name, self.postProcessorDefaultTooltip ) processor = self.getPostProcessor(name) - if processor.tooltipArgs: - self.post_processor_args_edit.setToolTip(processor.tooltipArgs) - else: - self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip) + # if processor.tooltipArgs: + # self.post_processor_args_edit.setToolTip(processor.tooltipArgs) + # else: + # self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip) else: self.post_processor_combo.setToolTip(self.postProcessorDefaultTooltip) - self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip) + # self.post_processor_args_edit.setToolTip(self.postProcessorArgsDefaultTooltip) def populate_from_machine(self, machine: Machine): """Populate UI fields from Machine object. @@ -1285,14 +1373,31 @@ class MachineEditorDialog(QtGui.QDialog): self.manufacturer_edit.setText(machine.manufacturer) self.description_edit.setText(machine.description) units = machine.configuration_units + self.units_combo.blockSignals(True) index = self.units_combo.findData(units) if index >= 0: self.units_combo.setCurrentIndex(index) + self.units_combo.blockSignals(False) self.current_units = units machine_type = machine.machine_type + self.type_combo.blockSignals(True) index = self.type_combo.findData(machine_type) if index >= 0: self.type_combo.setCurrentIndex(index) + self.type_combo.blockSignals(False) + + # Post processor selection + if self.enable_machine_postprocessor: + post_processor = machine.postprocessor_file_name + index = self.post_processor_combo.findText(post_processor, QtCore.Qt.MatchFixedString) + if index >= 0: + self.post_processor_combo.setCurrentIndex(index) + else: + self.post_processor_combo.setCurrentIndex(0) + self.updatePostProcessorTooltip() + + # Post processor arguments + # self.post_processor_args_edit.setText(machine.postprocessor_args) # Get units for suffixes in populate units = self.units_combo.itemData(self.units_combo.currentIndex()) @@ -1300,28 +1405,23 @@ class MachineEditorDialog(QtGui.QDialog): # Update axes UI after loading machine data self.update_axes() - spindles = machine.spindles - spindle_count = len(spindles) - if spindle_count == 0: - spindle_count = 1 # Default to 1 if none - spindle_count = min(spindle_count, 9) # Cap at 9 - self.spindle_count_combo.setCurrentText(str(spindle_count)) - self.update_spindles() # Update spindles after setting count (will populate from machine.spindles) + # Ensure at least 1 spindle + if len(machine.spindles) == 0: + machine.spindles.append( + Spindle( + name="Spindle 1", + id="spindle1", + max_power_kw=3.0, + max_rpm=24000, + min_rpm=6000, + tool_change="manual", + ) + ) + self.update_spindles() # Update spindles UI + self._update_spindle_button_state() # Post processor configuration - populate dynamically generated widgets if self.enable_machine_postprocessor and hasattr(self, "post_widgets"): - # Post processor selection - post_processor = machine.postprocessor_file_name - index = self.post_processor_combo.findText(post_processor, QtCore.Qt.MatchFixedString) - if index >= 0: - self.post_processor_combo.setCurrentIndex(index) - else: - self.post_processor_combo.setCurrentIndex(0) - - # Post processor arguments - self.post_processor_args_edit.setText(machine.postprocessor_args) - self.updatePostProcessorTooltip() - # Update all post-processor widgets from machine object self._populate_post_widgets_from_machine(machine) diff --git a/src/Mod/CAM/Path/Main/Gui/PreferencesJob.py b/src/Mod/CAM/Path/Main/Gui/PreferencesJob.py index 6900846bd6..4678029d40 100644 --- a/src/Mod/CAM/Path/Main/Gui/PreferencesJob.py +++ b/src/Mod/CAM/Path/Main/Gui/PreferencesJob.py @@ -39,7 +39,7 @@ class JobPreferencesPage: import FreeCADGui self.form = FreeCADGui.PySideUic.loadUi(":preferences/PathJob.ui") - self.form.toolBox.setCurrentIndex(0) # Take that qt designer! + self.form.tabWidget.setCurrentIndex(0) # Take that qt designer! self.postProcessorDefaultTooltip = self.form.defaultPostProcessor.toolTip() self.postProcessorArgsDefaultTooltip = self.form.defaultPostProcessorArgs.toolTip() diff --git a/src/Mod/CAM/Path/Tool/assets/ui/preferences.py b/src/Mod/CAM/Path/Tool/assets/ui/preferences.py index 5f1e763171..252de63165 100644 --- a/src/Mod/CAM/Path/Tool/assets/ui/preferences.py +++ b/src/Mod/CAM/Path/Tool/assets/ui/preferences.py @@ -48,20 +48,21 @@ def _is_writable_dir(path: pathlib.Path) -> bool: class AssetPreferencesPage: def __init__(self, parent=None): - self.form = QtGui.QToolBox() + self.form = QtGui.QWidget() self.form.setWindowTitle(translate("CAM_PreferencesAssets", "Assets")) - asset_path_widget = QtGui.QWidget() - main_layout = QtGui.QHBoxLayout(asset_path_widget) + # Set up main layout directly on the form + self.main_layout = QtGui.QVBoxLayout(self.form) # Create widgets - self.asset_path_label = QtGui.QLabel(translate("CAM_PreferencesAssets", "Asset directory")) + self.assets_group = QtGui.QGroupBox(translate("CAM_PreferencesAssets", "Asset Location")) + self.asset_path_label = QtGui.QLabel(translate("CAM_PreferencesAssets", "Default path")) self.asset_path_edit = QtGui.QLineEdit() self.asset_path_note_label = QtGui.QLabel( translate( "CAM_PreferencesAssets", "Note: Select the directory that will contain the " - "Tool folder with Bit/, Shape/, and Library/ subfolders.", + "Tools folder with Bit/, Shape/, and Library/ subfolders and the Machines/ folder.", ) ) self.asset_path_note_label.setWordWrap(True) @@ -76,39 +77,52 @@ class AssetPreferencesPage: font.setItalic(True) self.asset_path_note_label.setFont(font) - # Layout for asset path section - edit_button_layout = QtGui.QGridLayout() - edit_button_layout.addWidget(self.asset_path_label, 0, 0, QtCore.Qt.AlignVCenter) - edit_button_layout.addWidget(self.asset_path_edit, 0, 1, QtCore.Qt.AlignVCenter) - edit_button_layout.addWidget(self.select_path_button, 0, 2, QtCore.Qt.AlignVCenter) - edit_button_layout.addWidget(self.reset_path_button, 0, 3, QtCore.Qt.AlignVCenter) - edit_button_layout.addWidget(self.asset_path_note_label, 1, 1, 1, 1, QtCore.Qt.AlignTop) - edit_button_layout.setRowStretch(3, 1) + # Assets group box + self.assets_layout = QtGui.QGridLayout(self.assets_group) + self.assets_layout.addWidget(self.asset_path_label, 0, 0, QtCore.Qt.AlignVCenter) + self.assets_layout.addWidget(self.asset_path_edit, 0, 1, QtCore.Qt.AlignVCenter) + self.assets_layout.addWidget(self.select_path_button, 0, 2, QtCore.Qt.AlignVCenter) + self.assets_layout.addWidget(self.reset_path_button, 0, 3, QtCore.Qt.AlignVCenter) + self.assets_layout.addWidget(self.asset_path_note_label, 1, 1, 1, 1, QtCore.Qt.AlignTop) + self.main_layout.addWidget(self.assets_group) - main_layout.addLayout(edit_button_layout, QtCore.Qt.AlignTop) + # Machines group box + self.machines_group = QtGui.QGroupBox(translate("CAM_PreferencesAssets", "Machines")) + self.machines_layout = QtGui.QVBoxLayout(self.machines_group) - self.form.addItem(asset_path_widget, translate("CAM_PreferencesAssets", "Assets")) + self.warning_label = QtGui.QLabel( + translate( + "CAM_PreferencesAssets", + "Warning: Machine definition is an experimental feature. Changes " + "made here will not affect any CAM functionality", + ) + ) + self.warning_label.setWordWrap(True) + warning_font = self.warning_label.font() + warning_font.setItalic(True) + self.warning_label.setFont(warning_font) + self.warning_label.setContentsMargins(0, 0, 0, 10) + self.machines_layout.addWidget(self.warning_label) - # Integrate machines list into the Assets panel - machines_list_layout = QtGui.QVBoxLayout() - machines_label = QtGui.QLabel(translate("CAM_PreferencesAssets", "Machines")) - machines_list_layout.addWidget(machines_label) + self.machines_label = QtGui.QLabel(translate("CAM_PreferencesAssets", "Machines")) + self.machines_layout.addWidget(self.machines_label) self.machines_list = QtGui.QListWidget() - machines_list_layout.addWidget(self.machines_list) + self.machines_layout.addWidget(self.machines_list) # Buttons: Add / Edit / Delete - btn_layout = QtGui.QHBoxLayout() + self.btn_layout = QtGui.QHBoxLayout() self.add_machine_btn = QtGui.QPushButton(translate("CAM_PreferencesAssets", "Add")) self.edit_machine_btn = QtGui.QPushButton(translate("CAM_PreferencesAssets", "Edit")) self.delete_machine_btn = QtGui.QPushButton(translate("CAM_PreferencesAssets", "Delete")) - btn_layout.addWidget(self.add_machine_btn) - btn_layout.addWidget(self.edit_machine_btn) - btn_layout.addWidget(self.delete_machine_btn) - machines_list_layout.addLayout(btn_layout) + self.btn_layout.addWidget(self.add_machine_btn) + self.btn_layout.addWidget(self.edit_machine_btn) + self.btn_layout.addWidget(self.delete_machine_btn) + self.machines_layout.addLayout(self.btn_layout) - # Insert the machines list directly under the path controls - edit_button_layout.addLayout(machines_list_layout, 2, 0, 1, 4) + self.machines_layout.addStretch() # Prevent the list from stretching + + self.main_layout.addWidget(self.machines_group) # Wire up buttons self.add_machine_btn.clicked.connect(self.add_machine)