diff --git a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py index 1e49be1461..884f429389 100644 --- a/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py +++ b/src/Mod/CAM/Path/Tool/assets/ui/filedialog.py @@ -23,6 +23,7 @@ import pathlib from typing import Optional, Tuple, Type, Iterable from PySide.QtWidgets import QFileDialog, QMessageBox +from ...camassets import cam_assets from ..manager import AssetManager from ..serializer import AssetSerializer, Asset from .util import ( @@ -30,6 +31,7 @@ from .util import ( make_export_filters, get_serializer_from_extension, ) +import Path import Path.Preferences as Preferences @@ -56,7 +58,7 @@ class AssetOpenDialog(QFileDialog): if filters: self.selectNameFilter(filters[0]) # Default to "All supported files" - def _deserialize_selected_file(self, file_path: pathlib.Path) -> Optional[Asset]: + def deserialize_file(self, file_path: pathlib.Path, quiet=False) -> Optional[Asset]: """Deserialize the selected file using the appropriate serializer.""" # Find the correct serializer for the file. file_extension = file_path.suffix.lower() @@ -64,11 +66,15 @@ class AssetOpenDialog(QFileDialog): self.serializers, file_extension, for_import=True ) if not serializer_class: - QMessageBox.critical( - self, - "Error", - f"No supported serializer found for file extension '{file_extension}'", - ) + message = f"No supported serializer found for file extension '{file_extension}'" + if quiet: + Path.Log.error(message) + else: + QMessageBox.critical( + self, + "Error", + message, + ) return None # Check whether all dependencies for importing the file exist. @@ -101,32 +107,38 @@ class AssetOpenDialog(QFileDialog): break if not dependency_found: - QMessageBox.critical( - self, - "Error", - f"Failed to import {file_path}: required dependency {dependency_uri} not found in stores or in parallel Bit directory", - ) + message = f"Failed to import {file_path}: required dependency {dependency_uri} not found in stores or in parallel Bit directory" + if quiet: + Path.Log.error(message) + else: + QMessageBox.critical( + self, + "Error", + message, + ) return None # If we found external toolbits, ask user if they want to import them if external_toolbits: - from PySide.QtGui import QMessageBox - toolbit_names = [uri.asset_id for uri, _ in external_toolbits] - reply = QMessageBox.question( - self, - "Import External Toolbits", - f"This library references {len(external_toolbits)} toolbit(s) that are not in your local store:\n\n" - + "\n".join(f"• {name}" for name in toolbit_names) - + f"\n\nWould you like to import these toolbits into your local store?\n" - + "This will make them permanently available for use in other libraries.", - QMessageBox.Yes | QMessageBox.No, - QMessageBox.Yes, - ) + if quiet: + Path.Log.info("Importing tool bits for the library") + reply = QMessageBox.Yes + else: + reply = QMessageBox.question( + self, + "Import External Toolbits", + f"This library references {len(external_toolbits)} toolbit(s) that are not in your local store:\n\n" + + "\n".join(f"• {name}" for name in toolbit_names) + + f"\n\nWould you like to import these toolbits into your local store?\n" + + "This will make them permanently available for use in other libraries.", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.Yes, + ) if reply == QMessageBox.Yes: # Import the external toolbits into local store - self._import_external_toolbits(external_toolbits) + self._import_external_toolbits(external_toolbits, quiet=quiet) # After importing, use regular deserialization since toolbits are now in local store else: # User declined import, use context-aware deserialization for external loading @@ -160,7 +172,7 @@ class AssetOpenDialog(QFileDialog): filenames = self.selectedFiles() if filenames: file_path = pathlib.Path(filenames[0]) - asset = self._deserialize_selected_file(file_path) + asset = self.deserialize_file(file_path) if asset: return file_path, asset return None @@ -183,7 +195,7 @@ class AssetOpenDialog(QFileDialog): # Fallback to home directory if anything goes wrong return pathlib.Path.home() - def _import_external_toolbits(self, external_toolbits): + def _import_external_toolbits(self, external_toolbits, quiet=False): """Import external toolbits into the local asset store.""" from ...toolbit.serializers import all_serializers as toolbit_serializers from .util import get_serializer_from_extension @@ -214,6 +226,7 @@ class AssetOpenDialog(QFileDialog): toolbit.id = dependency_uri.asset_id # Import the toolbit into local store + cam_assets.add(toolbit) imported_count += 1 except Exception as e: @@ -226,12 +239,18 @@ class AssetOpenDialog(QFileDialog): message += f"\n\nFailed to import {len(failed_imports)} toolbit(s):\n" + "\n".join( failed_imports ) - QMessageBox.information(self, "Import Results", message) + if quiet: + Path.Log.info(message) + else: + QMessageBox.information(self, "Import Results", message) elif failed_imports: message = f"Failed to import all {len(failed_imports)} toolbit(s):\n" + "\n".join( failed_imports ) - QMessageBox.warning(self, "Import Failed", message) + if quiet: + Path.Log.error(message) + else: + QMessageBox.warning(self, "Import Failed", message) class AssetSaveDialog(QFileDialog): diff --git a/src/Mod/CAM/Path/Tool/migration/migration.py b/src/Mod/CAM/Path/Tool/migration/migration.py index 049f92cea1..40f29c3144 100644 --- a/src/Mod/CAM/Path/Tool/migration/migration.py +++ b/src/Mod/CAM/Path/Tool/migration/migration.py @@ -30,6 +30,12 @@ import FreeCAD import Path import Path.Preferences import pathlib +import os +import glob +from ..assets.ui import AssetOpenDialog +from ..camassets import cam_assets +from ..library.serializers import all_serializers as library_serializers +from ..library.models import Library # Logging setup - same pattern as Job.py if False: @@ -58,17 +64,18 @@ class CAMAssetMigrator: self.pref_group_path = "User parameter:BaseApp/Preferences/Mod/CAM/Migration" def check_migration_needed(self): + self.check_asset_location() + self.check_tool_library_workdir() + + def check_asset_location(self): """ Check if CAM asset migration is needed for version upgrade. This method determines if the current CAM assets are stored in a custom location outside the default user data directory and if migration has not been offered for the current FreeCAD version. - - Returns: - bool: True if migration should be offered, False otherwise """ - Path.Log.info("Starting CAM asset migration check") + Path.Log.debug("Starting CAM asset migration check") try: # Get current directories @@ -100,8 +107,8 @@ class CAMAssetMigrator: Path.Log.debug("Already using current version, no migration needed") return - Path.Log.info("Migration is needed and should be offered") - if self._offer_migration_to_user(): + Path.Log.info("Asset relocation is needed and should be offered") + if self._offer_asset_relocation(): self._migrate_assets(str(current_asset_path)) return @@ -109,15 +116,44 @@ class CAMAssetMigrator: Path.Log.error(f"Error checking CAM asset migration: {e}") import traceback - Path.Log.debug(f"Full traceback: {traceback.format_exc()}") - return False + Path.Log.info(f"Full traceback: {traceback.format_exc()}") + return - def _offer_migration_to_user(self): + def check_tool_library_workdir(self): + workdir_str = "LastPathToolLibrary" + migrated_str = "Migrated" + workdir_str + workdir = Path.Preferences.preferences().GetString(workdir_str) + migrated_dir = Path.Preferences.preferences().GetString(migrated_str) + Path.Log.debug(f"workdir: {workdir}, migrated: {migrated_dir}") + if workdir and not migrated_dir: + # Look for tool libraries to import + if os.path.isdir(workdir): + libraries = [f for f in glob.glob(workdir + os.path.sep + "*.fctl")] + libraries.sort() + if len(libraries): + # Migrate libraries, automatically and silently + Path.Log.info("Migrating tool libraries into CAM assets") + for library in libraries: + Path.Log.info("Migrating " + library) + import_dialog = AssetOpenDialog( + cam_assets, + asset_class=Library, + serializers=library_serializers, + parent=None, + ) + asset = import_dialog.deserialize_file(pathlib.Path(library), quiet=True) + if asset: + cam_assets.add(asset) + + # Mark directory as migrated + Path.Preferences.preferences().SetString(migrated_str, workdir) + + def _offer_asset_relocation(self): """ - Present migration dialog to user. + Present asset relocation dialog to user. Returns: - bool: True if user accepted migration, False otherwise + bool: True if user accepted relocation, False otherwise """ # Get current version info major = int(FreeCAD.ConfigGet("BuildVersionMajor")) @@ -127,7 +163,7 @@ class CAMAssetMigrator: # Get current asset path for display current_asset_path = Path.Preferences.getAssetPath() - Path.Log.debug(f"Offering migration to user for version {current_version}") + Path.Log.debug(f"Offering asset relocation to user for version {current_version}") if not FreeCAD.GuiUp: Path.Log.debug("GUI not available, skipping migration offer") @@ -141,7 +177,7 @@ class CAMAssetMigrator: "This will copy your assets to a new directory." ) - Path.Log.debug("Showing migration dialog to user") + Path.Log.debug("Showing asset relocation dialog to user") reply = QMessageBox.question( None, "CAM Asset Migration", msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes diff --git a/src/Mod/CAM/Path/Tool/toolbit/models/base.py b/src/Mod/CAM/Path/Tool/toolbit/models/base.py index f9dcfc5a83..f5ce0f135d 100644 --- a/src/Mod/CAM/Path/Tool/toolbit/models/base.py +++ b/src/Mod/CAM/Path/Tool/toolbit/models/base.py @@ -128,6 +128,8 @@ class ToolBit(Asset, ABC): raise ValueError("ToolBit dictionary is missing 'shape' key") # Try to find the shape type. Default to Unknown if necessary. + if "shape" in attrs and "shape-type" not in attrs: + attrs["shape-type"] = attrs["shape"] shape_type = attrs.get("shape-type") shape_class = ToolBitShape.get_shape_class_from_id(shape_id, shape_type) if not shape_class: