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
This commit is contained in:
Billy Huddleston
2026-01-08 19:28:32 -05:00
parent ca0894c6f4
commit 9af98c121e
6 changed files with 424 additions and 171 deletions

164
generate_machine_box.py Normal file
View File

@@ -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()

View File

@@ -15,20 +15,12 @@
</property>
<layout class="QVBoxLayout">
<item>
<widget class="QToolBox" name="toolBox">
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>695</width>
<height>308</height>
</rect>
</property>
<attribute name="label">
<widget class="QWidget" name="tab">
<attribute name="title">
<string>General</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
@@ -118,16 +110,8 @@ If left empty no template will be preselected.</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>695</width>
<height>480</height>
</rect>
</property>
<attribute name="label">
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Post processor</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
@@ -334,16 +318,8 @@ See the file save policy below on how to deal with name conflicts.</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_3">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>674</width>
<height>619</height>
</rect>
</property>
<attribute name="label">
<widget class="QWidget" name="tab_3">
<attribute name="title">
<string>Setup</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">

View File

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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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)