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:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user