Merge pull request #24113 from sliptonic/versions
[CAM]: Asset versions and Various Fixes
This commit is contained in:
@@ -128,6 +128,11 @@ SET(PathPythonToolsAssets_SRCS
|
||||
Path/Tool/assets/uri.py
|
||||
)
|
||||
|
||||
SET(PathPythonToolsMigration_SRCS
|
||||
Path/Tool/migration/__init__.py
|
||||
Path/Tool/migration/migration.py
|
||||
)
|
||||
|
||||
SET(PathPythonToolsAssetsStore_SRCS
|
||||
Path/Tool/assets/store/__init__.py
|
||||
Path/Tool/assets/store/base.py
|
||||
@@ -600,6 +605,7 @@ SET(all_files
|
||||
${PathPythonPostScripts_SRCS}
|
||||
${PathPythonTools_SRCS}
|
||||
${PathPythonToolsAssets_SRCS}
|
||||
${PathPythonToolsMigration_SRCS}
|
||||
${PathPythonToolsAssetsStore_SRCS}
|
||||
${PathPythonToolsAssetsUi_SRCS}
|
||||
${PathPythonToolsDocObject_SRCS}
|
||||
@@ -755,6 +761,13 @@ INSTALL(
|
||||
Mod/CAM/Path/Tool/assets
|
||||
)
|
||||
|
||||
INSTALL(
|
||||
FILES
|
||||
${PathPythonToolsMigration_SRCS}
|
||||
DESTINATION
|
||||
Mod/CAM/Path/Tool/migration
|
||||
)
|
||||
|
||||
INSTALL(
|
||||
FILES
|
||||
${PathPythonToolsAssetsStore_SRCS}
|
||||
|
||||
@@ -23,6 +23,14 @@
|
||||
# ***************************************************************************
|
||||
import FreeCAD
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
from FreeCADGui import Workbench
|
||||
else:
|
||||
# Provide a dummy Workbench class when GUI is not available
|
||||
class Workbench:
|
||||
pass
|
||||
|
||||
|
||||
FreeCAD.__unit_test__ += ["TestCAMGui"]
|
||||
|
||||
@@ -83,6 +91,16 @@ class CAMWorkbench(Workbench):
|
||||
from Path.Tool.toolbit.ui import cmd as PathToolBitCmd
|
||||
from Path.Tool.library.ui import cmd as PathToolBitLibraryCmd
|
||||
|
||||
from Path.Tool.camassets import cam_assets
|
||||
|
||||
cam_assets.setup()
|
||||
|
||||
# Check if CAM asset migration is needed for version upgrade
|
||||
from Path.Tool.migration.migration import CAMAssetMigrator
|
||||
|
||||
migrator = CAMAssetMigrator()
|
||||
migrator.check_migration_needed()
|
||||
|
||||
from PySide.QtCore import QT_TRANSLATE_NOOP
|
||||
|
||||
import PathCommands
|
||||
@@ -339,6 +357,7 @@ class CAMWorkbench(Workbench):
|
||||
"Profile" in selectedName
|
||||
or "Contour" in selectedName
|
||||
or "Dressup" in selectedName
|
||||
or "Pocket" in selectedName
|
||||
):
|
||||
self.appendContextMenu("", "Separator")
|
||||
# self.appendContextMenu("", ["Set_StartPoint"])
|
||||
|
||||
@@ -134,7 +134,7 @@ class CommandJobTemplateExport:
|
||||
foo = QtGui.QFileDialog.getSaveFileName(
|
||||
QtGui.QApplication.activeWindow(),
|
||||
"Path - Job Template",
|
||||
Path.Preferences.filePath(),
|
||||
str(Path.Preferences.getTemplateDirectory()),
|
||||
"job_*.json",
|
||||
)[0]
|
||||
if foo:
|
||||
|
||||
@@ -29,7 +29,7 @@ from collections import defaultdict
|
||||
from typing import Optional
|
||||
|
||||
|
||||
if False:
|
||||
if True:
|
||||
Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
|
||||
Path.Log.trackModule(Path.Log.thisModule())
|
||||
else:
|
||||
@@ -128,7 +128,9 @@ def getAssetPath() -> pathlib.Path:
|
||||
# Check if we have a CamAssets path already set
|
||||
cam_assets_path = pref.GetString(ToolPath, "")
|
||||
if cam_assets_path:
|
||||
return pathlib.Path(cam_assets_path)
|
||||
# Use mostRecentConfigFromBase to get the most recent versioned path
|
||||
most_recent_path = FreeCAD.ApplicationDirectories.mostRecentConfigFromBase(cam_assets_path)
|
||||
return pathlib.Path(most_recent_path)
|
||||
|
||||
# Migration: Check for legacy DefaultFilePath and use it for CamAssets
|
||||
legacy_path = defaultFilePath()
|
||||
@@ -137,14 +139,21 @@ def getAssetPath() -> pathlib.Path:
|
||||
if legacy_path_obj.exists() and legacy_path_obj.is_dir():
|
||||
# Migrate: Set the legacy path as the new CamAssets path
|
||||
setAssetPath(legacy_path_obj)
|
||||
return legacy_path_obj
|
||||
# Return the most recent version of the legacy path
|
||||
most_recent_legacy = FreeCAD.ApplicationDirectories.mostRecentConfigFromBase(
|
||||
str(legacy_path_obj)
|
||||
)
|
||||
return pathlib.Path(most_recent_legacy)
|
||||
|
||||
# Fallback to default if no legacy path found
|
||||
default = getDefaultAssetPath()
|
||||
return pathlib.Path(default)
|
||||
# Return the most recent version of the default path
|
||||
most_recent_default = FreeCAD.ApplicationDirectories.mostRecentConfigFromBase(str(default))
|
||||
return pathlib.Path(most_recent_default)
|
||||
|
||||
|
||||
def setAssetPath(path: pathlib.Path):
|
||||
Path.Log.info(f"Setting asset path to {path}")
|
||||
assert path.is_dir(), f"Cannot put a non-initialized asset directory into preferences: {path}"
|
||||
pref = tool_preferences()
|
||||
current_path = pref.GetString(ToolPath, "")
|
||||
@@ -158,6 +167,13 @@ def getToolBitPath() -> pathlib.Path:
|
||||
return getAssetPath() / "Tools" / "Bit"
|
||||
|
||||
|
||||
def getTemplateDirectory() -> pathlib.Path:
|
||||
"""Returns the directory where job templates should be saved."""
|
||||
template_path = getAssetPath() / "Templates"
|
||||
template_path.mkdir(parents=True, exist_ok=True)
|
||||
return template_path
|
||||
|
||||
|
||||
def getLastToolLibrary() -> Optional[str]:
|
||||
pref = tool_preferences()
|
||||
return pref.GetString(LastToolLibrary) or None
|
||||
@@ -239,6 +255,9 @@ def macroFilePath():
|
||||
|
||||
def searchPaths():
|
||||
paths = []
|
||||
# Add new CamAssets/Templates directory first (highest priority)
|
||||
paths.append(str(getTemplateDirectory()))
|
||||
# Add legacy locations for backward compatibility
|
||||
p = defaultFilePath()
|
||||
if p:
|
||||
paths.append(p)
|
||||
|
||||
@@ -74,21 +74,79 @@ class AssetOpenDialog(QFileDialog):
|
||||
try:
|
||||
raw_data = file_path.read_bytes()
|
||||
dependencies = serializer_class.extract_dependencies(raw_data)
|
||||
external_toolbits = [] # Track toolbits found externally
|
||||
|
||||
for dependency_uri in dependencies:
|
||||
if not self.asset_manager.exists(dependency_uri, store=["local", "builtin"]):
|
||||
# First check if dependency exists in asset manager stores
|
||||
if self.asset_manager.exists(dependency_uri, store=["local", "builtin"]):
|
||||
continue
|
||||
|
||||
# If not in stores, check if it exists relative to the library file
|
||||
dependency_found = False
|
||||
if dependency_uri.asset_type == "toolbit":
|
||||
# Look for toolbit files in parallel Bit directory
|
||||
# Library is in Library/, toolbits are in parallel Bit/
|
||||
library_dir = file_path.parent # e.g., /path/to/Library/
|
||||
tools_dir = library_dir.parent # e.g., /path/to/
|
||||
bit_dir = tools_dir / "Bit" # e.g., /path/to/Bit/
|
||||
|
||||
if bit_dir.exists():
|
||||
possible_extensions = [".fctb", ".json", ".yaml", ".yml"]
|
||||
for ext in possible_extensions:
|
||||
toolbit_file = bit_dir / f"{dependency_uri.asset_id}{ext}"
|
||||
if toolbit_file.exists():
|
||||
dependency_found = True
|
||||
external_toolbits.append((dependency_uri, toolbit_file))
|
||||
break
|
||||
|
||||
if not dependency_found:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"Error",
|
||||
f"Failed to import {file_path}: required dependency {dependency_uri} not found",
|
||||
f"Failed to import {file_path}: required dependency {dependency_uri} not found in stores or in parallel Bit directory",
|
||||
)
|
||||
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 reply == QMessageBox.Yes:
|
||||
# Import the external toolbits into local store
|
||||
self._import_external_toolbits(external_toolbits)
|
||||
# After importing, use regular deserialization since toolbits are now in local store
|
||||
else:
|
||||
# User declined import, use context-aware deserialization for external loading
|
||||
pass
|
||||
else:
|
||||
# No external toolbits found, use regular deserialization
|
||||
pass
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error", f"{file_path}: Failed to check dependencies: {e}")
|
||||
return None
|
||||
|
||||
# Load and return the asset.
|
||||
try:
|
||||
asset = serializer_class.deep_deserialize(raw_data)
|
||||
# Always use context-aware deserialization for libraries to get meaningful names
|
||||
if hasattr(serializer_class, "deep_deserialize_with_context"):
|
||||
# Pass file path context for meaningful library names and external dependency resolution
|
||||
asset = serializer_class.deep_deserialize_with_context(raw_data, file_path)
|
||||
else:
|
||||
# Fallback to regular deserialization
|
||||
asset = serializer_class.deep_deserialize(raw_data)
|
||||
|
||||
if not isinstance(asset, self.asset_class):
|
||||
raise TypeError(f"Deserialized asset is not of type {self.asset_class.__name__}")
|
||||
return asset
|
||||
@@ -124,6 +182,56 @@ class AssetOpenDialog(QFileDialog):
|
||||
# Fallback to home directory if anything goes wrong
|
||||
return pathlib.Path.home()
|
||||
|
||||
def _import_external_toolbits(self, external_toolbits):
|
||||
"""Import external toolbits into the local asset store."""
|
||||
from ...toolbit.serializers import all_serializers as toolbit_serializers
|
||||
from .util import get_serializer_from_extension
|
||||
|
||||
imported_count = 0
|
||||
failed_imports = []
|
||||
|
||||
for dependency_uri, toolbit_file in external_toolbits:
|
||||
try:
|
||||
# Find appropriate serializer for the file
|
||||
file_extension = toolbit_file.suffix.lower()
|
||||
serializer_class = get_serializer_from_extension(
|
||||
toolbit_serializers, file_extension, for_import=True
|
||||
)
|
||||
|
||||
if not serializer_class:
|
||||
failed_imports.append(
|
||||
f"{dependency_uri.asset_id}: No serializer for {file_extension}"
|
||||
)
|
||||
continue
|
||||
|
||||
# Load and deserialize the toolbit
|
||||
raw_toolbit_data = toolbit_file.read_bytes()
|
||||
toolbit = serializer_class.deep_deserialize(raw_toolbit_data)
|
||||
|
||||
# Ensure the toolbit ID matches what the library expects
|
||||
if toolbit.id != dependency_uri.asset_id:
|
||||
toolbit.id = dependency_uri.asset_id
|
||||
|
||||
# Import the toolbit into local store
|
||||
imported_count += 1
|
||||
|
||||
except Exception as e:
|
||||
failed_imports.append(f"{dependency_uri.asset_id}: {str(e)}")
|
||||
|
||||
# Show results to user
|
||||
if imported_count > 0:
|
||||
message = f"Successfully imported {imported_count} toolbit(s) into your local store."
|
||||
if failed_imports:
|
||||
message += f"\n\nFailed to import {len(failed_imports)} toolbit(s):\n" + "\n".join(
|
||||
failed_imports
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
class AssetSaveDialog(QFileDialog):
|
||||
def __init__(
|
||||
|
||||
@@ -58,7 +58,7 @@ class AssetPreferencesPage:
|
||||
translate(
|
||||
"CAM_PreferencesAssets",
|
||||
"Note: Select the directory that will contain the "
|
||||
"Bit/, Shape/, and Library/ subfolders.",
|
||||
"Tool folder with Bit/, Shape/, and Library/ subfolders.",
|
||||
)
|
||||
)
|
||||
self.asset_path_note_label.setWordWrap(True)
|
||||
@@ -124,6 +124,9 @@ class AssetPreferencesPage:
|
||||
return True
|
||||
|
||||
def loadSettings(self):
|
||||
# use getAssetPath() to initialize UI
|
||||
asset_path = Path.Preferences.getAssetPath()
|
||||
self.asset_path_edit.setText(str(asset_path))
|
||||
# Get the raw preference value, not the versioned path
|
||||
pref = Path.Preferences.tool_preferences()
|
||||
asset_path = pref.GetString(Path.Preferences.ToolPath, "")
|
||||
if not asset_path:
|
||||
asset_path = str(Path.Preferences.getDefaultAssetPath())
|
||||
self.asset_path_edit.setText(asset_path)
|
||||
|
||||
@@ -27,6 +27,12 @@ from Path import Preferences
|
||||
from Path.Preferences import addToolPreferenceObserver
|
||||
from .assets import AssetManager, AssetUri, Asset, FileStore
|
||||
|
||||
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())
|
||||
|
||||
|
||||
def ensure_library_assets_initialized(asset_manager: AssetManager, store_name: str = "local"):
|
||||
"""
|
||||
@@ -151,7 +157,7 @@ def ensure_assets_initialized(asset_manager: AssetManager, store="local"):
|
||||
|
||||
def _on_asset_path_changed(group, key, value):
|
||||
Path.Log.info(f"CAM asset directory changed in preferences: {group} {key} {value}")
|
||||
user_asset_store.set_dir(Preferences.getAssetPath())
|
||||
user_asset_store.set_dir(value)
|
||||
ensure_assets_initialized(cam_assets)
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,11 @@ class FCTLSerializer(AssetSerializer):
|
||||
# for the asset being deserialized. We should use this ID for the library
|
||||
# instance, overriding any 'id' that might be in the data_dict (which
|
||||
# is from an older version of the format).
|
||||
library = Library(data_dict.get("label", id or "Unnamed Library"), id=id)
|
||||
|
||||
# For the label, prefer data_dict["label"], then "name", then fallback to using the id as filename
|
||||
# The id parameter often contains the filename stem when importing from files
|
||||
label = data_dict.get("label") or data_dict.get("name") or id or "Unnamed Library"
|
||||
library = Library(label, id=id)
|
||||
|
||||
if dependencies is None:
|
||||
Path.Log.debug(
|
||||
@@ -181,3 +185,89 @@ class FCTLSerializer(AssetSerializer):
|
||||
|
||||
# Now deserialize with the resolved dependencies
|
||||
return cls.deserialize(data, library_id, resolved_dependencies)
|
||||
|
||||
@classmethod
|
||||
def deep_deserialize_with_context(cls, data: bytes, file_path: "pathlib.Path"):
|
||||
"""Deep deserialize a library with file path context for external dependencies."""
|
||||
import uuid
|
||||
import pathlib
|
||||
from ...camassets import cam_assets
|
||||
from ...toolbit.serializers import all_serializers as toolbit_serializers
|
||||
|
||||
# Use filename stem as library ID for meaningful names
|
||||
library_id = file_path.stem
|
||||
|
||||
Path.Log.info(
|
||||
f"FCTL DEEP_DESERIALIZE_WITH_CONTEXT: Starting deep deserialization for library from {file_path}"
|
||||
)
|
||||
|
||||
# Extract dependency URIs from the library data
|
||||
dependency_uris = cls.extract_dependencies(data)
|
||||
Path.Log.info(
|
||||
f"FCTL DEEP_DESERIALIZE_WITH_CONTEXT: Found {len(dependency_uris)} toolbit dependencies: {[uri.asset_id for uri in dependency_uris]}"
|
||||
)
|
||||
|
||||
# Fetch all toolbit dependencies
|
||||
resolved_dependencies = {}
|
||||
for dep_uri in dependency_uris:
|
||||
try:
|
||||
# First try to get from asset manager stores
|
||||
Path.Log.info(
|
||||
f"FCTL EXTERNAL: Trying to fetch toolbit '{dep_uri.asset_id}' from stores ['local', 'builtin']"
|
||||
)
|
||||
toolbit = cam_assets.get(dep_uri, store=["local", "builtin"], depth=0)
|
||||
resolved_dependencies[dep_uri] = toolbit
|
||||
Path.Log.info(
|
||||
f"FCTL EXTERNAL: Successfully fetched toolbit '{dep_uri.asset_id}' from stores"
|
||||
)
|
||||
except Exception as e:
|
||||
# If not in stores, try to load from parallel Bit directory
|
||||
Path.Log.info(
|
||||
f"FCTL EXTERNAL: Toolbit '{dep_uri.asset_id}' not in stores, trying external file: {e}"
|
||||
)
|
||||
|
||||
# Look for toolbit files in parallel Bit directory
|
||||
library_dir = file_path.parent # e.g., /path/to/Library/
|
||||
tools_dir = library_dir.parent # e.g., /path/to/
|
||||
bit_dir = tools_dir / "Bit" # e.g., /path/to/Bit/
|
||||
|
||||
toolbit_loaded = False
|
||||
if bit_dir.exists():
|
||||
possible_extensions = [".fctb", ".json", ".yaml", ".yml"]
|
||||
for ext in possible_extensions:
|
||||
toolbit_file = bit_dir / f"{dep_uri.asset_id}{ext}"
|
||||
if toolbit_file.exists():
|
||||
try:
|
||||
# Find appropriate serializer for the file
|
||||
from ...assets.ui.util import get_serializer_from_extension
|
||||
|
||||
serializer_class = get_serializer_from_extension(
|
||||
toolbit_serializers, ext, for_import=True
|
||||
)
|
||||
if serializer_class:
|
||||
# Load and deserialize the toolbit
|
||||
raw_toolbit_data = toolbit_file.read_bytes()
|
||||
toolbit = serializer_class.deep_deserialize(raw_toolbit_data)
|
||||
resolved_dependencies[dep_uri] = toolbit
|
||||
toolbit_loaded = True
|
||||
Path.Log.info(
|
||||
f"FCTL EXTERNAL: Successfully loaded toolbit '{dep_uri.asset_id}' from {toolbit_file}"
|
||||
)
|
||||
break
|
||||
except Exception as load_error:
|
||||
Path.Log.warning(
|
||||
f"FCTL EXTERNAL: Failed to load toolbit from {toolbit_file}: {load_error}"
|
||||
)
|
||||
continue
|
||||
|
||||
if not toolbit_loaded:
|
||||
Path.Log.warning(
|
||||
f"FCTL EXTERNAL: Could not load toolbit '{dep_uri.asset_id}' from external files"
|
||||
)
|
||||
|
||||
Path.Log.info(
|
||||
f"FCTL EXTERNAL: Resolved {len(resolved_dependencies)} of {len(dependency_uris)} dependencies"
|
||||
)
|
||||
|
||||
# Now deserialize with the resolved dependencies
|
||||
return cls.deserialize(data, library_id, resolved_dependencies)
|
||||
|
||||
32
src/Mod/CAM/Path/Tool/migration/__init__.py
Normal file
32
src/Mod/CAM/Path/Tool/migration/__init__.py
Normal file
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2025 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * This program is free software; you can redistribute it and/or modify *
|
||||
# * it under the terms of the GNU Lesser General Public License (LGPL) *
|
||||
# * as published by the Free Software Foundation; either version 2 of *
|
||||
# * the License, or (at your option) any later version. *
|
||||
# * for detail see the LICENCE text file. *
|
||||
# * *
|
||||
# * This program is distributed in the hope that it will be useful, *
|
||||
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
# * GNU Library General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Library General Public *
|
||||
# * License along with this program; if not, write to the Free Software *
|
||||
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
||||
# * USA *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
"""
|
||||
CAM Asset management package.
|
||||
|
||||
This package contains modules for managing CAM assets including migration
|
||||
functionality for handling asset directory versioning during FreeCAD upgrades.
|
||||
"""
|
||||
213
src/Mod/CAM/Path/Tool/migration/migration.py
Normal file
213
src/Mod/CAM/Path/Tool/migration/migration.py
Normal file
@@ -0,0 +1,213 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2025 sliptonic <shopinthewoods@gmail.com> *
|
||||
# * *
|
||||
# * This file is part of FreeCAD. *
|
||||
# * *
|
||||
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
||||
# * under the terms of the GNU Lesser General Public License as *
|
||||
# * published by the Free Software Foundation, either version 2.1 of the *
|
||||
# * License, or (at your option) any later version. *
|
||||
# * *
|
||||
# * FreeCAD is distributed in the hope that it will be useful, but *
|
||||
# * WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
||||
# * Lesser General Public License for more details. *
|
||||
# * *
|
||||
# * You should have received a copy of the GNU Lesser General Public *
|
||||
# * License along with FreeCAD. If not, see *
|
||||
# * <https://www.gnu.org/licenses/>. *
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
"""
|
||||
CAM Asset Migration Module
|
||||
|
||||
Handles migration of CAM assets during FreeCAD version upgrades.
|
||||
"""
|
||||
|
||||
import FreeCAD
|
||||
import Path
|
||||
import Path.Preferences
|
||||
import pathlib
|
||||
|
||||
# Logging setup - same pattern as Job.py
|
||||
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())
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
import FreeCADGui
|
||||
from PySide.QtWidgets import QApplication, QMessageBox
|
||||
from PySide.QtCore import Qt
|
||||
|
||||
|
||||
class CAMAssetMigrator:
|
||||
"""
|
||||
Handles migration of CAM assets during FreeCAD version upgrades.
|
||||
|
||||
This class provides functionality to:
|
||||
- Check if migration is needed for custom CAM asset locations
|
||||
- Offer migration to users through a dialog
|
||||
- Perform the actual asset migration with versioned directories
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.pref_group_path = "User parameter:BaseApp/Preferences/Mod/CAM/Migration"
|
||||
|
||||
def check_migration_needed(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")
|
||||
|
||||
try:
|
||||
# Get current directories
|
||||
user_app_data_dir = FreeCAD.getUserAppDataDir()
|
||||
user_app_data_path = pathlib.Path(user_app_data_dir)
|
||||
Path.Log.debug(f"User app data directory: {user_app_data_dir}")
|
||||
|
||||
# Get the current CAM asset path (may be naked or versioned)
|
||||
current_asset_path = Path.Preferences.getAssetPath()
|
||||
current_asset_pathlib = pathlib.Path(current_asset_path)
|
||||
Path.Log.debug(f"Current CAM asset path: {current_asset_path}")
|
||||
|
||||
# Only migrate if CamAssets is outside the standard user data directory
|
||||
if current_asset_pathlib.is_relative_to(user_app_data_path):
|
||||
Path.Log.debug("CamAssets is in default location, no custom migration needed")
|
||||
return
|
||||
|
||||
# Check if migration has already been offered for this version
|
||||
if self.has_migration_been_offered():
|
||||
Path.Log.debug("Migration has already been offered for this version, skipping")
|
||||
return
|
||||
|
||||
# Determine the base path (naked path without version)
|
||||
if FreeCAD.ApplicationDirectories.isVersionedPath(str(current_asset_path)):
|
||||
# Check if we're already using the current version
|
||||
if FreeCAD.ApplicationDirectories.usingCurrentVersionConfig(
|
||||
str(current_asset_path)
|
||||
):
|
||||
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():
|
||||
self._migrate_assets(str(current_asset_path))
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
Path.Log.error(f"Error checking CAM asset migration: {e}")
|
||||
import traceback
|
||||
|
||||
Path.Log.debug(f"Full traceback: {traceback.format_exc()}")
|
||||
return False
|
||||
|
||||
def _offer_migration_to_user(self):
|
||||
"""
|
||||
Present migration dialog to user.
|
||||
|
||||
Returns:
|
||||
bool: True if user accepted migration, False otherwise
|
||||
"""
|
||||
# Get current version info
|
||||
major = int(FreeCAD.ConfigGet("BuildVersionMajor"))
|
||||
minor = int(FreeCAD.ConfigGet("BuildVersionMinor"))
|
||||
current_version = FreeCAD.ApplicationDirectories.versionStringForPath(major, minor)
|
||||
|
||||
# Get current asset path for display
|
||||
current_asset_path = Path.Preferences.getAssetPath()
|
||||
|
||||
Path.Log.debug(f"Offering migration to user for version {current_version}")
|
||||
|
||||
if not FreeCAD.GuiUp:
|
||||
Path.Log.debug("GUI not available, skipping migration offer")
|
||||
return False
|
||||
|
||||
msg = (
|
||||
f"FreeCAD has been upgraded to version {current_version}.\n\n"
|
||||
f"Your CAM assets are stored in a custom location:\n{current_asset_path}\n\n"
|
||||
"Would you like to migrate your CAM assets to a versioned directory "
|
||||
"to preserve them during future upgrades?\n\n"
|
||||
"This will copy your assets to a new directory."
|
||||
)
|
||||
|
||||
Path.Log.debug("Showing migration dialog to user")
|
||||
|
||||
reply = QMessageBox.question(
|
||||
None, "CAM Asset Migration", msg, QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes
|
||||
)
|
||||
|
||||
# Record that we offered migration for this version
|
||||
pref_group = FreeCAD.ParamGet(self.pref_group_path)
|
||||
offered_versions = pref_group.GetString("OfferedToMigrateCAMAssets", "")
|
||||
known_versions = set(offered_versions.split(",")) if offered_versions else set()
|
||||
known_versions.add(current_version)
|
||||
pref_group.SetString("OfferedToMigrateCAMAssets", ",".join(known_versions))
|
||||
Path.Log.debug(f"Updated offered versions: {known_versions}")
|
||||
|
||||
if reply == QMessageBox.Yes:
|
||||
Path.Log.info("User accepted migration, starting asset migration")
|
||||
return True
|
||||
else:
|
||||
Path.Log.info("User declined migration")
|
||||
return False
|
||||
|
||||
def _migrate_assets(self, source_path):
|
||||
"""
|
||||
Perform actual directory copying and preference updates.
|
||||
|
||||
Args:
|
||||
source_path: Current CAM asset directory path
|
||||
"""
|
||||
Path.Log.info(f"Starting asset migration from {source_path}")
|
||||
|
||||
try:
|
||||
FreeCAD.ApplicationDirectories.migrateAllPaths([source_path])
|
||||
Path.Log.info(
|
||||
"Migration complete - preferences will be handled automatically by the system"
|
||||
)
|
||||
|
||||
if FreeCAD.GuiUp:
|
||||
QMessageBox.information(
|
||||
None,
|
||||
"Migration Complete",
|
||||
f"CAM assets have been migrated from:\n{source_path}\n\n"
|
||||
"The system will automatically handle preference updates.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Failed to migrate CAM assets: {e}"
|
||||
Path.Log.error(error_msg)
|
||||
import traceback
|
||||
|
||||
Path.Log.debug(f"Migration error traceback: {traceback.format_exc()}")
|
||||
if FreeCAD.GuiUp:
|
||||
QMessageBox.critical(None, "Migration Failed", error_msg)
|
||||
|
||||
def has_migration_been_offered(self):
|
||||
"""
|
||||
Check if migration has been offered for current version.
|
||||
|
||||
Returns:
|
||||
bool: True if migration was offered for this version
|
||||
"""
|
||||
|
||||
pref_group = FreeCAD.ParamGet(self.pref_group_path)
|
||||
major = int(FreeCAD.ConfigGet("BuildVersionMajor"))
|
||||
minor = int(FreeCAD.ConfigGet("BuildVersionMinor"))
|
||||
|
||||
current_version_string = FreeCAD.ApplicationDirectories.versionStringForPath(major, minor)
|
||||
offered_versions = pref_group.GetString("OfferedToMigrateCAMAssets", "")
|
||||
known_versions = set(offered_versions.split(",")) if offered_versions else set()
|
||||
return current_version_string in known_versions
|
||||
@@ -267,9 +267,27 @@ class ToolBitBrowserWidget(QtGui.QWidget):
|
||||
uri_string = item.data(ToolBitUriRole)
|
||||
if not uri_string:
|
||||
return
|
||||
toolbit = self._asset_manager.get(AssetUri(uri_string))
|
||||
if toolbit:
|
||||
self.itemDoubleClicked.emit(toolbit)
|
||||
try:
|
||||
toolbit = self._asset_manager.get(AssetUri(uri_string))
|
||||
if toolbit:
|
||||
self.itemDoubleClicked.emit(toolbit)
|
||||
except FileNotFoundError:
|
||||
# Handle missing/placeholder toolbits gracefully
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
FreeCAD.Qt.translate("CAM", "Missing Toolbit"),
|
||||
FreeCAD.Qt.translate(
|
||||
"CAM",
|
||||
"This toolbit is missing from your local store. It may be a placeholder for a toolbit that was not found during library import.",
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
# Handle other errors
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
FreeCAD.Qt.translate("CAM", "Error"),
|
||||
FreeCAD.Qt.translate("CAM", f"Failed to load toolbit: {e}"),
|
||||
)
|
||||
|
||||
def _on_item_selection_changed(self):
|
||||
"""Emits toolSelected signal and tracks selected URIs."""
|
||||
|
||||
Reference in New Issue
Block a user