Super cool idea for a UX Improvement

Issue #1821 wontfix
Joe Morris @ FAST Animation Studio Toolz created an issue

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)

  1. Joe Morris @ FAST Animation Studio Toolz reporter
    • ```

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

    ``` *

    Joe MorrisNovember 6, 2023 12:09am
    
    
    
    I think the optimal logic is letting the end user decide message box placement overall so I added two panel properties for it and I forgot to give you my draw centered operator operator that way if anywhere in your add-on they need to open a file browser then it centered or preferences and also both my restart blender operators I changed the logic on it because using GW was not good in every situation and it uses just a default blender system again apologize if I've already told you this but I sent this to DIFFEOMORPHIC and wanted to make sure you had all the latest information just in case anyways take care and have an awesome Sunday!!.... Ohh and there's another involved panel property it's giving the option to close windows in case people don't like that...Take care!!
    
    • 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'} ```

  2. Joe Morris @ FAST Animation Studio Toolz reporter

    Sorry my logo getting put on there complete accident I copied and pasted this so apologize if that gets in the way

  3. Thomas Larsson repo owner

    TL;DR. You have your own plugin, I think. You can implement this there, the present UI works fine for me.

  4. Joe Morris @ FAST Animation Studio Toolz 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!!

  5. Log in to comment