Merge pull request #25106 from davidgilkaufman/tool_migrations_custom_dir

[CAM] Offer automatic migration of tools in (old) custom working directory to the new tool system
This commit is contained in:
sliptonic
2025-11-23 12:28:17 -06:00
committed by GitHub
3 changed files with 98 additions and 41 deletions

View File

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

View File

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

View File

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