Super cool idea for a UX Improvement
I just implemented and it really makes the user experience more like Adobe Software and look a little bit more polished…. But I figured out a way to draw centered message boxes and the code's already done and well tested so you could just implement… Anyways take care and have an awesome Sunday and if you're interested I also really fine tuned a restart blender op so if you wanna add that it closes you know all those windows that stay open you know like a file browser where when you save blender and reopen it ….how it's like named just blender and it's not really functional It closes those I made it optional...and it works really awesome…… Then all you do… As it's simple to implement.. is every place The end user would have to restart...you just make a function for it and put it right there and it just works perfectly every time
class FAST_OT_draw_centered_info(bpy.types.Operator):
bl_idname = "fast.draw_centered_info"
bl_label = "FAST Information:"
bl_options = {"REGISTER"} # Explicitly define bl_options
string: bpy.props.StringProperty(default="")
wrap: bpy.props.BoolProperty(default=False)
width: bpy.props.IntProperty(default=400)
def invoke(self, context, event):
manager = bpy.context.preferences.addons[__name__].preferences.Prop
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
# Calculate the center of the region
center_x = region.x + (region.width // 2)
center_y = region.y + (region.height // 2)
# Adjust starting X position for the message box to be centered
# The adjustment value can be tweaked for better positioning
adjustment_value = self.width / 2 - 155
# Ensure the adjusted position is a rounded integer
message_box_start_x = round(center_x - adjustment_value)
# Apply offsets to fine-tune the position
final_x = message_box_start_x + manager.offset_x
final_y = center_y + manager.offset_y
# Warp the cursor to the adjusted position
context.window.cursor_warp(final_x, final_y)
# The message box will now appear where the cursor was warped to
return context.window_manager.invoke_props_dialog(self, width=self.width)
# If we don't find a VIEW_3D area, report an error
self.report({'ERROR'}, "No 3D Viewport found")
return {'CANCELLED'}
def draw(self, context):
layout = self.layout
if "\n" in self.string: # Check for multi-line string
for line in self.string.split("\n"):
layout.label(text=line)
else:
if self.wrap:
wrap_text(layout, string=self.string, text_length=30, center=False)
else:
layout.label(text=self.string)
def execute(self, context):
return {'FINISHED'}
def draw_centered_info(string, title=None, width=315, wrap=False):
if title:
FAST_OT_draw_centered_info.bl_label = title
bpy.ops.fast.draw_centered_info('INVOKE_DEFAULT', string=string, width=width, wrap=wrap)
# Same idea but if somebody opens up a file browser then it looks nice
class FAST_OT_draw_centered_operator(bpy.types.Operator):
bl_idname = "fast.draw_centered_operator"
bl_label = "Draw Centered Window"
bl_options = {"REGISTER"}
operator_idname: bpy.props.StringProperty()
operator_properties: bpy.props.StringProperty(default="{}")
offset_x: bpy.props.IntProperty(default=50)
offset_y: bpy.props.IntProperty(default=145)
def invoke(self, context, event):
# The context should already be correctly set when this operator is called
area = context.area
screen_width = area.width
screen_height = area.height
center_x = int(screen_width / 2) + self.offset_x
center_y = int(screen_height / 2) + self.offset_y
# Warp the cursor to the center of the area
context.window.cursor_warp(center_x, center_y)
# Access the operator using the idname
op = bpy.ops
try:
for attr in self.operator_idname.split('.'):
op = getattr(op, attr)
except AttributeError as e:
self.report({'ERROR'}, f"Failed to access operator {self.operator_idname}: {e}")
return {'CANCELLED'}
# Convert the operator_properties string to a dictionary
try:
properties = eval(self.operator_properties)
except SyntaxError as e:
self.report({'ERROR'}, f"Syntax error in operator properties: {e}")
return {'CANCELLED'}
# Check if properties is a dictionary
if not isinstance(properties, dict):
self.report({'ERROR'}, "Operator properties must be a dictionary.")
return {'CANCELLED'}
# Call the operator with the unpacked properties
op('INVOKE_DEFAULT', **properties)
return {'FINISHED'}
def draw_centered_operator(operator_idname, offset_x=50, offset_y=145, **operator_properties):
# Construct the override dictionary
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'VIEW_3D':
# Set the context to the 3D view
override = {'window': window, 'screen': screen, 'area': area}
properties_as_str = str(operator_properties) # Ensure it's a string representation
bpy.ops.fast.draw_centered_operator(override, 'INVOKE_DEFAULT', operator_idname=operator_idname,
operator_properties=properties_as_str,
offset_x=offset_x, offset_y=offset_y)
return
# And here is both my restart blender operators......
def restart_blender():
blender_config_path = bpy.utils.user_resource("CONFIG")
blender_script_path = bpy.utils.script_path_user()
blender_app_path = os.path.dirname(bpy.app.binary_path)
current_scene_path = bpy.data.filepath
batch_file_content = f"""@echo off
chcp 65001
set BLENDER_USER_CONFIG={blender_config_path}
set BLENDER_USER_SCRIPTS={blender_script_path}
set CYCLES_OPENCL_TEST=NONE
cd /d "{blender_app_path}"
start blender.exe "{current_scene_path}"
"""
temp_directory = tempfile.gettempdir()
batch_file_path = os.path.join(temp_directory, "temp_blender_restart.bat")
with open(batch_file_path, "w") as batch_file:
batch_file.write(batch_file_content)
subprocess.Popen(batch_file_path, shell=True)
time.sleep(0.1)
os.remove(batch_file_path)
bpy.ops.wm.quit_blender()
# You just save them to a temporary file and then next time blender is started up if those values aren't right set them back
class FAST_OT_confirm_and_restart_blender(bpy.types.Operator):
bl_idname = "fast.confirm_and_restart_blender"
bl_label = "Let's Restart Blender!"
bl_options = {"REGISTER"}
string: bpy.props.StringProperty(default="")
wrap: bpy.props.BoolProperty(default=False)
width: bpy.props.IntProperty(default=400)
def invoke(self, context, event):
manager = bpy.context.preferences.addons[__name__].preferences.Prop
for area in context.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
# Calculate the center of the region
center_x = region.x + (region.width // 2)
center_y = region.y + (region.height // 2)
# Adjust starting X position for the message box to be centered
# The adjustment value can be tweaked for better positioning
adjustment_value = self.width / 2 - 155
# Ensure the adjusted position is a rounded integer
message_box_start_x = round(center_x - adjustment_value)
# Apply offsets to fine-tune the position
final_x = message_box_start_x + manager.offset_x
final_y = center_y + manager.offset_y
# Warp the cursor to the adjusted position
context.window.cursor_warp(final_x, final_y)
# The message box will now appear where the cursor was warped to
return context.window_manager.invoke_props_dialog(self, width=self.width)
# If we don't find a VIEW_3D area, report an error
self.report({'ERROR'}, "No 3D Viewport found")
return {'CANCELLED'}
def draw(self, context):
layout = self.layout
if "\n" in self.string:
for line in self.string.split("\n"):
row = layout.row(align=True)
row.label(text=" " * 2 + line + " " * 2) # Adding spaces for padding
else:
if self.wrap:
wrap_text(layout, string=self.string, text_length=30, center=False)
else:
row = layout.row(align=True)
row.label(text=" " * 2 + self.string + " " * 2) # Adding spaces for padding
def execute(self, context):
restart_blender()
return {'FINISHED'}
def confirm_and_restart_blender(string, title=None, width=315, wrap=False):
if title:
FAST_OT_confirm_and_restart_blender.bl_label = title
# Create the override dictionary
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'VIEW_3D':
# This sets the context to the 3D view
override = {'window': window, 'screen': screen, 'area': area}
bpy.ops.fast.confirm_and_restart_blender(override, 'INVOKE_DEFAULT', string=string, width=width, wrap=wrap)
return
class FAST_OT_save_restart_blender(bpy.types.Operator):
"""Saves file and restarts Blender with specified functionality."""
bl_idname = "fast.save_restart_blender"
bl_label = "SAVE & RESTART BLENDER"
bl_options = {"REGISTER"}
filepath: bpy.props.StringProperty()
new_filepath: bpy.props.StringProperty()
area_types_to_close = {'PREFERENCES', 'FILE_BROWSER', 'IMAGE_EDITOR'}
def calculate_file_paths(self):
self.filepath = bpy.data.filepath
if not self.filepath:
manager = bpy.context.preferences.addons[__name__].preferences.Prop
backup_directory = manager.file_backup_directory
if not os.path.exists(backup_directory):
os.makedirs(backup_directory)
self.filepath = os.path.join(backup_directory, "saved_after_enable.blend")
directory, full_filename = os.path.split(self.filepath)
base_name, ext = os.path.splitext(full_filename)
# Check available disk space before saving
disk_usage = shutil.disk_usage(directory)
free_space_mb = disk_usage.free / (1024 * 1024) # Convert to MB
# Check if the original file exists before trying to get its size
if os.path.exists(self.filepath):
try:
current_file_size_mb = os.path.getsize(self.filepath) / (1024 * 1024)
# Proceed with file size dependent operations.
except FileNotFoundError as e:
self.report({'ERROR'}, "The file path does not exist: " + str(e))
return {'CANCELLED'}
except Exception as e:
print(f"An unexpected error occurred: {e}")
return {'CANCELLED'}
else:
# If the file doesn't exist, set size to zero and proceed to create new backup path
current_file_size_mb = 0
if free_space_mb < 2 * current_file_size_mb:
self.report({"INFO"}, "Insufficient disk space to save file.")
return {'CANCELLED'}
# This checks if the base name ends with an underscore followed by a digit(s)
if re.search(r"_(\d+)$", base_name):
prefix, number = re.match(r"(.*?)_(\d+)$", base_name).groups()
else:
prefix, number = base_name, 0
self.new_filepath = ""
while True:
number = int(number) + 1
new_filename = f"{prefix}_{number}{ext}"
self.new_filepath = os.path.join(directory, new_filename)
if not os.path.exists(self.new_filepath):
break
# print("Filepath:", self.filepath)
# print("New Filepath:", self.new_filepath)
return self.filepath, self.new_filepath
def save_before_restart(self):
print("🚀 ~ file: fast_global.py:872 ~ self):", self)
try:
stdout = io.StringIO()
with redirect_stdout(stdout):
bpy.ops.wm.save_userpref()
bpy.ops.wm.save_as_mainfile(filepath=self.filepath, check_existing=False)
bpy.ops.wm.save_mainfile(filepath=self.new_filepath, check_existing=False)
stdout.close()
except Exception as e:
self.report({'ERROR'}, f"Encountered an error: {e}. Operation aborted.")
print(f"Encountered an error while saving: {e}. Operation aborted.")
# Utility function to list all open areas of a specific type
def list_open_areas(self, area_type):
open_areas = []
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == area_type:
open_areas.append(area)
return open_areas
# Function to close specified areas
def close_areas(self, area_types):
manager = bpy.context.preferences.addons[__name__].preferences.Prop
for window in bpy.context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type in area_types:
# Blender 3.2 and more
if bpy.app.version >= (3, 2, 0):
with bpy.context.temp_override(window=window, area=area):
bpy.ops.wm.window_close()
# Blender < 3.2 with override context
else:
override = {'window': window, 'screen': screen, 'area': area}
bpy.ops.wm.window_close(override)
def execute(self, context):
manager = bpy.context.preferences.addons[__name__].preferences.Prop
manager.was_restarted = True
self.calculate_file_paths()
# Close the areas
if manager.restart_close_windows:
before_close = {area_type: self.list_open_areas(area_type) for area_type in self.area_types_to_close}
print("Before closing:", before_close)
self.close_areas(self.area_types_to_close)
# List areas after closing to confirm
after_close = {area_type: self.list_open_areas(area_type) for area_type in self.area_types_to_close}
print("After closing:", after_close)
# Check if all specified windows are closed
all_closed = all(len(after_close[area_type]) == 0 for area_type in self.area_types_to_close)
if all_closed:
self.report({'INFO'}, "All specified windows have been closed.")
else:
self.report({'WARNING'}, "Some windows could not be closed.")
confirm_and_restart_blender("File was Saved Twice! Click 'OK' to Restart!", title=None, width=240, wrap=False)
return {'FINISHED'}
def ask_to_restart_enable():
manager = bpy.context.preferences.addons[__name__].preferences.Prop
if not manager.enable_done:
manager.enable_done = True
bpy.ops.fast.save_restart_blender()
def ask_to_restart():
bpy.ops.fast.save_restart_blender()
Yeah I told the creator of extreme PBR about this and he was really excited that I came up with this..
Comments (6)
-
reporter -
reporter Sorry my logo getting put on there complete accident I copied and pasted this so apologize if that gets in the way
-
- changed status to open
-
repo owner TL;DR. You have your own plugin, I think. You can implement this there, the present UI works fine for me.
-
repo owner - changed status to wontfix
-
reporter sorry I just noticed that some of those items got pasted in there messy… Not a very good demonstration of the code but yeah I just wanted to make sure you had it in case you wanted to i've noticed the UI looks a lot nicer when I open you know centered preferences windows and centered file path boxes and centered message boxes but I don't think I'm gonna put centered message boxes on Transform panel but noticed it looks nicer on everything else….. Anyways it's there and well tested if you decide later… Take care and have a good Wednesday!!
- Log in to comment
class FAST_OT_draw_centered_info(bpy.types.Operator): bl_idname = "fast.draw_centered_info" bl_label = "FAST Information:" bl_options = {"REGISTER"} # Explicitly define bl_options
def draw_centered_info(string, title=None, width=315, wrap=False): if title: FAST_OT_draw_centered_info.bl_label = title
def draw_centered_operator(operator_idname, offset_x=50, offset_y=145, **operator_properties): # Construct the override dictionary for window in bpy.context.window_manager.windows: screen = window.screen for area in screen.areas: if area.type == 'VIEW_3D': # Set the context to the 3D view override = {'window': window, 'screen': screen, 'area': area} properties_as_str = str(operator_properties) # Ensure it's a string representation bpy.ops.fast.draw_centered_operator(override, 'INVOKE_DEFAULT', operator_idname=operator_idname, operator_properties=properties_as_str, offset_x=offset_x, offset_y=offset_y) return
# And here is both my restart blender operators......
def restart_blender(): blender_config_path = bpy.utils.user_resource("CONFIG") blender_script_path = bpy.utils.script_path_user() blender_app_path = os.path.dirname(bpy.app.binary_path) current_scene_path = bpy.data.filepath
chcp 65001 set BLENDER_USER_CONFIG={blender_config_path} set BLENDER_USER_SCRIPTS={blender_script_path} set CYCLES_OPENCL_TEST=NONE cd /d "{blender_app_path}" start blender.exe "{current_scene_path}" """
# You just save them to a temporary file and then next time blender is started up if those values aren't right set them back class FAST_OT_confirm_and_restart_blender(bpy.types.Operator): bl_idname = "fast.confirm_and_restart_blender" bl_label = "Let's Restart Blender!" bl_options = {"REGISTER"}
def confirm_and_restart_blender(string, title=None, width=315, wrap=False):
class FAST_OT_save_restart_blender(bpy.types.Operator): """Saves file and restarts Blender with specified functionality.""" bl_idname = "fast.save_restart_blender" bl_label = "SAVE & RESTART BLENDER" bl_options = {"REGISTER"}
def ask_to_restart_enable():
def ask_to_restart():
``` *
Joe MorrisNovember 6, 2023 12:15am
```
May not need but these are integral for me in the UX involving this you could probably come up with a blender only version of this else
if you're gonna bundle PY get window No there is a Mac and Linux also version called ('pypiwin32', 'pypiwin32'), or
('pywinctl', 'pywinctl'), I forgot but I'm kinda in a rush but any trouble installing it let me know it was kind of a difficult one (On the tutorials or the question and answer site it was the DLL 's that were important to add right) I have all the code for that too... You don't have to bundle the whole library you could just trace whatever functionality they use and grab it from their code.. GPT said it was cool when I did that with P Y audio detect silence
class EnsureConsoleOpen(bpy.types.Operator): bl_idname = "wm.ensure_console_open"
bl_label = "Ensure Blender Console Window is Open"
bl_description = "Toggle the Blender console window to ensure it's open"
def execute(self, context): try: if platform.system() == 'Windows': try: # Nested try-except for the GW operations
windows = gw.getAllWindows() except Exception as e: windows = [] # Ensure windows is an empty list so the loop doesn't run
pass
for window in windows: handle = ctypes.windll.user32.FindWindowW(None, window.title) class_name = ctypes.create_unicode_buffer(256) ctypes.windll.user32.GetClassNameW(handle, class_name, 256)
# Print the window title and class name
# print(f"Window Title: {window.title}, Class Name: {class_name.value}")
if ((class_name.value == 'ConsoleWindowClass' and 'blend' in window.title.lower()) or (class_name.value == 'GHOST_WindowClass' and 'blender' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'blender' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'Blender' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'Blender 3.6.4' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'Blender 3.6.5' in window.title)): return {"FINISHED"}
if platform.system() == 'Windows': getattr(bpy.ops.wm, "console_toggle", lambda: None)()
else: # This code is expected to work on Linux and macOS
# provided 'wmctrl' is available on the system
subprocess.run(["wmctrl", "-a", "Blender"])
except Exception: bpy.ops.mac_linux.error_notification('INVOKE_DEFAULT') return {"CANCELLED"}
return {"FINISHED"}
class FAST_OT_show_console(bpy.types.Operator): bl_idname = "fast.show_console"
bl_label = "Bring Blender Console Window to Foreground"
bl_description = "Bring the Blender console window to the foreground"
def execute(self, context): # Get the manager instance
manager = bpy.context.preferences.addons[name].preferences.Prop
try: # Ensure the console window is open
bpy.ops.wm.ensure_console_open()
try: # Nested try-except for the GW operations
windows = gw.getAllWindows() except Exception as e: windows = [] # Ensure windows is an empty list so the loop doesn't run
pass
for window in windows: handle = ctypes.windll.user32.FindWindowW(None, window.title) class_name = ctypes.create_unicode_buffer(256) ctypes.windll.user32.GetClassNameW(handle, class_name, 256)
if 'Blender' in window.title and re.search(r'([A-Z]:\[^|"<>?\n])|((\\.?\.)|([^:?"<>|\/\n]+))((\\[^:?"<>|\/\n]+)+)?(\)?\S+.blend', window.title, re.I) and class_name.value == 'GHOST_WindowClass': blender_main_window_handle = handle
for window in windows: handle = ctypes.windll.user32.FindWindowW(None, window.title) class_name = ctypes.create_unicode_buffer(256) ctypes.windll.user32.GetClassNameW(handle, class_name, 256)
# print(f"Window Title: {window.title}")
# print(f"Handle Value: {handle}")
# print(f"Class Name: {class_name.value}")
if ((class_name.value == 'ConsoleWindowClass' and 'blend' in window.title.lower()) or (class_name.value == 'GHOST_WindowClass' and 'blender' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'blender' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'Blender' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'Blender 3.6.4' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'Blender 3.6.5' in window.title)): try: # After finding the console window
manager.console_window_handle = handle
# print("🚀manager.console_window_handle:", manager.console_window_handle)
window.activate() except: pass
window.show() window.restore()
# If the preference is set to True, keep the console window always on top
if manager.always_on_top: ctypes.windll.user32.SetWindowPos(handle, -1, 0, 0, 0, 0, 3)
return {"FINISHED"}
except Exception: traceback.print_exc() self.report({"WARNING"}, "Must Install Dependencies to use FAST functionality!\n") return {"CANCELLED"}
return {"FINISHED"}
Assuming this is the correct constant for the SW_MINIMIZE command from the Windows API
SW_MINIMIZE = 6
class FAST_OT_hide_console(bpy.types.Operator): bl_idname = "fast.hide_console"
bl_label = "Hide Blender Console Window"
bl_description = "Hide the Blender console window"
def execute(self, context): # Get the manager instance
manager = bpy.context.preferences.addons[name].preferences.Prop
try: # Ensure the console window is open
bpy.ops.wm.ensure_console_open()
try: # Nested try-except for the GW operations
windows = gw.getAllWindows() except Exception as e: windows = [] # Ensure windows is an empty list so the loop doesn't run
pass
console_window_found = False
for window in windows: handle = ctypes.windll.user32.FindWindowW(None, window.title) class_name = ctypes.create_unicode_buffer(256) ctypes.windll.user32.GetClassNameW(handle, class_name, 256)
# print(f"Window Title: {window.title}")
# print(f"Handle Value: {handle}")
# print(f"Class Name: {class_name.value}")
if ((class_name.value == 'ConsoleWindowClass' and 'blend' in window.title.lower()) or (class_name.value == 'GHOST_WindowClass' and 'blender' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'blender' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'Blender' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'Blender 3.6.4' in window.title) or (class_name.value == 'CASCADIA_HOSTING_WINDOW_CLASS' and 'Blender 3.6.5' in window.title)): console_window_found = True
# Minimize the console window
ctypes.windll.user32.ShowWindow(handle, SW_MINIMIZE) break
if not console_window_found: self.report({'INFO'}, "Console window not found.") return {'CANCELLED'}
except Exception as e: traceback.print_exc() self.report({'ERROR'}, "An error occurred while trying to hide the console: {}".format(e)) return {'CANCELLED'}
return {'FINISHED'} ```