[CAM] Add UI elements for viewing and editing tool controller parameters from within the operation edit UI (#23180)

* [CAM] extract tool controller ui elements into their own file

* [CAM] make the changes in the tool controller UI only apply when ok is clicked

* [CAM] Add tool controller edit panel to the Profile operation

* [CAM] Add copy button to in-operation tool controller editor

* [CAM] clean up changes

* [CAM] Add tool controller edit UI to all operations

Notes on changes that were not a simple copy/paste job from the changes
I made for Profile:

- Deburr: changed TC/coolant rows from 1 and 2 to 0 and 1

- Probe: didn't work at all initially due to bug in main where ShapeName
  changed to ShapeType. I added a utility for reading either a ShapeType
or a ShapeName (check for both properties, convert ShapeType to lower
case) and applied it to probe and camotics

- Drilling: moved Keep Tool Down checkbox up from row 8 to row 2 (all
  intermediate rows were missing) and added the edit checkbox in row 3
below it

- VBit, Probe (or anything else that requires a specific tool type): in
  Base.py setupToolController(), I added a check to see if the currently
selected tool is an invalid type, and if so and there is a valid tool,
then change to that one. This fixes two UI bugs. Plausibly pre-existing,
if there is one valid tool and an invalid tool is selected, it's
impossible to switch to the valid one because you can't generate a combo
box change event for the new tool. Definitely new: if an invalid tool is
selected and there are no valid tools, the combo box will be empty but
the new tool controller edit utility will let you edit the current TC
anyway.

- Thread Milling: replaced the Tool Controller GroupBox with the
  standard QFrame layout, and added the checkbox. Note that this
operation doesn't have a UI element for coolant -- likely a bug, but I
didn't look into it

- Surface: Changed from form layout to grid layout. Deleted an old
  SurfaceEdit.ui file -- it was replaced with PageOpSurfaceEdit.ui in
2017 but not deleted (commit 77af19e7489e1fc637a68cdad220e5dd430dc2b9)

- Waterline: Changed from form layout to grid layout

* [CAM] Bug fixes

setupUi() wasn't called on the tool controller editor, preventing
changes in its UI from being written back to the object immediately.
This caused weird behavior where if you edited a field twice it would
reset the second time it was focused

Added a hook to automatically update the TC combo box when the TC name
(or anything else about it, since that was easier) changes

* Fix usage of QSignalBlockers

* [CAM] Block scroll events on tool number and spindle direction when not
focused

Specifically, if you mouse over either of these UI elements and use the
scroll wheel, it used to focus the element and change its value. This
commit makes it do neither of those things, for these specific elements,
as a measure against users accidentally changing these values.

* disable tcNumber edit field in operations panel

* Add "New tool controller" option to TC combo box

When selected, it opens (toggles, technically) the tool bit dock and
returns to the previous selection. Adding a new tool controller using
the dock (already, before this commit) automatically switches the
operation's tool controller to the new one

* Add "Copy" option to tool controller combo box

* Copy TC function only in combo box, no button

* [CAM] update in-operation "new tool controller" function to use a dialog

* [CAM] make the tool selector always a dialog and never a dock

* remove spacer from ToolControllerEdit.ui to fix exces white space

* [CAM] change tool dialog default sizing/spacing

* [CAM] fix bug where copying tool controller doesn't copy all values
This commit is contained in:
David Kaufman
2025-09-02 11:19:33 -04:00
committed by GitHub
parent 85af5e6095
commit db9a615985
27 changed files with 637 additions and 762 deletions

View File

@@ -188,80 +188,104 @@ class CommandPathToolController(object):
FreeCAD.ActiveDocument.recompute()
class BlockScrollWheel(QtCore.QObject):
def eventFilter(self, obj, event):
if event.type() == QtCore.QEvent.Type.Wheel:
if not obj.hasFocus():
return True
return super().eventFilter(obj, event)
class ToolControllerEditor(object):
def __init__(self, obj, asDialog):
def __init__(
self, obj, asDialog, notifyChanged=None, showCountLabel=False, disableToolNumber=False
):
self.notifyChanged = notifyChanged
self.form = FreeCADGui.PySideUic.loadUi(":/panels/DlgToolControllerEdit.ui")
self.controller = FreeCADGui.PySideUic.loadUi(":/panels/ToolControllerEdit.ui")
self.form.tc_layout.addWidget(self.controller)
if not asDialog:
self.form.buttonBox.hide()
if not showCountLabel:
self.controller.tcOperationCountLabel.hide()
self.obj = obj
comboToPropertyMap = [("spindleDirection", "SpindleDir")]
enumTups = PathToolController.ToolController.propertyEnumerations(dataType="raw")
PathGuiUtil.populateCombobox(self.form, enumTups, comboToPropertyMap)
self.vertFeed = PathGuiUtil.QuantitySpinBox(self.form.vertFeed, obj, "VertFeed")
self.horizFeed = PathGuiUtil.QuantitySpinBox(self.form.horizFeed, obj, "HorizFeed")
self.vertRapid = PathGuiUtil.QuantitySpinBox(self.form.vertRapid, obj, "VertRapid")
self.horizRapid = PathGuiUtil.QuantitySpinBox(self.form.horizRapid, obj, "HorizRapid")
PathGuiUtil.populateCombobox(self.controller, enumTups, comboToPropertyMap)
self.vertFeed = PathGuiUtil.QuantitySpinBox(self.controller.vertFeed, obj, "VertFeed")
self.horizFeed = PathGuiUtil.QuantitySpinBox(self.controller.horizFeed, obj, "HorizFeed")
self.vertRapid = PathGuiUtil.QuantitySpinBox(self.controller.vertRapid, obj, "VertRapid")
self.horizRapid = PathGuiUtil.QuantitySpinBox(self.controller.horizRapid, obj, "HorizRapid")
self.blockScrollWheel = BlockScrollWheel()
self.controller.tcNumber.installEventFilter(self.blockScrollWheel)
self.controller.spindleDirection.installEventFilter(self.blockScrollWheel)
self.controller.tcNumber.setReadOnly(disableToolNumber)
self.editor = None
self.form.toolBox.widget(1).hide()
self.form.toolBox.removeItem(1)
def selectInComboBox(self, name, combo):
"""selectInComboBox(name, combo) ...
helper function to select a specific value in a combo box."""
blocker = QtCore.QSignalBlocker(combo)
index = combo.currentIndex() # Save initial index
with QtCore.QSignalBlocker(combo):
index = combo.currentIndex() # Save initial index
# Search using currentData and return if found
newindex = combo.findData(name)
if newindex >= 0:
combo.setCurrentIndex(newindex)
return
# Search using currentData and return if found
newindex = combo.findData(name)
if newindex >= 0:
combo.setCurrentIndex(newindex)
return
# if not found, search using current text
newindex = combo.findText(name, QtCore.Qt.MatchFixedString)
if newindex >= 0:
combo.setCurrentIndex(newindex)
return
# if not found, search using current text
newindex = combo.findText(name, QtCore.Qt.MatchFixedString)
if newindex >= 0:
combo.setCurrentIndex(newindex)
return
# not found, return unchanged
combo.setCurrentIndex(index)
# not found, return unchanged
combo.setCurrentIndex(index)
return
def updateUi(self):
tc = self.obj
self.form.tcName.setText(tc.Label)
self.form.tcNumber.setValue(tc.ToolNumber)
self.horizFeed.updateWidget()
self.horizRapid.updateWidget()
self.vertFeed.updateWidget()
self.vertRapid.updateWidget()
self.form.spindleSpeed.setValue(tc.SpindleSpeed)
self.selectInComboBox(tc.SpindleDir, self.form.spindleDirection)
with (
QtCore.QSignalBlocker(self.controller.tcName),
QtCore.QSignalBlocker(self.controller.tcNumber),
QtCore.QSignalBlocker(self.horizFeed.widget),
QtCore.QSignalBlocker(self.horizRapid.widget),
QtCore.QSignalBlocker(self.vertFeed.widget),
QtCore.QSignalBlocker(self.vertRapid.widget),
QtCore.QSignalBlocker(self.controller.spindleSpeed),
QtCore.QSignalBlocker(self.controller.spindleDirection),
):
self.controller.tcName.setText(tc.Label)
self.controller.tcNumber.setValue(tc.ToolNumber)
self.horizFeed.updateWidget()
self.horizRapid.updateWidget()
self.vertFeed.updateWidget()
self.vertRapid.updateWidget()
self.controller.spindleSpeed.setValue(tc.SpindleSpeed)
# index = self.form.spindleDirection.findText(
# tc.SpindleDir, QtCore.Qt.MatchFixedString
# )
# if index >= 0:
# self.form.spindleDirection.setCurrentIndex(index)
self.selectInComboBox(tc.SpindleDir, self.controller.spindleDirection)
if self.editor:
self.editor.updateUI()
if self.editor:
self.editor.updateUI()
def updateToolController(self):
tc = self.obj
try:
tc.Label = self.form.tcName.text()
tc.ToolNumber = self.form.tcNumber.value()
tc.Label = self.controller.tcName.text()
tc.ToolNumber = self.controller.tcNumber.value()
self.horizFeed.updateProperty()
self.vertFeed.updateProperty()
self.horizRapid.updateProperty()
self.vertRapid.updateProperty()
tc.SpindleSpeed = self.form.spindleSpeed.value()
tc.SpindleDir = self.form.spindleDirection.currentData()
tc.SpindleSpeed = self.controller.spindleSpeed.value()
tc.SpindleDir = self.controller.spindleDirection.currentData()
if self.editor:
self.editor.updateTool()
@@ -270,23 +294,29 @@ class ToolControllerEditor(object):
except Exception as e:
Path.Log.error("Error updating TC: {}".format(e))
def refresh(self):
def changed(self):
self.form.blockSignals(True)
self.controller.blockSignals(True)
self.updateToolController()
self.updateUi()
self.controller.blockSignals(False)
self.form.blockSignals(False)
if self.notifyChanged:
self.notifyChanged()
def setupUi(self):
if self.editor:
self.editor.setupUI()
self.form.tcName.editingFinished.connect(self.refresh)
self.form.horizFeed.editingFinished.connect(self.refresh)
self.form.vertFeed.editingFinished.connect(self.refresh)
self.form.horizRapid.editingFinished.connect(self.refresh)
self.form.vertRapid.editingFinished.connect(self.refresh)
self.form.spindleSpeed.editingFinished.connect(self.refresh)
self.form.spindleDirection.currentIndexChanged.connect(self.refresh)
self.controller.tcName.textChanged.connect(self.changed)
self.controller.tcNumber.editingFinished.connect(self.changed)
self.vertFeed.widget.textChanged.connect(self.changed)
self.horizFeed.widget.textChanged.connect(self.changed)
self.vertRapid.widget.textChanged.connect(self.changed)
self.horizRapid.widget.textChanged.connect(self.changed)
self.controller.spindleSpeed.editingFinished.connect(self.changed)
self.controller.spindleDirection.currentIndexChanged.connect(self.changed)
class TaskPanel:
@@ -359,6 +389,9 @@ class DlgToolControllerEdit:
Path.Log.info("revert")
self.obj.Proxy.setFromTemplate(self.obj, restoreTC)
rc = True
else:
self.editor.updateToolController()
self.obj.Proxy.execute(self.obj)
return rc

View File

@@ -48,8 +48,10 @@ class CommandToolBitLibraryDockOpen:
def GetResources(self):
return {
"Pixmap": "CAM_ToolTable",
"MenuText": QT_TRANSLATE_NOOP("CAM_ToolBitDock", "Toolbit Dock"),
"ToolTip": QT_TRANSLATE_NOOP("CAM_ToolBitDock", "Toggles the toolbit dock"),
"MenuText": QT_TRANSLATE_NOOP("CAM_ToolBitSelection", "Add toolbit"),
"ToolTip": QT_TRANSLATE_NOOP(
"CAM_ToolBitSelection", "Opens the toolbit selection dialog"
),
"Accel": "P, T",
"CmdType": "ForEdit",
}

View File

@@ -28,7 +28,7 @@ import FreeCADGui
import Path
import Path.Tool.Gui.Controller as PathToolControllerGui
import PathScripts.PathUtilsGui as PathUtilsGui
from PySide import QtGui, QtCore
from PySide import QtGui, QtCore, QtWidgets
from functools import partial
from typing import List, Tuple
from ...camassets import cam_assets, ensure_assets_initialized
@@ -50,12 +50,20 @@ translate = FreeCAD.Qt.translate
class ToolBitLibraryDock(object):
"""Controller for displaying a library and creating ToolControllers"""
def __init__(self):
def __init__(self, defaultJob=None, autoClose=False):
ensure_assets_initialized(cam_assets)
# Create the main form widget directly
self.form = QtGui.QDockWidget()
self.defaultJob = defaultJob
self.autoClose = autoClose
self.form = QtWidgets.QDialog()
self.form.setObjectName("ToolSelector")
self.form.setWindowTitle(translate("CAM_ToolBit", "Tool Selector"))
self.form.setMinimumSize(600, 400)
self.form.resize(800, 600)
self.form.adjustSize()
self.form_layout = QtGui.QVBoxLayout(self.form)
self.form_layout.setContentsMargins(4, 4, 4, 4)
self.form_layout.setSpacing(4)
# Create the browser widget
self.browser_widget = LibraryBrowserWidget(asset_manager=cam_assets)
@@ -69,6 +77,8 @@ class ToolBitLibraryDock(object):
# Create a main widget and layout for the dock
main_widget = QtGui.QWidget()
main_layout = QtGui.QVBoxLayout(main_widget)
main_layout.setContentsMargins(4, 4, 4, 4)
main_layout.setSpacing(4)
# Add the browser widget to the layout
main_layout.addWidget(self.browser_widget)
@@ -88,7 +98,7 @@ class ToolBitLibraryDock(object):
main_layout.addLayout(button_layout)
# Set the main widget as the dock's widget
self.form.setWidget(main_widget)
self.form.layout().addWidget(main_widget)
# Connect signals from the browser widget and buttons
self.browser_widget.toolSelected.connect(self._update_state)
@@ -152,11 +162,12 @@ class ToolBitLibraryDock(object):
translate("CAM_ToolBit", "Please create a Job first."),
)
return
elif len(jobs) == 1:
job = jobs[0]
elif self.defaultJob or len(jobs) == 1:
job = self.defaultJob or jobs[0]
else:
userinput = PathUtilsGui.PathUtilsUserInput()
job = userinput.chooseJob(jobs)
self.defaultJob = job
if job is None: # user may have canceled
return
@@ -169,21 +180,9 @@ class ToolBitLibraryDock(object):
job.Proxy.addToolController(tc)
FreeCAD.ActiveDocument.recompute()
if self.autoClose:
self.form.accept()
def open(self, path=None):
"""load library stored in path and bring up ui"""
docs = FreeCADGui.getMainWindow().findChildren(QtGui.QDockWidget)
for doc in docs:
if doc.objectName() == "ToolSelector":
if doc.isVisible():
doc.deleteLater()
return
else:
doc.setVisible(True)
return
mw = FreeCADGui.getMainWindow()
mw.addDockWidget(
QtCore.Qt.RightDockWidgetArea,
self.form,
QtCore.Qt.Orientation.Vertical,
)
self.form.exec_()