2026-02-16
This commit is contained in:
@@ -328,6 +328,8 @@ class ATOMIC_OT_inspect_materials(bpy.types.Operator):
|
||||
|
||||
# user lists
|
||||
users_objects = []
|
||||
users_brushes = []
|
||||
users_node_groups = []
|
||||
|
||||
def draw(self, context):
|
||||
global inspection_update_trigger
|
||||
@@ -349,13 +351,35 @@ class ATOMIC_OT_inspect_materials(bpy.types.Operator):
|
||||
if atom.materials_field in bpy.data.materials.keys():
|
||||
self.users_objects = \
|
||||
users.material_objects(atom.materials_field)
|
||||
self.users_brushes = \
|
||||
users.material_brushes(atom.materials_field)
|
||||
self.users_node_groups = \
|
||||
users.material_node_groups_list(atom.materials_field)
|
||||
|
||||
# if key is invalid, empty the user lists
|
||||
else:
|
||||
self.users_objects = []
|
||||
self.users_brushes = []
|
||||
self.users_node_groups = []
|
||||
|
||||
inspection_update_trigger = False
|
||||
|
||||
# brushes box list
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Brushes",
|
||||
items=self.users_brushes,
|
||||
icon="BRUSH_DATA"
|
||||
)
|
||||
|
||||
# node groups box list
|
||||
ui_layouts.box_list(
|
||||
layout=layout,
|
||||
title="Node Groups",
|
||||
items=self.users_node_groups,
|
||||
icon="NODETREE"
|
||||
)
|
||||
|
||||
# objects box list
|
||||
ui_layouts.box_list_diverse(
|
||||
layout=layout,
|
||||
|
||||
@@ -57,6 +57,27 @@ class ATOMIC_PT_main_panel(bpy.types.Panel):
|
||||
atom.worlds
|
||||
]
|
||||
|
||||
# Progress display section (only visible when operation is running)
|
||||
if atom.is_operation_running:
|
||||
box = layout.box()
|
||||
col = box.column(align=True)
|
||||
|
||||
# Progress bar with percentage (Blender shows percentage in the bar with PERCENTAGE subtype)
|
||||
progress_row = col.row(align=True)
|
||||
progress_row.scale_y = 1.5
|
||||
progress_row.prop(atom, "operation_progress", text="", slider=True)
|
||||
|
||||
# Status text
|
||||
if atom.operation_status:
|
||||
col.label(text=atom.operation_status, icon='TIME')
|
||||
|
||||
# Cancel button
|
||||
row = col.row()
|
||||
row.scale_y = 1.5
|
||||
row.operator("atomic.cancel_operation", text="Cancel", icon='X')
|
||||
|
||||
layout.separator()
|
||||
|
||||
# nuke and clean buttons
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 2.0
|
||||
@@ -160,10 +181,12 @@ class ATOMIC_PT_main_panel(bpy.types.Panel):
|
||||
# right column
|
||||
col = split.column(align=True)
|
||||
|
||||
# images buttons
|
||||
splitcol = col.split(factor=0.8, align=True)
|
||||
|
||||
splitcol.prop(
|
||||
# images buttons (deep scan checkbox, images checkbox, inspect button)
|
||||
# Standard split layout for images (matches other categories)
|
||||
images_split = col.split(factor=0.8, align=True)
|
||||
|
||||
# Images checkbox (will be slightly offset due to deep scan, but inspect aligns)
|
||||
images_split.prop(
|
||||
atom,
|
||||
"images",
|
||||
text="Images",
|
||||
@@ -171,7 +194,8 @@ class ATOMIC_PT_main_panel(bpy.types.Panel):
|
||||
icon='IMAGE_DATA'
|
||||
)
|
||||
|
||||
splitcol.operator(
|
||||
# Inspect button (right, aligns with other inspect buttons)
|
||||
images_split.operator(
|
||||
"atomic.inspect_images",
|
||||
icon='VIEWZOOM',
|
||||
text=""
|
||||
@@ -267,7 +291,11 @@ class ATOMIC_PT_main_panel(bpy.types.Panel):
|
||||
icon='RESTRICT_SELECT_OFF'
|
||||
)
|
||||
|
||||
|
||||
# Cache and missing file management
|
||||
row = layout.row(align=True)
|
||||
row.operator("atomic.clear_cache", text="Clear Cache", icon="FILE_REFRESH")
|
||||
row.operator("atomic.detect_missing", text="Detect Missing", icon="LIBRARY_DATA_DIRECT")
|
||||
|
||||
reg_list = [ATOMIC_PT_main_panel]
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,41 @@ from .. import config
|
||||
from ..stats import missing
|
||||
from .utils import ui_layouts
|
||||
|
||||
# Module-level state for detect missing operator instance
|
||||
_detect_missing_operator_instance = None
|
||||
|
||||
|
||||
def _warp_cursor_to_area_center(context, prefer_area_type="VIEW_3D") -> None:
|
||||
"""Best-effort: move cursor to area center so popups appear centered.
|
||||
|
||||
Blender's popup placement is often tied to the last event mouse position.
|
||||
Warping is a hack, but it's the only reliable way to 'center' popups in some contexts.
|
||||
"""
|
||||
win = getattr(context, "window", None)
|
||||
screen = getattr(context, "screen", None)
|
||||
if win is None or screen is None:
|
||||
return
|
||||
|
||||
area = None
|
||||
for a in screen.areas:
|
||||
if a.type == prefer_area_type:
|
||||
area = a
|
||||
break
|
||||
if area is None and screen.areas:
|
||||
area = screen.areas[0]
|
||||
if area is None:
|
||||
return
|
||||
|
||||
try:
|
||||
# cursor_warp expects WINDOW-relative coordinates (0,0 at window bottom-left),
|
||||
# not OS desktop coordinates. Warping to the window center is the most
|
||||
# reliable option across layouts.
|
||||
x = int(win.width / 2)
|
||||
y = int(win.height / 2)
|
||||
win.cursor_warp(x, y)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Atomic Data Manager Detect Missing Files Popup
|
||||
class ATOMIC_OT_detect_missing(bpy.types.Operator):
|
||||
@@ -42,38 +77,6 @@ class ATOMIC_OT_detect_missing(bpy.types.Operator):
|
||||
missing_images = []
|
||||
missing_libraries = []
|
||||
|
||||
# missing file recovery option enum property
|
||||
recovery_option: bpy.props.EnumProperty(
|
||||
items=[
|
||||
(
|
||||
'IGNORE',
|
||||
'Ignore Missing Files',
|
||||
'Ignore the missing files and leave them offline'
|
||||
),
|
||||
(
|
||||
'RELOAD',
|
||||
'Reload Missing Files',
|
||||
'Reload the missing files from their existing file paths'
|
||||
),
|
||||
(
|
||||
'REMOVE',
|
||||
'Remove Missing Files',
|
||||
'Remove the missing files from the project'
|
||||
),
|
||||
(
|
||||
'SEARCH',
|
||||
'Search for Missing Files (under development)',
|
||||
'Search for the missing files in a directory'
|
||||
),
|
||||
(
|
||||
'REPLACE',
|
||||
'Specify Replacement Files (under development)',
|
||||
'Replace missing files with new files'
|
||||
),
|
||||
],
|
||||
default='IGNORE'
|
||||
)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
@@ -109,12 +112,20 @@ class ATOMIC_OT_detect_missing(bpy.types.Operator):
|
||||
|
||||
row = layout.separator() # extra space
|
||||
|
||||
# recovery option selection
|
||||
# recovery option buttons
|
||||
row = layout.row()
|
||||
row.label(text="What would you like to do?")
|
||||
|
||||
row = layout.row()
|
||||
row.prop(self, 'recovery_option', text="")
|
||||
row.scale_y = 1.5
|
||||
op_reload = row.operator("atomic.reload_missing", text="Reload", icon="FILE_REFRESH")
|
||||
op_remove = row.operator("atomic.remove_missing", text="Remove", icon="TRASH")
|
||||
op_search = row.operator("atomic.search_missing", text="Search", icon="VIEWZOOM")
|
||||
op_replace = row.operator("atomic.replace_missing", text="Replace", icon="FILEBROWSER")
|
||||
|
||||
# Refresh button
|
||||
row = layout.row()
|
||||
refresh_op = row.operator("atomic.detect_missing_refresh", text="Refresh", icon="FILE_REFRESH")
|
||||
|
||||
# missing files interface if no missing files are found
|
||||
else:
|
||||
@@ -129,54 +140,89 @@ class ATOMIC_OT_detect_missing(bpy.types.Operator):
|
||||
row = layout.separator() # extra space
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
# ignore missing files will take no action
|
||||
|
||||
# reload missing files
|
||||
if self.recovery_option == 'RELOAD':
|
||||
bpy.ops.atomic.reload_missing('INVOKE_DEFAULT')
|
||||
|
||||
# remove missing files
|
||||
elif self.recovery_option == 'REMOVE':
|
||||
bpy.ops.atomic.remove_missing('INVOKE_DEFAULT')
|
||||
|
||||
# search for missing files
|
||||
elif self.recovery_option == 'SEARCH':
|
||||
bpy.ops.atomic.search_missing('INVOKE_DEFAULT')
|
||||
|
||||
# replace missing files
|
||||
elif self.recovery_option == 'REPLACE':
|
||||
bpy.ops.atomic.replace_missing('INVOKE_DEFAULT')
|
||||
|
||||
# Buttons now directly invoke operators, so execute just closes the dialog
|
||||
# IGNORE is the default behavior (no action taken)
|
||||
return {'FINISHED'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
|
||||
# update missing file lists
|
||||
global _detect_missing_operator_instance
|
||||
|
||||
# Store operator instance for refresh functionality
|
||||
_detect_missing_operator_instance = self
|
||||
|
||||
# Always refresh missing file lists when invoked
|
||||
self.missing_images = missing.images()
|
||||
self.missing_libraries = missing.libraries()
|
||||
|
||||
wm = context.window_manager
|
||||
|
||||
# Force popup placement to screen center (see BlenderArtists reference).
|
||||
_warp_cursor_to_area_center(context)
|
||||
|
||||
# invoke large dialog if there are missing files
|
||||
if self.missing_images or self.missing_libraries:
|
||||
return wm.invoke_props_dialog(self, width=500)
|
||||
|
||||
# invoke small dialog if there are no missing files
|
||||
else:
|
||||
return wm.invoke_popup(self, width=300)
|
||||
return wm.invoke_props_dialog(self, width=300)
|
||||
|
||||
|
||||
@persistent
|
||||
def autodetect_missing_files(dummy=None):
|
||||
# invokes the detect missing popup when missing files are detected upon
|
||||
# loading a new Blender project
|
||||
# Use a timer to defer the operator call since load_post handlers
|
||||
# cannot directly invoke operators that modify data
|
||||
if config.enable_missing_file_warning and \
|
||||
(missing.images() or missing.libraries()):
|
||||
def invoke_detect_missing():
|
||||
try:
|
||||
bpy.ops.atomic.detect_missing('INVOKE_DEFAULT')
|
||||
except RuntimeError:
|
||||
# If still in invalid context, ignore (will be handled on next user action)
|
||||
pass
|
||||
return None # Run once
|
||||
|
||||
bpy.app.timers.register(invoke_detect_missing, first_interval=0.1)
|
||||
|
||||
|
||||
# Refresh operator for missing file detection
|
||||
class ATOMIC_OT_detect_missing_refresh(bpy.types.Operator):
|
||||
"""Refresh missing file detection"""
|
||||
bl_idname = "atomic.detect_missing_refresh"
|
||||
bl_label = "Refresh Missing Files"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
global _detect_missing_operator_instance
|
||||
|
||||
# Update the stored operator instance if it exists and is valid
|
||||
if _detect_missing_operator_instance is not None:
|
||||
try:
|
||||
# Check if operator instance is still valid
|
||||
_ = _detect_missing_operator_instance.bl_idname
|
||||
|
||||
# Update the missing file lists
|
||||
_detect_missing_operator_instance.missing_images = missing.images()
|
||||
_detect_missing_operator_instance.missing_libraries = missing.libraries()
|
||||
|
||||
# Redraw all areas to refresh the dialog
|
||||
for area in context.screen.areas:
|
||||
area.tag_redraw()
|
||||
|
||||
self.report({'INFO'}, "Missing files list refreshed")
|
||||
return {'FINISHED'}
|
||||
except (ReferenceError, AttributeError, TypeError):
|
||||
# Operator instance invalidated, clear it
|
||||
_detect_missing_operator_instance = None
|
||||
|
||||
# If no valid instance, invoke a new dialog
|
||||
bpy.ops.atomic.detect_missing('INVOKE_DEFAULT')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
reg_list = [ATOMIC_OT_detect_missing]
|
||||
reg_list = [ATOMIC_OT_detect_missing, ATOMIC_OT_detect_missing_refresh]
|
||||
|
||||
|
||||
def register():
|
||||
|
||||
@@ -24,11 +24,41 @@ some functions for syncing the preference properties with external factors.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import os
|
||||
from bpy.utils import register_class
|
||||
from ..utils import compat
|
||||
from .. import config
|
||||
import sys
|
||||
# updater removed in Blender 4.5 extension format
|
||||
|
||||
# Get the root module name dynamically
|
||||
def _get_addon_module_name():
|
||||
"""Get the root addon module name for bl_idname."""
|
||||
# In Blender 5.0 extensions loaded via VSCode, the module name is the full path
|
||||
# e.g., "bl_ext.vscode_development.atomic_data_manager"
|
||||
# We need to get it from the parent package (atomic_data_manager)
|
||||
try:
|
||||
# Get parent package name from __package__ (remove .ui suffix)
|
||||
if __package__:
|
||||
parent_pkg = __package__.rsplit('.', 1)[0] if '.' in __package__ else __package__
|
||||
# Get the actual module from sys.modules to get its __name__
|
||||
parent_module = sys.modules.get(parent_pkg)
|
||||
if parent_module and hasattr(parent_module, '__name__'):
|
||||
module_name = parent_module.__name__
|
||||
config.debug_print(f"[Atomic Debug] Using parent module __name__ as bl_idname: {module_name}")
|
||||
return module_name
|
||||
else:
|
||||
# Use the package name directly
|
||||
config.debug_print(f"[Atomic Debug] Using parent package name as bl_idname: {parent_pkg}")
|
||||
return parent_pkg
|
||||
except Exception as e:
|
||||
config.debug_print(f"[Atomic Debug] Could not get parent module name: {e}")
|
||||
|
||||
# Last fallback
|
||||
module_name = "atomic_data_manager"
|
||||
config.debug_print(f"[Atomic Debug] Using fallback bl_idname: {module_name}")
|
||||
return module_name
|
||||
|
||||
|
||||
def _get_addon_prefs():
|
||||
# robustly find our AddonPreferences instance regardless of module name
|
||||
@@ -106,6 +136,9 @@ def copy_prefs_to_config(self, context):
|
||||
config.include_fake_users = \
|
||||
atomic_preferences.include_fake_users
|
||||
|
||||
config.enable_debug_prints = \
|
||||
atomic_preferences.enable_debug_prints
|
||||
|
||||
# hidden atomic preferences
|
||||
config.pie_menu_type = \
|
||||
atomic_preferences.pie_menu_type
|
||||
@@ -192,7 +225,9 @@ def remove_pie_menu_hotkeys():
|
||||
|
||||
# Atomic Data Manager Preference Panel UI
|
||||
class ATOMIC_PT_preferences_panel(bpy.types.AddonPreferences):
|
||||
bl_idname = "atomic_data_manager"
|
||||
# bl_idname must match the add-on's module name exactly
|
||||
# Get it dynamically to ensure it matches what Blender registered
|
||||
bl_idname = _get_addon_module_name()
|
||||
|
||||
# visible atomic preferences
|
||||
enable_missing_file_warning: bpy.props.BoolProperty(
|
||||
@@ -214,6 +249,11 @@ class ATOMIC_PT_preferences_panel(bpy.types.AddonPreferences):
|
||||
update=update_pie_menu_hotkeys
|
||||
)
|
||||
|
||||
enable_debug_prints: bpy.props.BoolProperty(
|
||||
description="Enable debug print statements in the console",
|
||||
default=False
|
||||
)
|
||||
|
||||
# hidden atomic preferences
|
||||
pie_menu_type: bpy.props.StringProperty(
|
||||
default="D"
|
||||
@@ -243,6 +283,9 @@ class ATOMIC_PT_preferences_panel(bpy.types.AddonPreferences):
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
# Debug: verify draw is being called
|
||||
config.debug_print("[Atomic Debug] Preferences draw() method called")
|
||||
|
||||
split = layout.split()
|
||||
|
||||
@@ -266,6 +309,13 @@ class ATOMIC_PT_preferences_panel(bpy.types.AddonPreferences):
|
||||
text="Include Fake Users"
|
||||
)
|
||||
|
||||
# enable debug prints toggle
|
||||
col.prop(
|
||||
self,
|
||||
"enable_debug_prints",
|
||||
text="Enable Debug Prints"
|
||||
)
|
||||
|
||||
# pie menu settings
|
||||
pie_split = col.split(factor=0.55) # nice
|
||||
|
||||
@@ -317,7 +367,13 @@ keymaps = []
|
||||
|
||||
def register():
|
||||
for cls in reg_list:
|
||||
register_class(cls)
|
||||
try:
|
||||
register_class(cls)
|
||||
config.debug_print(f"[Atomic Debug] Registered preferences class: {cls.__name__} with bl_idname: {cls.bl_idname}")
|
||||
except Exception as e:
|
||||
print(f"[Atomic Error] Failed to register preferences class {cls.__name__}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# make sure global preferences are updated on registration
|
||||
copy_prefs_to_config(None, None)
|
||||
|
||||
@@ -362,7 +362,6 @@ class ATOMIC_PT_stats_panel(bpy.types.Panel):
|
||||
text="Unnamed: {0}".format(count.worlds_unnamed())
|
||||
)
|
||||
|
||||
|
||||
reg_list = [ATOMIC_PT_stats_panel]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user