| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | import yaml |
| | import pathlib |
| | import FreeCAD |
| | import FreeCADGui |
| | import Path |
| | from PySide.QtGui import ( |
| | QStandardItem, |
| | QStandardItemModel, |
| | QPixmap, |
| | QDialog, |
| | QMessageBox, |
| | QWidget, |
| | ) |
| | from PySide.QtCore import Qt, QEvent |
| | from typing import List, cast, Tuple, Optional |
| | from ...assets import AssetUri |
| | from ...assets.ui import AssetOpenDialog, AssetSaveDialog |
| | from ...camassets import cam_assets, ensure_assets_initialized |
| | from ...shape.ui.shapeselector import ShapeSelector |
| | from ...toolbit import ToolBit |
| | from ...toolbit.serializers import all_serializers as toolbit_serializers |
| | from ...toolbit.ui import ToolBitEditor |
| | from ...toolbit.ui.toollist import ToolBitUriListMimeType |
| | from ...toolbit.ui.util import natural_sort_key |
| | from ...toolbit.util import setToolBitSchema |
| | from ..serializers import all_serializers as library_serializers |
| | from ..models import Library |
| | from .browser import LibraryBrowserWidget |
| | from .properties import LibraryPropertyDialog |
| |
|
| |
|
| | if False: |
| | Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) |
| | Path.Log.trackModule(Path.Log.thisModule()) |
| | else: |
| | Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) |
| |
|
| |
|
| | _LibraryRole = Qt.UserRole + 1 |
| | translate = FreeCAD.Qt.translate |
| |
|
| |
|
| | class LibraryEditor(QWidget): |
| | """LibraryEditor is the controller for |
| | displaying/selecting/creating/editing a collection of ToolBits.""" |
| |
|
| | def __init__(self, parent=None): |
| | super().__init__(parent=parent) |
| | Path.Log.track() |
| | ensure_assets_initialized(cam_assets) |
| | self.form = FreeCADGui.PySideUic.loadUi(":/panels/ToolBitLibraryEdit.ui") |
| | self.form.installEventFilter(self) |
| | self._base_title = self.form.windowTitle() |
| |
|
| | |
| | self.listModel = QStandardItemModel() |
| | self.form.TableList.setModel(self.listModel) |
| | self.form.TableList.clicked.connect(self._on_library_selected) |
| |
|
| | |
| | self.form.TableList.viewport().installEventFilter(self) |
| |
|
| | |
| | self.browser = LibraryBrowserWidget( |
| | asset_manager=cam_assets, |
| | parent=self, |
| | ) |
| | self.browser.setDragEnabled(True) |
| | self.form.verticalLayout_2.layout().replaceWidget(self.form.toolTable, self.browser) |
| | self.form.toolTable.hide() |
| |
|
| | |
| | self.browser.itemDoubleClicked.connect(self.browser._on_edit_requested) |
| |
|
| | self.form.addLibraryButton.clicked.connect(self._on_add_library_requested) |
| | self.form.removeLibraryButton.clicked.connect(self._on_remove_library_requested) |
| | self.form.renameLibraryButton.clicked.connect(self._on_rename_library_requested) |
| | self.form.importLibraryButton.clicked.connect(self._on_import_library_requested) |
| | self.form.exportLibraryButton.clicked.connect(self._on_export_library_requested) |
| |
|
| | self.form.addToolBitButton.clicked.connect(self._on_add_toolbit_requested) |
| | self.form.importToolBitButton.clicked.connect(self._on_import_toolbit_requested) |
| | self.form.exportToolBitButton.clicked.connect(self._on_export_toolbit_requested) |
| |
|
| | |
| | self._refresh_library_list() |
| | self._select_last_library() |
| | self._update_button_states() |
| |
|
| | def _highlight_row(self, index): |
| | """Highlights the row at the given index using the selection model.""" |
| | if not index.isValid(): |
| | return |
| | self.form.TableList.setCurrentIndex(index) |
| |
|
| | def _clear_highlight(self): |
| | """Clears the highlighting from the previously highlighted row.""" |
| | self.form.TableList.selectionModel().clear() |
| |
|
| | def eventFilter(self, obj, event): |
| | if event.type() == QEvent.KeyPress and self.form.TableList.hasFocus(): |
| | if event.key() == Qt.Key_F2: |
| | Path.Log.debug("F2 pressed on library list.") |
| | self._on_rename_library_requested() |
| | return True |
| | elif event.key() == Qt.Key_Delete: |
| | Path.Log.debug("Del pressed on library list.") |
| | self._on_remove_library_requested() |
| | return True |
| | if obj == self.form.TableList.viewport(): |
| | if event.type() == QEvent.DragEnter or event.type() == QEvent.DragMove: |
| | return self._handle_drag_enter(event) |
| | elif event.type() == QEvent.DragLeave: |
| | self._handle_drag_leave(event) |
| | return True |
| | elif event.type() == QEvent.Drop: |
| | return self._handle_drop(event) |
| | return super().eventFilter(obj, event) |
| |
|
| | def _handle_drag_enter(self, event): |
| | """Handle drag enter and move events for the library list.""" |
| | mime_data = event.mimeData() |
| | Path.Log.debug(f"_handle_drag_enter: MIME formats: {mime_data.formats()}") |
| | if not mime_data.hasFormat(ToolBitUriListMimeType): |
| | Path.Log.debug("_handle_drag_enter: Invalid MIME type, ignoring") |
| | return True |
| |
|
| | |
| | pos = event.pos() |
| | event.acceptProposedAction() |
| | index = self.form.TableList.indexAt(pos) |
| | if not index.isValid(): |
| | self._clear_highlight() |
| | return True |
| |
|
| | |
| | item = self.listModel.itemFromIndex(index) |
| | if not item or item.data(_LibraryRole) == "all_tools": |
| | self._clear_highlight() |
| | return True |
| |
|
| | self._highlight_row(index) |
| | return True |
| |
|
| | def _handle_drag_leave(self, event): |
| | """Handle drag leave event for the library list.""" |
| | self._clear_highlight() |
| |
|
| | def _handle_drop(self, event): |
| | """Handle drop events to move or copy toolbits to the target library.""" |
| | mime_data = event.mimeData() |
| | if not (mime_data.hasFormat(ToolBitUriListMimeType)): |
| | event.ignore() |
| | return True |
| |
|
| | self._clear_highlight() |
| | pos = event.pos() |
| | index = self.form.TableList.indexAt(pos) |
| | if not index.isValid(): |
| | event.ignore() |
| | return True |
| |
|
| | item = self.listModel.itemFromIndex(index) |
| | if not item or item.data(_LibraryRole) == "all_tools": |
| | event.ignore() |
| | return True |
| |
|
| | target_library_id = item.data(_LibraryRole) |
| | target_library_uri = f"toolbitlibrary://{target_library_id}" |
| | target_library = cast(Library, cam_assets.get(target_library_uri, depth=1)) |
| |
|
| | try: |
| | clipboard_content_yaml = mime_data.data(ToolBitUriListMimeType).data().decode("utf-8") |
| | clipboard_data_dict = yaml.safe_load(clipboard_content_yaml) |
| |
|
| | if not isinstance(clipboard_data_dict, dict) or "toolbits" not in clipboard_data_dict: |
| | event.ignore() |
| | return True |
| |
|
| | uris = clipboard_data_dict["toolbits"] |
| | new_uris = set() |
| |
|
| | |
| | current_library = self.browser.get_current_library() |
| |
|
| | for uri in uris: |
| | try: |
| | toolbit = cast(ToolBit, cam_assets.get(AssetUri(uri), depth=0)) |
| | if toolbit: |
| | added_toolbit = target_library.add_bit(toolbit) |
| | if added_toolbit: |
| | new_uris.add(str(toolbit.get_uri())) |
| |
|
| | |
| | |
| | if current_library and current_library.get_id() != "all_tools": |
| | current_library.remove_bit(toolbit) |
| | except Exception as e: |
| | Path.Log.error(f"Failed to load toolbit from URI {uri}: {e}") |
| | continue |
| |
|
| | if new_uris: |
| | cam_assets.add(target_library) |
| | |
| | if current_library and current_library.get_id() != "all_tools": |
| | cam_assets.add(current_library) |
| | self.browser.refresh() |
| | self.browser.select_by_uri(list(new_uris)) |
| | self._update_button_states() |
| |
|
| | event.acceptProposedAction() |
| | except Exception as e: |
| | Path.Log.error(f"Failed to process drop event: {e}") |
| | event.ignore() |
| | return True |
| |
|
| | def get_selected_library_id(self) -> Optional[str]: |
| | index = self.form.TableList.currentIndex() |
| | if not index.isValid(): |
| | return None |
| | item = self.listModel.itemFromIndex(index) |
| | if not item: |
| | return None |
| | return item.data(_LibraryRole) |
| |
|
| | def get_selected_library(self, depth=1) -> Optional[Library]: |
| | library_id = self.get_selected_library_id() |
| | if not library_id: |
| | return None |
| | uri = f"toolbitlibrary://{library_id}" |
| | return cast(Library, cam_assets.get(uri, depth=depth)) |
| |
|
| | def select_library_by_uri(self, uri: AssetUri): |
| | |
| | index = 0 |
| | for i in range(self.listModel.rowCount()): |
| | item = self.listModel.item(i) |
| | if item and item.data(_LibraryRole) == uri.asset_id: |
| | index = i |
| | break |
| | else: |
| | return |
| |
|
| | |
| | if index <= self.listModel.rowCount(): |
| | item = self.listModel.item(index) |
| | if item: |
| | self.form.TableList.setCurrentIndex(self.listModel.index(index, 0)) |
| | self._on_library_selected() |
| |
|
| | def _select_last_library(self): |
| | |
| | last_used_lib_identifier = Path.Preferences.getLastToolLibrary() |
| | if last_used_lib_identifier: |
| | uri = Library.resolve_name(last_used_lib_identifier) |
| | self.select_library_by_uri(uri) |
| |
|
| | def open(self): |
| | Path.Log.track() |
| | return self.form.exec_() |
| |
|
| | def _refresh_library_list(self): |
| | """Clears and repopulates the self.listModel with available libraries.""" |
| | Path.Log.track() |
| | self.listModel.clear() |
| |
|
| | |
| | all_tools_item = QStandardItem(translate("CAM", "All Toolbits")) |
| | all_tools_item.setData("all_tools", _LibraryRole) |
| | |
| | |
| | font = all_tools_item.font() |
| | font.setBold(True) |
| | font.setItalic(True) |
| | all_tools_item.setFont(font) |
| | self.listModel.appendRow(all_tools_item) |
| |
|
| | |
| | try: |
| | |
| | |
| | |
| | libraries = cast(List[Library], cam_assets.fetch(asset_type="toolbitlibrary", depth=0)) |
| | except Exception as e: |
| | Path.Log.error(f"Failed to fetch toolbit libraries: {e}") |
| | return |
| |
|
| | |
| | for library in sorted( |
| | libraries, |
| | key=lambda library: natural_sort_key(library.label or library.get_id()), |
| | ): |
| | lib_uri_str = str(library.get_uri()) |
| | libItem = QStandardItem(library.label or library.get_id()) |
| | libItem.setToolTip(f"ID: {library.get_id()}\nURI: {lib_uri_str}") |
| | libItem.setData(library.get_id(), _LibraryRole) |
| | libItem.setIcon(QPixmap(":/icons/CAM_ToolTable.svg")) |
| | self.listModel.appendRow(libItem) |
| |
|
| | Path.Log.debug("model rows: {}".format(self.listModel.rowCount())) |
| |
|
| | self.listModel.setHorizontalHeaderLabels(["Library"]) |
| |
|
| | def _on_library_selected(self): |
| | """Sets the current library in the browser when a library is selected.""" |
| | Path.Log.debug("_on_library_selected: Called.") |
| | index = self.form.TableList.currentIndex() |
| | item = self.listModel.itemFromIndex(index) |
| | if not item: |
| | return |
| | if item.data(_LibraryRole) == "all_tools": |
| | selected_library = None |
| | else: |
| | selected_library = self.get_selected_library() |
| | self.browser.set_current_library(selected_library) |
| | self._update_window_title() |
| | self._update_button_states() |
| |
|
| | def _update_window_title(self): |
| | """Updates the window title with the current library name.""" |
| | current_library = self.browser.get_current_library() |
| | if current_library: |
| | title = f"{self._base_title} - {current_library.label}" |
| | else: |
| | title = self._base_title |
| | self.form.setWindowTitle(title) |
| |
|
| | def _update_button_states(self): |
| | """Updates the enabled state of library management buttons.""" |
| | library_selected = self.browser.get_current_library() is not None |
| | self.form.addLibraryButton.setEnabled(True) |
| | self.form.removeLibraryButton.setEnabled(library_selected) |
| | self.form.renameLibraryButton.setEnabled(library_selected) |
| | self.form.exportLibraryButton.setEnabled(library_selected) |
| | self.form.importLibraryButton.setEnabled(True) |
| | self.form.addToolBitButton.setEnabled( |
| | True |
| | ) |
| | |
| |
|
| | def _save_library(self): |
| | """Internal method to save the current tool library asset""" |
| | Path.Log.track() |
| | library = self.browser.get_current_library() |
| | if not library: |
| | return |
| |
|
| | |
| | try: |
| | cam_assets.add(library) |
| | Path.Log.debug(f"Library {library.get_uri()} saved") |
| | except Exception as e: |
| | Path.Log.error(f"Failed to save library {library.get_uri()}: {e}") |
| | QMessageBox.critical( |
| | self.form, |
| | translate("CAM_ToolBit", "Error Saving Library"), |
| | str(e), |
| | ) |
| | raise |
| |
|
| | def _on_add_library_requested(self): |
| | Path.Log.debug("_on_add_library_requested: Called.") |
| | new_library = Library(FreeCAD.Qt.translate("CAM", "New Library")) |
| | dialog = LibraryPropertyDialog(new_library, new=True, parent=self) |
| | if dialog.exec_() != QDialog.Accepted: |
| | return |
| |
|
| | uri = cam_assets.add(new_library) |
| | Path.Log.debug(f"_on_add_library_requested: New library URI = {uri}") |
| | self._refresh_library_list() |
| | self.select_library_by_uri(uri) |
| | self._update_button_states() |
| |
|
| | def _on_remove_library_requested(self): |
| | """Handles request to remove the selected library.""" |
| | Path.Log.debug("_on_remove_library_requested: Called.") |
| | current_library = self.browser.get_current_library() |
| | if not current_library: |
| | return |
| |
|
| | reply = QMessageBox.question( |
| | self, |
| | FreeCAD.Qt.translate("CAM", "Confirm Library Removal"), |
| | FreeCAD.Qt.translate( |
| | "CAM", |
| | "Are you sure you want to remove the library '{0}'?\n" |
| | "This will not delete the toolbits contained within it.", |
| | ).format(current_library.label), |
| | QMessageBox.Yes | QMessageBox.No, |
| | QMessageBox.No, |
| | ) |
| |
|
| | if reply != QMessageBox.Yes: |
| | return |
| |
|
| | try: |
| | library_uri = current_library.get_uri() |
| | cam_assets.delete(library_uri) |
| | Path.Log.info(f"Library {current_library.label} deleted.") |
| | self._refresh_library_list() |
| | self.browser.refresh() |
| | self._update_button_states() |
| | except FileNotFoundError as e: |
| | Path.Log.error(f"Failed to delete library {current_library.label}: {e}") |
| | QMessageBox.critical( |
| | self, |
| | FreeCAD.Qt.translate("CAM", "Error"), |
| | FreeCAD.Qt.translate("CAM", "Failed to delete library '{0}': {1}").format( |
| | current_library.label, str(e) |
| | ), |
| | ) |
| |
|
| | def _on_rename_library_requested(self): |
| | """Handles request to rename the selected library.""" |
| | Path.Log.debug("_on_rename_library_requested: Called.") |
| | current_library = self.browser.get_current_library() |
| | if not current_library: |
| | return |
| |
|
| | dialog = LibraryPropertyDialog(current_library, new=False, parent=self) |
| | if dialog.exec_() != QDialog.Accepted: |
| | return |
| |
|
| | cam_assets.add(current_library) |
| | self._refresh_library_list() |
| | self._update_button_states() |
| |
|
| | def _on_import_library_requested(self): |
| | """Handles request to import a library.""" |
| | Path.Log.debug("_on_import_library_requested: Called.") |
| | dialog = AssetOpenDialog( |
| | cam_assets, asset_class=Library, serializers=library_serializers, parent=self |
| | ) |
| | response = dialog.exec_() |
| | if not response: |
| | return |
| | file_path, library = cast(Tuple[pathlib.Path, Library], response) |
| |
|
| | try: |
| | cam_assets.add(library) |
| | self._refresh_library_list() |
| | self._update_button_states() |
| | except Exception as e: |
| | Path.Log.error(f"Failed to import library: {file_path} {e}") |
| | QMessageBox.critical( |
| | self, |
| | FreeCAD.Qt.translate("CAM", "Error"), |
| | FreeCAD.Qt.translate("CAM", f"Failed to import library: {file_path} {e}"), |
| | ) |
| |
|
| | def _on_export_library_requested(self): |
| | """Handles request to export the selected library.""" |
| | Path.Log.debug("_on_export_library_requested: Called.") |
| | current_library = self.browser.get_current_library() |
| | if not current_library: |
| | return |
| |
|
| | dialog = AssetSaveDialog(asset_class=Library, serializers=library_serializers, parent=self) |
| | dialog.exec_(current_library) |
| | self._update_button_states() |
| |
|
| | def _on_add_toolbit_requested(self): |
| | """Handles request to add a new toolbit to the current library or create standalone.""" |
| | Path.Log.debug("_on_add_toolbit_requested: Called.") |
| | current_library = self.browser.get_current_library() |
| |
|
| | |
| | selector = ShapeSelector() |
| | shape = selector.show() |
| | if shape is None: |
| | return |
| |
|
| | try: |
| | |
| | tool_bit_classes = {b.SHAPE_CLASS.name: b for b in ToolBit.__subclasses__()} |
| | tool_bit_class = tool_bit_classes.get(shape.name) |
| | if not tool_bit_class: |
| | raise ValueError(f"No ToolBit subclass found for shape '{shape.name}'") |
| |
|
| | |
| | new_toolbit = tool_bit_class(shape) |
| | new_toolbit.label = FreeCAD.Qt.translate("CAM", "New Toolbit") |
| |
|
| | |
| | tool_asset_uri = cam_assets.add(new_toolbit) |
| | Path.Log.debug(f"_on_add_toolbit_requested: Saved tool with URI: {tool_asset_uri}") |
| |
|
| | |
| | if current_library: |
| | toolno = current_library.add_bit(new_toolbit) |
| | Path.Log.debug( |
| | f"_on_add_toolbit_requested: Added toolbit {new_toolbit.get_id()} (URI: {new_toolbit.get_uri()}) " |
| | f"to current_library with number {toolno}." |
| | ) |
| | |
| | cam_assets.add(current_library) |
| | else: |
| | Path.Log.debug( |
| | f"_on_add_toolbit_requested: Created standalone toolbit {new_toolbit.get_id()} (URI: {new_toolbit.get_uri()})" |
| | ) |
| |
|
| | except Exception as e: |
| | Path.Log.error(f"Failed to create or add new toolbit: {e}") |
| | QMessageBox.critical( |
| | self, |
| | FreeCAD.Qt.translate("CAM", "Error Creating Toolbit"), |
| | str(e), |
| | ) |
| | raise |
| |
|
| | setToolBitSchema() |
| | self.browser.refresh() |
| | self.browser.select_by_uri([str(new_toolbit.get_uri())]) |
| | self._update_button_states() |
| |
|
| | def _on_import_toolbit_requested(self): |
| | """Handles request to import a toolbit.""" |
| | Path.Log.debug("_on_import_toolbit_requested: Called.") |
| | current_library = self.browser.get_current_library() |
| | if not current_library: |
| | Path.Log.warning("Cannot import toolbit: No library selected.") |
| | QMessageBox.warning( |
| | self, |
| | FreeCAD.Qt.translate("CAM", "Warning"), |
| | FreeCAD.Qt.translate("CAM", "Please select a library first."), |
| | ) |
| | return |
| |
|
| | dialog = AssetOpenDialog( |
| | cam_assets, asset_class=ToolBit, serializers=toolbit_serializers, parent=self |
| | ) |
| | response = dialog.exec_() |
| | if not response: |
| | return |
| | file_path, toolbit = cast(Tuple[pathlib.Path, ToolBit], response) |
| |
|
| | |
| | Path.Log.info( |
| | f"IMPORT TOOLBIT: file_path={file_path}, toolbit.id={toolbit.id}, toolbit.label={toolbit.label}" |
| | ) |
| | import traceback |
| |
|
| | stack = traceback.format_stack() |
| | caller_info = "".join(stack[-3:-1]) |
| | Path.Log.info(f"IMPORT TOOLBIT CALLER:\n{caller_info}") |
| |
|
| | |
| | toolbit_uri = toolbit.get_uri() |
| | Path.Log.info(f"IMPORT CHECK: toolbit_uri={toolbit_uri}") |
| | existing_toolbit = None |
| | try: |
| | existing_toolbit = cam_assets.get(toolbit_uri, store=["local", "builtin"], depth=0) |
| | Path.Log.info( |
| | f"IMPORT CHECK: Toolbit {toolbit.id} already exists, using existing reference" |
| | ) |
| | Path.Log.info( |
| | f"IMPORT CHECK: existing_toolbit.id={existing_toolbit.id}, existing_toolbit.label={existing_toolbit.label}" |
| | ) |
| | except FileNotFoundError: |
| | |
| | Path.Log.info(f"IMPORT CHECK: Toolbit {toolbit.id} is new, saving to disk") |
| | new_uri = cam_assets.add(toolbit) |
| | Path.Log.info(f"IMPORT CHECK: Toolbit saved with new URI: {new_uri}") |
| | existing_toolbit = toolbit |
| |
|
| | |
| | Path.Log.info( |
| | f"IMPORT ADD: Adding toolbit {existing_toolbit.id} to library {current_library.label}" |
| | ) |
| | added_toolbit = current_library.add_bit(existing_toolbit) |
| | if added_toolbit: |
| | Path.Log.info(f"IMPORT ADD: Successfully added toolbit to library") |
| | cam_assets.add(current_library) |
| | self.browser.refresh() |
| | self.browser.select_by_uri([str(existing_toolbit.get_uri())]) |
| | self._update_button_states() |
| | else: |
| | Path.Log.warning(f"IMPORT ADD: Failed to add toolbit {existing_toolbit.id} to library") |
| | Path.Log.warning( |
| | f"IMPORT FAILED: Failed to import toolbit from {file_path} to library {current_library.label}." |
| | ) |
| | QMessageBox.warning( |
| | self, |
| | FreeCAD.Qt.translate("CAM", "Warning"), |
| | FreeCAD.Qt.translate( |
| | "CAM", |
| | f"Failed to import toolbit from '{file_path}' to library '{current_library.label}'.", |
| | ), |
| | ) |
| |
|
| | def _on_export_toolbit_requested(self): |
| | """Handles request to export the selected toolbit.""" |
| | Path.Log.debug("_on_export_toolbit_requested: Called.") |
| | selected_toolbits = self.browser.get_selected_bits() |
| | if not selected_toolbits: |
| | Path.Log.warning("Cannot export toolbit: No toolbit selected.") |
| | QMessageBox.warning( |
| | self, |
| | FreeCAD.Qt.translate("CAM", "Warning"), |
| | FreeCAD.Qt.translate("CAM", "Please select a toolbit to export."), |
| | ) |
| | return |
| |
|
| | if len(selected_toolbits) > 1: |
| | Path.Log.warning("Cannot export multiple toolbits: Please select only one.") |
| | QMessageBox.warning( |
| | self, |
| | FreeCAD.Qt.translate("CAM", "Warning"), |
| | FreeCAD.Qt.translate("CAM", "Please select only one toolbit to export."), |
| | ) |
| | return |
| |
|
| | toolbit_to_export = selected_toolbits[0] |
| | dialog = AssetSaveDialog(asset_class=ToolBit, serializers=toolbit_serializers, parent=self) |
| | dialog.exec_(toolbit_to_export) |
| | self._update_button_states() |
| |
|