2626 lines
		
	
	
		
			80 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			2626 lines
		
	
	
		
			80 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| # ##### BEGIN GPL LICENSE BLOCK #####
 | |
| #
 | |
| #  This program is free software; you can redistribute it and/or
 | |
| #  modify it under the terms of the GNU General Public License
 | |
| #  as published by the Free Software Foundation; either version 2
 | |
| #  of the License, or (at your option) any later version.
 | |
| #
 | |
| #  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 General Public License for more details.
 | |
| #
 | |
| #  You should have received a copy of the GNU General Public License
 | |
| #  along with this program; if not, write to the Free Software Foundation,
 | |
| #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 | |
| #
 | |
| # ##### END GPL LICENSE BLOCK #####
 | |
| 
 | |
| # <pep8 compliant>
 | |
| 
 | |
| import bpy
 | |
| from bpy.types import (
 | |
|     Menu,
 | |
|     Operator,
 | |
| )
 | |
| from bpy.props import (
 | |
|     BoolProperty,
 | |
|     CollectionProperty,
 | |
|     EnumProperty,
 | |
|     FloatProperty,
 | |
|     IntProperty,
 | |
|     StringProperty,
 | |
| )
 | |
| 
 | |
| # FIXME, we need a way to detect key repeat events.
 | |
| # unfortunately checking event previous values isn't reliable.
 | |
| use_toolbar_release_hack = True
 | |
| 
 | |
| 
 | |
| rna_path_prop = StringProperty(
 | |
|     name="Context Attributes",
 | |
|     description="RNA context string",
 | |
|     maxlen=1024,
 | |
| )
 | |
| 
 | |
| rna_reverse_prop = BoolProperty(
 | |
|     name="Reverse",
 | |
|     description="Cycle backwards",
 | |
|     default=False,
 | |
| )
 | |
| 
 | |
| rna_wrap_prop = BoolProperty(
 | |
|     name="Wrap",
 | |
|     description="Wrap back to the first/last values",
 | |
|     default=False,
 | |
| )
 | |
| 
 | |
| rna_relative_prop = BoolProperty(
 | |
|     name="Relative",
 | |
|     description="Apply relative to the current value (delta)",
 | |
|     default=False,
 | |
| )
 | |
| 
 | |
| rna_space_type_prop = EnumProperty(
 | |
|     name="Type",
 | |
|     items=tuple(
 | |
|         (e.identifier, e.name, "", e. value)
 | |
|         for e in bpy.types.Space.bl_rna.properties["type"].enum_items
 | |
|     ),
 | |
|     default='EMPTY',
 | |
| )
 | |
| 
 | |
| # Note, this can be used for more operators,
 | |
| # currently not used for all "WM_OT_context_" operators.
 | |
| rna_module_prop = StringProperty(
 | |
|     name="Module",
 | |
|     description="Optionally override the context with a module",
 | |
|     maxlen=1024,
 | |
| )
 | |
| 
 | |
| 
 | |
| def context_path_validate(context, data_path):
 | |
|     try:
 | |
|         value = eval("context.%s" % data_path) if data_path else Ellipsis
 | |
|     except AttributeError as ex:
 | |
|         if str(ex).startswith("'NoneType'"):
 | |
|             # One of the items in the rna path is None, just ignore this
 | |
|             value = Ellipsis
 | |
|         else:
 | |
|             # We have a real error in the rna path, don't ignore that
 | |
|             raise
 | |
| 
 | |
|     return value
 | |
| 
 | |
| 
 | |
| def operator_value_is_undo(value):
 | |
|     if value in {None, Ellipsis}:
 | |
|         return False
 | |
| 
 | |
|     # typical properties or objects
 | |
|     id_data = getattr(value, "id_data", Ellipsis)
 | |
| 
 | |
|     if id_data is None:
 | |
|         return False
 | |
|     elif id_data is Ellipsis:
 | |
|         # handle mathutils types
 | |
|         id_data = getattr(getattr(value, "owner", None), "id_data", None)
 | |
| 
 | |
|         if id_data is None:
 | |
|             return False
 | |
| 
 | |
|     # return True if its a non window ID type
 | |
|     return (isinstance(id_data, bpy.types.ID) and
 | |
|             (not isinstance(id_data, (bpy.types.WindowManager,
 | |
|                                       bpy.types.Screen,
 | |
|                                       bpy.types.Brush,
 | |
|                                       ))))
 | |
| 
 | |
| 
 | |
| def operator_path_is_undo(context, data_path):
 | |
|     # note that if we have data paths that use strings this could fail
 | |
|     # luckily we don't do this!
 | |
|     #
 | |
|     # When we can't find the data owner assume no undo is needed.
 | |
|     data_path_head = data_path.rpartition(".")[0]
 | |
| 
 | |
|     if not data_path_head:
 | |
|         return False
 | |
| 
 | |
|     value = context_path_validate(context, data_path_head)
 | |
| 
 | |
|     return operator_value_is_undo(value)
 | |
| 
 | |
| 
 | |
| def operator_path_undo_return(context, data_path):
 | |
|     return {'FINISHED'} if operator_path_is_undo(context, data_path) else {'CANCELLED'}
 | |
| 
 | |
| 
 | |
| def operator_value_undo_return(value):
 | |
|     return {'FINISHED'} if operator_value_is_undo(value) else {'CANCELLED'}
 | |
| 
 | |
| 
 | |
| def execute_context_assign(self, context):
 | |
|     data_path = self.data_path
 | |
|     if context_path_validate(context, data_path) is Ellipsis:
 | |
|         return {'PASS_THROUGH'}
 | |
| 
 | |
|     if getattr(self, "relative", False):
 | |
|         exec("context.%s += self.value" % data_path)
 | |
|     else:
 | |
|         exec("context.%s = self.value" % data_path)
 | |
| 
 | |
|     return operator_path_undo_return(context, data_path)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_set_boolean(Operator):
 | |
|     """Set a context value"""
 | |
|     bl_idname = "wm.context_set_boolean"
 | |
|     bl_label = "Context Set Boolean"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value: BoolProperty(
 | |
|         name="Value",
 | |
|         description="Assignment value",
 | |
|         default=True,
 | |
|     )
 | |
| 
 | |
|     execute = execute_context_assign
 | |
| 
 | |
| 
 | |
| class WM_OT_context_set_int(Operator):  # same as enum
 | |
|     """Set a context value"""
 | |
|     bl_idname = "wm.context_set_int"
 | |
|     bl_label = "Context Set"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value: IntProperty(
 | |
|         name="Value",
 | |
|         description="Assign value",
 | |
|         default=0,
 | |
|     )
 | |
|     relative: rna_relative_prop
 | |
| 
 | |
|     execute = execute_context_assign
 | |
| 
 | |
| 
 | |
| class WM_OT_context_scale_float(Operator):
 | |
|     """Scale a float context value"""
 | |
|     bl_idname = "wm.context_scale_float"
 | |
|     bl_label = "Context Scale Float"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value: FloatProperty(
 | |
|         name="Value",
 | |
|         description="Assign value",
 | |
|         default=1.0,
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path = self.data_path
 | |
|         if context_path_validate(context, data_path) is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         value = self.value
 | |
| 
 | |
|         if value == 1.0:  # nothing to do
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         exec("context.%s *= value" % data_path)
 | |
| 
 | |
|         return operator_path_undo_return(context, data_path)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_scale_int(Operator):
 | |
|     """Scale an int context value"""
 | |
|     bl_idname = "wm.context_scale_int"
 | |
|     bl_label = "Context Scale Int"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value: FloatProperty(
 | |
|         name="Value",
 | |
|         description="Assign value",
 | |
|         default=1.0,
 | |
|     )
 | |
|     always_step: BoolProperty(
 | |
|         name="Always Step",
 | |
|         description="Always adjust the value by a minimum of 1 when 'value' is not 1.0",
 | |
|         default=True,
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path = self.data_path
 | |
|         if context_path_validate(context, data_path) is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         value = self.value
 | |
| 
 | |
|         if value == 1.0:  # nothing to do
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         if getattr(self, "always_step", False):
 | |
|             if value > 1.0:
 | |
|                 add = "1"
 | |
|                 func = "max"
 | |
|             else:
 | |
|                 add = "-1"
 | |
|                 func = "min"
 | |
|             exec("context.%s = %s(round(context.%s * value), context.%s + %s)" %
 | |
|                  (data_path, func, data_path, data_path, add))
 | |
|         else:
 | |
|             exec("context.%s *= value" % data_path)
 | |
| 
 | |
|         return operator_path_undo_return(context, data_path)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_set_float(Operator):  # same as enum
 | |
|     """Set a context value"""
 | |
|     bl_idname = "wm.context_set_float"
 | |
|     bl_label = "Context Set Float"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value: FloatProperty(
 | |
|         name="Value",
 | |
|         description="Assignment value",
 | |
|         default=0.0,
 | |
|     )
 | |
|     relative: rna_relative_prop
 | |
| 
 | |
|     execute = execute_context_assign
 | |
| 
 | |
| 
 | |
| class WM_OT_context_set_string(Operator):  # same as enum
 | |
|     """Set a context value"""
 | |
|     bl_idname = "wm.context_set_string"
 | |
|     bl_label = "Context Set String"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value: StringProperty(
 | |
|         name="Value",
 | |
|         description="Assign value",
 | |
|         maxlen=1024,
 | |
|     )
 | |
| 
 | |
|     execute = execute_context_assign
 | |
| 
 | |
| 
 | |
| class WM_OT_context_set_enum(Operator):
 | |
|     """Set a context value"""
 | |
|     bl_idname = "wm.context_set_enum"
 | |
|     bl_label = "Context Set Enum"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value: StringProperty(
 | |
|         name="Value",
 | |
|         description="Assignment value (as a string)",
 | |
|         maxlen=1024,
 | |
|     )
 | |
| 
 | |
|     execute = execute_context_assign
 | |
| 
 | |
| 
 | |
| class WM_OT_context_set_value(Operator):
 | |
|     """Set a context value"""
 | |
|     bl_idname = "wm.context_set_value"
 | |
|     bl_label = "Context Set Value"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value: StringProperty(
 | |
|         name="Value",
 | |
|         description="Assignment value (as a string)",
 | |
|         maxlen=1024,
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path = self.data_path
 | |
|         if context_path_validate(context, data_path) is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
|         exec("context.%s = %s" % (data_path, self.value))
 | |
|         return operator_path_undo_return(context, data_path)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_toggle(Operator):
 | |
|     """Toggle a context value"""
 | |
|     bl_idname = "wm.context_toggle"
 | |
|     bl_label = "Context Toggle"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     module: rna_module_prop
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path = self.data_path
 | |
| 
 | |
|         module = self.module
 | |
|         if not module:
 | |
|             base = context
 | |
|         else:
 | |
|             from importlib import import_module
 | |
|             base = import_module(self.module)
 | |
| 
 | |
|         if context_path_validate(base, data_path) is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         exec("base.%s = not (base.%s)" % (data_path, data_path))
 | |
| 
 | |
|         return operator_path_undo_return(base, data_path)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_toggle_enum(Operator):
 | |
|     """Toggle a context value"""
 | |
|     bl_idname = "wm.context_toggle_enum"
 | |
|     bl_label = "Context Toggle Values"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value_1: StringProperty(
 | |
|         name="Value",
 | |
|         description="Toggle enum",
 | |
|         maxlen=1024,
 | |
|     )
 | |
|     value_2: StringProperty(
 | |
|         name="Value",
 | |
|         description="Toggle enum",
 | |
|         maxlen=1024,
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path = self.data_path
 | |
| 
 | |
|         if context_path_validate(context, data_path) is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         # failing silently is not ideal, but we don't want errors for shortcut
 | |
|         # keys that some values that are only available in a particular context
 | |
|         try:
 | |
|             exec("context.%s = ('%s', '%s')[context.%s != '%s']" %
 | |
|                  (data_path, self.value_1,
 | |
|                   self.value_2, data_path,
 | |
|                   self.value_2,
 | |
|                   ))
 | |
|         except:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         return operator_path_undo_return(context, data_path)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_cycle_int(Operator):
 | |
|     """Set a context value (useful for cycling active material, """ \
 | |
|         """vertex keys, groups, etc.)"""
 | |
|     bl_idname = "wm.context_cycle_int"
 | |
|     bl_label = "Context Int Cycle"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     reverse: rna_reverse_prop
 | |
|     wrap: rna_wrap_prop
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path = self.data_path
 | |
|         value = context_path_validate(context, data_path)
 | |
|         if value is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         if self.reverse:
 | |
|             value -= 1
 | |
|         else:
 | |
|             value += 1
 | |
| 
 | |
|         exec("context.%s = value" % data_path)
 | |
| 
 | |
|         if self.wrap:
 | |
|             if value != eval("context.%s" % data_path):
 | |
|                 # relies on rna clamping integers out of the range
 | |
|                 if self.reverse:
 | |
|                     value = (1 << 31) - 1
 | |
|                 else:
 | |
|                     value = -1 << 31
 | |
| 
 | |
|                 exec("context.%s = value" % data_path)
 | |
| 
 | |
|         return operator_path_undo_return(context, data_path)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_cycle_enum(Operator):
 | |
|     """Toggle a context value"""
 | |
|     bl_idname = "wm.context_cycle_enum"
 | |
|     bl_label = "Context Enum Cycle"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     reverse: rna_reverse_prop
 | |
|     wrap: rna_wrap_prop
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path = self.data_path
 | |
|         value = context_path_validate(context, data_path)
 | |
|         if value is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         orig_value = value
 | |
| 
 | |
|         # Have to get rna enum values
 | |
|         rna_struct_str, rna_prop_str = data_path.rsplit('.', 1)
 | |
|         i = rna_prop_str.find('[')
 | |
| 
 | |
|         # just in case we get "context.foo.bar[0]"
 | |
|         if i != -1:
 | |
|             rna_prop_str = rna_prop_str[0:i]
 | |
| 
 | |
|         rna_struct = eval("context.%s.rna_type" % rna_struct_str)
 | |
| 
 | |
|         rna_prop = rna_struct.properties[rna_prop_str]
 | |
| 
 | |
|         if type(rna_prop) != bpy.types.EnumProperty:
 | |
|             raise Exception("expected an enum property")
 | |
| 
 | |
|         enums = rna_struct.properties[rna_prop_str].enum_items.keys()
 | |
|         orig_index = enums.index(orig_value)
 | |
| 
 | |
|         # Have the info we need, advance to the next item.
 | |
|         #
 | |
|         # When wrap's disabled we may set the value to its self,
 | |
|         # this is done to ensure update callbacks run.
 | |
|         if self.reverse:
 | |
|             if orig_index == 0:
 | |
|                 advance_enum = enums[-1] if self.wrap else enums[0]
 | |
|             else:
 | |
|                 advance_enum = enums[orig_index - 1]
 | |
|         else:
 | |
|             if orig_index == len(enums) - 1:
 | |
|                 advance_enum = enums[0] if self.wrap else enums[-1]
 | |
|             else:
 | |
|                 advance_enum = enums[orig_index + 1]
 | |
| 
 | |
|         # set the new value
 | |
|         exec("context.%s = advance_enum" % data_path)
 | |
|         return operator_path_undo_return(context, data_path)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_cycle_array(Operator):
 | |
|     """Set a context array value """ \
 | |
|         """(useful for cycling the active mesh edit mode)"""
 | |
|     bl_idname = "wm.context_cycle_array"
 | |
|     bl_label = "Context Array Cycle"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     reverse: rna_reverse_prop
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path = self.data_path
 | |
|         value = context_path_validate(context, data_path)
 | |
|         if value is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         def cycle(array):
 | |
|             if self.reverse:
 | |
|                 array.insert(0, array.pop())
 | |
|             else:
 | |
|                 array.append(array.pop(0))
 | |
|             return array
 | |
| 
 | |
|         exec("context.%s = cycle(context.%s[:])" % (data_path, data_path))
 | |
| 
 | |
|         return operator_path_undo_return(context, data_path)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_menu_enum(Operator):
 | |
|     bl_idname = "wm.context_menu_enum"
 | |
|     bl_label = "Context Enum Menu"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path = self.data_path
 | |
|         value = context_path_validate(context, data_path)
 | |
| 
 | |
|         if value is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         base_path, prop_string = data_path.rsplit(".", 1)
 | |
|         value_base = context_path_validate(context, base_path)
 | |
|         prop = value_base.bl_rna.properties[prop_string]
 | |
| 
 | |
|         def draw_cb(self, context):
 | |
|             layout = self.layout
 | |
|             layout.prop(value_base, prop_string, expand=True)
 | |
| 
 | |
|         context.window_manager.popup_menu(draw_func=draw_cb, title=prop.name, icon=prop.icon)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_context_pie_enum(Operator):
 | |
|     bl_idname = "wm.context_pie_enum"
 | |
|     bl_label = "Context Enum Pie"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
|         wm = context.window_manager
 | |
|         data_path = self.data_path
 | |
|         value = context_path_validate(context, data_path)
 | |
| 
 | |
|         if value is Ellipsis:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         base_path, prop_string = data_path.rsplit(".", 1)
 | |
|         value_base = context_path_validate(context, base_path)
 | |
|         prop = value_base.bl_rna.properties[prop_string]
 | |
| 
 | |
|         def draw_cb(self, context):
 | |
|             layout = self.layout
 | |
|             layout.prop(value_base, prop_string, expand=True)
 | |
| 
 | |
|         wm.popup_menu_pie(draw_func=draw_cb, title=prop.name, icon=prop.icon, event=event)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_operator_pie_enum(Operator):
 | |
|     bl_idname = "wm.operator_pie_enum"
 | |
|     bl_label = "Operator Enum Pie"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: StringProperty(
 | |
|         name="Operator",
 | |
|         description="Operator name (in python as string)",
 | |
|         maxlen=1024,
 | |
|     )
 | |
|     prop_string: StringProperty(
 | |
|         name="Property",
 | |
|         description="Property name (as a string)",
 | |
|         maxlen=1024,
 | |
|     )
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
|         wm = context.window_manager
 | |
| 
 | |
|         data_path = self.data_path
 | |
|         prop_string = self.prop_string
 | |
| 
 | |
|         # same as eval("bpy.ops." + data_path)
 | |
|         op_mod_str, ob_id_str = data_path.split(".", 1)
 | |
|         op = getattr(getattr(bpy.ops, op_mod_str), ob_id_str)
 | |
|         del op_mod_str, ob_id_str
 | |
| 
 | |
|         try:
 | |
|             op_rna = op.get_rna_type()
 | |
|         except KeyError:
 | |
|             self.report({'ERROR'}, "Operator not found: bpy.ops.%s" % data_path)
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         def draw_cb(self, context):
 | |
|             layout = self.layout
 | |
|             pie = layout.menu_pie()
 | |
|             pie.operator_enum(data_path, prop_string)
 | |
| 
 | |
|         wm.popup_menu_pie(draw_func=draw_cb, title=op_rna.name, event=event)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_context_set_id(Operator):
 | |
|     """Set a context value to an ID data-block"""
 | |
|     bl_idname = "wm.context_set_id"
 | |
|     bl_label = "Set Library ID"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path_prop
 | |
|     value: StringProperty(
 | |
|         name="Value",
 | |
|         description="Assign value",
 | |
|         maxlen=1024,
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         value = self.value
 | |
|         data_path = self.data_path
 | |
| 
 | |
|         # match the pointer type from the target property to bpy.data.*
 | |
|         # so we lookup the correct list.
 | |
|         data_path_base, data_path_prop = data_path.rsplit(".", 1)
 | |
|         data_prop_rna = eval("context.%s" % data_path_base).rna_type.properties[data_path_prop]
 | |
|         data_prop_rna_type = data_prop_rna.fixed_type
 | |
| 
 | |
|         id_iter = None
 | |
| 
 | |
|         for prop in bpy.data.rna_type.properties:
 | |
|             if prop.rna_type.identifier == "CollectionProperty":
 | |
|                 if prop.fixed_type == data_prop_rna_type:
 | |
|                     id_iter = prop.identifier
 | |
|                     break
 | |
| 
 | |
|         if id_iter:
 | |
|             value_id = getattr(bpy.data, id_iter).get(value)
 | |
|             exec("context.%s = value_id" % data_path)
 | |
| 
 | |
|         return operator_path_undo_return(context, data_path)
 | |
| 
 | |
| 
 | |
| doc_id = StringProperty(
 | |
|     name="Doc ID",
 | |
|     maxlen=1024,
 | |
|     options={'HIDDEN'},
 | |
| )
 | |
| 
 | |
| data_path_iter = StringProperty(
 | |
|     description="The data path relative to the context, must point to an iterable")
 | |
| 
 | |
| data_path_item = StringProperty(
 | |
|     description="The data path from each iterable to the value (int or float)")
 | |
| 
 | |
| 
 | |
| class WM_OT_context_collection_boolean_set(Operator):
 | |
|     """Set boolean values for a collection of items"""
 | |
|     bl_idname = "wm.context_collection_boolean_set"
 | |
|     bl_label = "Context Collection Boolean Set"
 | |
|     bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
 | |
| 
 | |
|     data_path_iter: data_path_iter
 | |
|     data_path_item: data_path_item
 | |
| 
 | |
|     type: EnumProperty(
 | |
|         name="Type",
 | |
|         items=(
 | |
|             ('TOGGLE', "Toggle", ""),
 | |
|             ('ENABLE', "Enable", ""),
 | |
|             ('DISABLE', "Disable", ""),
 | |
|         ),
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         data_path_iter = self.data_path_iter
 | |
|         data_path_item = self.data_path_item
 | |
| 
 | |
|         items = list(getattr(context, data_path_iter))
 | |
|         items_ok = []
 | |
|         is_set = False
 | |
|         for item in items:
 | |
|             try:
 | |
|                 value_orig = eval("item." + data_path_item)
 | |
|             except:
 | |
|                 continue
 | |
| 
 | |
|             if value_orig is True:
 | |
|                 is_set = True
 | |
|             elif value_orig is False:
 | |
|                 pass
 | |
|             else:
 | |
|                 self.report({'WARNING'}, "Non boolean value found: %s[ ].%s" %
 | |
|                             (data_path_iter, data_path_item))
 | |
|                 return {'CANCELLED'}
 | |
| 
 | |
|             items_ok.append(item)
 | |
| 
 | |
|         # avoid undo push when nothing to do
 | |
|         if not items_ok:
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         if self.type == 'ENABLE':
 | |
|             is_set = True
 | |
|         elif self.type == 'DISABLE':
 | |
|             is_set = False
 | |
|         else:
 | |
|             is_set = not is_set
 | |
| 
 | |
|         exec_str = "item.%s = %s" % (data_path_item, is_set)
 | |
|         for item in items_ok:
 | |
|             exec(exec_str)
 | |
| 
 | |
|         return operator_value_undo_return(item)
 | |
| 
 | |
| 
 | |
| class WM_OT_context_modal_mouse(Operator):
 | |
|     """Adjust arbitrary values with mouse input"""
 | |
|     bl_idname = "wm.context_modal_mouse"
 | |
|     bl_label = "Context Modal Mouse"
 | |
|     bl_options = {'GRAB_CURSOR', 'BLOCKING', 'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path_iter: data_path_iter
 | |
|     data_path_item: data_path_item
 | |
|     header_text: StringProperty(
 | |
|         name="Header Text",
 | |
|         description="Text to display in header during scale",
 | |
|     )
 | |
| 
 | |
|     input_scale: FloatProperty(
 | |
|         description="Scale the mouse movement by this value before applying the delta",
 | |
|         default=0.01,
 | |
|     )
 | |
|     invert: BoolProperty(
 | |
|         description="Invert the mouse input",
 | |
|         default=False,
 | |
|     )
 | |
|     initial_x: IntProperty(options={'HIDDEN'})
 | |
| 
 | |
|     def _values_store(self, context):
 | |
|         data_path_iter = self.data_path_iter
 | |
|         data_path_item = self.data_path_item
 | |
| 
 | |
|         self._values = values = {}
 | |
| 
 | |
|         for item in getattr(context, data_path_iter):
 | |
|             try:
 | |
|                 value_orig = eval("item." + data_path_item)
 | |
|             except:
 | |
|                 continue
 | |
| 
 | |
|             # check this can be set, maybe this is library data.
 | |
|             try:
 | |
|                 exec("item.%s = %s" % (data_path_item, value_orig))
 | |
|             except:
 | |
|                 continue
 | |
| 
 | |
|             values[item] = value_orig
 | |
| 
 | |
|     def _values_delta(self, delta):
 | |
|         delta *= self.input_scale
 | |
|         if self.invert:
 | |
|             delta = - delta
 | |
| 
 | |
|         data_path_item = self.data_path_item
 | |
|         for item, value_orig in self._values.items():
 | |
|             if type(value_orig) == int:
 | |
|                 exec("item.%s = int(%d)" % (data_path_item, round(value_orig + delta)))
 | |
|             else:
 | |
|                 exec("item.%s = %f" % (data_path_item, value_orig + delta))
 | |
| 
 | |
|     def _values_restore(self):
 | |
|         data_path_item = self.data_path_item
 | |
|         for item, value_orig in self._values.items():
 | |
|             exec("item.%s = %s" % (data_path_item, value_orig))
 | |
| 
 | |
|         self._values.clear()
 | |
| 
 | |
|     def _values_clear(self):
 | |
|         self._values.clear()
 | |
| 
 | |
|     def modal(self, context, event):
 | |
|         event_type = event.type
 | |
| 
 | |
|         if event_type == 'MOUSEMOVE':
 | |
|             delta = event.mouse_x - self.initial_x
 | |
|             self._values_delta(delta)
 | |
|             header_text = self.header_text
 | |
|             if header_text:
 | |
|                 if len(self._values) == 1:
 | |
|                     (item, ) = self._values.keys()
 | |
|                     header_text = header_text % eval("item.%s" % self.data_path_item)
 | |
|                 else:
 | |
|                     header_text = (self.header_text % delta) + " (delta)"
 | |
|                 context.area.header_text_set(header_text)
 | |
| 
 | |
|         elif 'LEFTMOUSE' == event_type:
 | |
|             item = next(iter(self._values.keys()))
 | |
|             self._values_clear()
 | |
|             context.area.header_text_set(None)
 | |
|             return operator_value_undo_return(item)
 | |
| 
 | |
|         elif event_type in {'RIGHTMOUSE', 'ESC'}:
 | |
|             self._values_restore()
 | |
|             context.area.header_text_set(None)
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         return {'RUNNING_MODAL'}
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
|         self._values_store(context)
 | |
| 
 | |
|         if not self._values:
 | |
|             self.report({'WARNING'}, "Nothing to operate on: %s[ ].%s" %
 | |
|                         (self.data_path_iter, self.data_path_item))
 | |
| 
 | |
|             return {'CANCELLED'}
 | |
|         else:
 | |
|             self.initial_x = event.mouse_x
 | |
| 
 | |
|             context.window_manager.modal_handler_add(self)
 | |
|             return {'RUNNING_MODAL'}
 | |
| 
 | |
| 
 | |
| class WM_OT_url_open(Operator):
 | |
|     """Open a website in the web-browser"""
 | |
|     bl_idname = "wm.url_open"
 | |
|     bl_label = ""
 | |
|     bl_options = {'INTERNAL'}
 | |
| 
 | |
|     url: StringProperty(
 | |
|         name="URL",
 | |
|         description="URL to open",
 | |
|     )
 | |
| 
 | |
|     def execute(self, _context):
 | |
|         import webbrowser
 | |
|         webbrowser.open(self.url)
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_url_open_preset(Operator):
 | |
|     """Open a preset website in the web-browser"""
 | |
|     bl_idname = "wm.url_open_preset"
 | |
|     bl_label = "Open Preset Website"
 | |
|     bl_options = {'INTERNAL'}
 | |
| 
 | |
|     type: EnumProperty(
 | |
|         name="Site",
 | |
|         items=lambda self, _context: (
 | |
|             item for (item, _) in WM_OT_url_open_preset.preset_items
 | |
|         ),
 | |
|     )
 | |
| 
 | |
|     id: StringProperty(
 | |
|         name="Identifier",
 | |
|         description="Optional identifier",
 | |
|     )
 | |
| 
 | |
|     def _url_from_bug(self, _context):
 | |
|         from bl_ui_utils.bug_report_url import url_prefill_from_blender
 | |
|         return url_prefill_from_blender()
 | |
| 
 | |
|     def _url_from_bug_addon(self, _context):
 | |
|         from bl_ui_utils.bug_report_url import url_prefill_from_blender
 | |
|         return url_prefill_from_blender(addon_info=self.id)
 | |
| 
 | |
|     def _url_from_release_notes(self, _context):
 | |
|         return "https://www.blender.org/download/releases/%d-%d/" % bpy.app.version[:2]
 | |
| 
 | |
|     def _url_from_manual(self, _context):
 | |
|         if bpy.app.version_cycle in {"rc", "release"}:
 | |
|             manual_version = "%d.%d" % bpy.app.version[:2]
 | |
|         else:
 | |
|             manual_version = "dev"
 | |
|         return "https://docs.blender.org/manual/en/" + manual_version + "/"
 | |
| 
 | |
|     # This list is: (enum_item, url) pairs.
 | |
|     # Allow dynamically extending.
 | |
|     preset_items = [
 | |
|         # Dynamic URL's.
 | |
|         (('BUG', "Bug",
 | |
|           "Report a bug with pre-filled version information"),
 | |
|          _url_from_bug),
 | |
|         (('BUG_ADDON', "Add-On Bug",
 | |
|           "Report a bug in an add-on"),
 | |
|          _url_from_bug_addon),
 | |
|         (('RELEASE_NOTES', "Release Notes",
 | |
|           "Read about whats new in this version of Blender"),
 | |
|          _url_from_release_notes),
 | |
|         (('MANUAL', "Manual",
 | |
|           "The reference manual for this version of Blender"),
 | |
|          _url_from_manual),
 | |
| 
 | |
|         # Static URL's.
 | |
|         (('FUND', "Development Fund",
 | |
|           "The donation program to support maintenance and improvements"),
 | |
|          "https://fund.blender.org"),
 | |
|         (('BLENDER', "blender.org",
 | |
|           "Blender's official web-site"),
 | |
|          "https://www.blender.org"),
 | |
|         (('CREDITS', "Credits",
 | |
|           "Lists committers to Blender's source code"),
 | |
|          "https://www.blender.org/about/credits/"),
 | |
|     ]
 | |
| 
 | |
|     def execute(self, context):
 | |
|         url = None
 | |
|         type = self.type
 | |
|         for (item_id, _, _), url in self.preset_items:
 | |
|             if item_id == type:
 | |
|                 if callable(url):
 | |
|                     url = url(self, context)
 | |
|                 break
 | |
| 
 | |
|         import webbrowser
 | |
|         webbrowser.open(url)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_path_open(Operator):
 | |
|     """Open a path in a file browser"""
 | |
|     bl_idname = "wm.path_open"
 | |
|     bl_label = ""
 | |
|     bl_options = {'INTERNAL'}
 | |
| 
 | |
|     filepath: StringProperty(
 | |
|         subtype='FILE_PATH',
 | |
|         options={'SKIP_SAVE'},
 | |
|     )
 | |
| 
 | |
|     def execute(self, _context):
 | |
|         import sys
 | |
|         import os
 | |
|         import subprocess
 | |
| 
 | |
|         filepath = self.filepath
 | |
| 
 | |
|         if not filepath:
 | |
|             self.report({'ERROR'}, "File path was not set")
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         filepath = bpy.path.abspath(filepath)
 | |
|         filepath = os.path.normpath(filepath)
 | |
| 
 | |
|         if not os.path.exists(filepath):
 | |
|             self.report({'ERROR'}, "File '%s' not found" % filepath)
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         if sys.platform[:3] == "win":
 | |
|             os.startfile(filepath)
 | |
|         elif sys.platform == "darwin":
 | |
|             subprocess.check_call(["open", filepath])
 | |
|         else:
 | |
|             try:
 | |
|                 subprocess.check_call(["xdg-open", filepath])
 | |
|             except:
 | |
|                 # xdg-open *should* be supported by recent Gnome, KDE, Xfce
 | |
|                 import traceback
 | |
|                 traceback.print_exc()
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""):
 | |
| 
 | |
|     def operator_exists_pair(a, b):
 | |
|         # Not fast, this is only for docs.
 | |
|         return b in dir(getattr(bpy.ops, a))
 | |
| 
 | |
|     def operator_exists_single(a):
 | |
|         a, b = a.partition("_OT_")[::2]
 | |
|         return operator_exists_pair(a.lower(), b)
 | |
| 
 | |
|     id_split = doc_id.split(".")
 | |
|     url = rna = None
 | |
| 
 | |
|     if len(id_split) == 1:  # rna, class
 | |
|         if do_url:
 | |
|             url = "%s/bpy.types.%s.html" % (url_prefix, id_split[0])
 | |
|         else:
 | |
|             rna = "bpy.types.%s" % id_split[0]
 | |
| 
 | |
|     elif len(id_split) == 2:  # rna, class.prop
 | |
|         class_name, class_prop = id_split
 | |
| 
 | |
|         # an operator (common case - just button referencing an op)
 | |
|         if operator_exists_pair(class_name, class_prop):
 | |
|             if do_url:
 | |
|                 url = (
 | |
|                     "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
 | |
|                     (url_prefix, class_name, class_name, class_prop)
 | |
|                 )
 | |
|             else:
 | |
|                 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
 | |
|         elif operator_exists_single(class_name):
 | |
|             # note: ignore the prop name since we don't have a way to link into it
 | |
|             class_name, class_prop = class_name.split("_OT_", 1)
 | |
|             class_name = class_name.lower()
 | |
|             if do_url:
 | |
|                 url = (
 | |
|                     "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
 | |
|                     (url_prefix, class_name, class_name, class_prop)
 | |
|                 )
 | |
|             else:
 | |
|                 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
 | |
|         else:
 | |
|             # an RNA setting, common case
 | |
|             rna_class = getattr(bpy.types, class_name)
 | |
| 
 | |
|             # detect if this is a inherited member and use that name instead
 | |
|             rna_parent = rna_class.bl_rna
 | |
|             rna_prop = rna_parent.properties.get(class_prop)
 | |
|             if rna_prop:
 | |
|                 rna_parent = rna_parent.base
 | |
|                 while rna_parent and rna_prop == rna_parent.properties.get(class_prop):
 | |
|                     class_name = rna_parent.identifier
 | |
|                     rna_parent = rna_parent.base
 | |
| 
 | |
|                 if do_url:
 | |
|                     url = (
 | |
|                         "%s/bpy.types.%s.html#bpy.types.%s.%s" %
 | |
|                         (url_prefix, class_name, class_name, class_prop)
 | |
|                     )
 | |
|                 else:
 | |
|                     rna = "bpy.types.%s.%s" % (class_name, class_prop)
 | |
|             else:
 | |
|                 # We assume this is custom property, only try to generate generic url/rna_id...
 | |
|                 if do_url:
 | |
|                     url = ("%s/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items" % (url_prefix,))
 | |
|                 else:
 | |
|                     rna = "bpy.types.bpy_struct"
 | |
| 
 | |
|     return url if do_url else rna
 | |
| 
 | |
| 
 | |
| class WM_OT_doc_view_manual(Operator):
 | |
|     """Load online manual"""
 | |
|     bl_idname = "wm.doc_view_manual"
 | |
|     bl_label = "View Manual"
 | |
| 
 | |
|     doc_id: doc_id
 | |
| 
 | |
|     @staticmethod
 | |
|     def _find_reference(rna_id, url_mapping, verbose=True):
 | |
|         if verbose:
 | |
|             print("online manual check for: '%s'... " % rna_id)
 | |
|         from fnmatch import fnmatchcase
 | |
|         # XXX, for some reason all RNA ID's are stored lowercase
 | |
|         # Adding case into all ID's isn't worth the hassle so force lowercase.
 | |
|         rna_id = rna_id.lower()
 | |
|         for pattern, url_suffix in url_mapping:
 | |
|             if fnmatchcase(rna_id, pattern):
 | |
|                 if verbose:
 | |
|                     print("            match found: '%s' --> '%s'" % (pattern, url_suffix))
 | |
|                 return url_suffix
 | |
|         if verbose:
 | |
|             print("match not found")
 | |
|         return None
 | |
| 
 | |
|     @staticmethod
 | |
|     def _lookup_rna_url(rna_id, verbose=True):
 | |
|         for prefix, url_manual_mapping in bpy.utils.manual_map():
 | |
|             rna_ref = WM_OT_doc_view_manual._find_reference(rna_id, url_manual_mapping, verbose=verbose)
 | |
|             if rna_ref is not None:
 | |
|                 url = prefix + rna_ref
 | |
|                 return url
 | |
| 
 | |
|     def execute(self, _context):
 | |
|         rna_id = _wm_doc_get_id(self.doc_id, do_url=False)
 | |
|         if rna_id is None:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         url = self._lookup_rna_url(rna_id)
 | |
| 
 | |
|         if url is None:
 | |
|             self.report(
 | |
|                 {'WARNING'},
 | |
|                 "No reference available %r, "
 | |
|                 "Update info in 'rna_manual_reference.py' "
 | |
|                 "or callback to bpy.utils.manual_map()" %
 | |
|                 self.doc_id
 | |
|             )
 | |
|             return {'CANCELLED'}
 | |
|         else:
 | |
|             import webbrowser
 | |
|             webbrowser.open(url)
 | |
|             return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_doc_view(Operator):
 | |
|     """Open online reference docs in a web browser"""
 | |
|     bl_idname = "wm.doc_view"
 | |
|     bl_label = "View Documentation"
 | |
| 
 | |
|     doc_id: doc_id
 | |
|     if bpy.app.version_cycle in {"release", "rc", "beta"}:
 | |
|         _prefix = ("https://docs.blender.org/api/%d.%d%s" %
 | |
|                    (bpy.app.version[0], bpy.app.version[1], bpy.app.version_char))
 | |
|     else:
 | |
|         _prefix = ("https://docs.blender.org/api/master")
 | |
| 
 | |
|     def execute(self, _context):
 | |
|         url = _wm_doc_get_id(self.doc_id, do_url=True, url_prefix=self._prefix)
 | |
|         if url is None:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         import webbrowser
 | |
|         webbrowser.open(url)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| rna_path = StringProperty(
 | |
|     name="Property Edit",
 | |
|     description="Property data_path edit",
 | |
|     maxlen=1024,
 | |
|     options={'HIDDEN'},
 | |
| )
 | |
| 
 | |
| rna_value = StringProperty(
 | |
|     name="Property Value",
 | |
|     description="Property value edit",
 | |
|     maxlen=1024,
 | |
| )
 | |
| 
 | |
| rna_default = StringProperty(
 | |
|     name="Default Value",
 | |
|     description="Default value of the property. Important for NLA mixing",
 | |
|     maxlen=1024,
 | |
| )
 | |
| 
 | |
| rna_property = StringProperty(
 | |
|     name="Property Name",
 | |
|     description="Property name edit",
 | |
|     maxlen=1024,
 | |
| )
 | |
| 
 | |
| rna_min = FloatProperty(
 | |
|     name="Min",
 | |
|     default=-10000.0,
 | |
|     precision=3,
 | |
| )
 | |
| 
 | |
| rna_max = FloatProperty(
 | |
|     name="Max",
 | |
|     default=10000.0,
 | |
|     precision=3,
 | |
| )
 | |
| 
 | |
| rna_use_soft_limits = BoolProperty(
 | |
|     name="Use Soft Limits",
 | |
| )
 | |
| 
 | |
| rna_is_overridable_library = BoolProperty(
 | |
|     name="Is Library Overridable",
 | |
|     default=False,
 | |
| )
 | |
| 
 | |
| # Most useful entries of rna_enum_property_subtype_items for number arrays:
 | |
| rna_vector_subtype_items = (
 | |
|     ('NONE', "Plain Data", "Data values without special behavior"),
 | |
|     ('COLOR', "Linear Color", "Color in the linear space"),
 | |
|     ('COLOR_GAMMA', "Gamma-Corrected Color", "Color in the gamma corrected space"),
 | |
|     ('EULER', "Euler Angles", "Euler rotation angles in radians"),
 | |
|     ('QUATERNION', "Quaternion Rotation", "Quaternion rotation (affects NLA blending)"),
 | |
| )
 | |
| 
 | |
| 
 | |
| class WM_OT_properties_edit(Operator):
 | |
|     bl_idname = "wm.properties_edit"
 | |
|     bl_label = "Edit Property"
 | |
|     # register only because invoke_props_popup requires.
 | |
|     bl_options = {'REGISTER', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path
 | |
|     property: rna_property
 | |
|     value: rna_value
 | |
|     default: rna_default
 | |
|     min: rna_min
 | |
|     max: rna_max
 | |
|     use_soft_limits: rna_use_soft_limits
 | |
|     is_overridable_library: rna_is_overridable_library
 | |
|     soft_min: rna_min
 | |
|     soft_max: rna_max
 | |
|     description: StringProperty(
 | |
|         name="Tooltip",
 | |
|     )
 | |
|     subtype: EnumProperty(
 | |
|         name="Subtype",
 | |
|         items=lambda self, _context: WM_OT_properties_edit.subtype_items,
 | |
|     )
 | |
| 
 | |
|     subtype_items = rna_vector_subtype_items
 | |
| 
 | |
|     def _init_subtype(self, prop_type, is_array, subtype):
 | |
|         subtype = subtype or 'NONE'
 | |
|         subtype_items = rna_vector_subtype_items
 | |
| 
 | |
|         # Add a temporary enum entry to preserve unknown subtypes
 | |
|         if not any(subtype == item[0] for item in subtype_items):
 | |
|             subtype_items += ((subtype, subtype, ""),)
 | |
| 
 | |
|         WM_OT_properties_edit.subtype_items = subtype_items
 | |
|         self.subtype = subtype
 | |
| 
 | |
|     def _cmp_props_get(self):
 | |
|         # Changing these properties will refresh the UI
 | |
|         return {
 | |
|             "use_soft_limits": self.use_soft_limits,
 | |
|             "soft_range": (self.soft_min, self.soft_max),
 | |
|             "hard_range": (self.min, self.max),
 | |
|         }
 | |
| 
 | |
|     def get_value_eval(self):
 | |
|         try:
 | |
|             value_eval = eval(self.value)
 | |
|             # assert else None -> None, not "None", see [#33431]
 | |
|             assert(type(value_eval) in {str, float, int, bool, tuple, list})
 | |
|         except:
 | |
|             value_eval = self.value
 | |
| 
 | |
|         return value_eval
 | |
| 
 | |
|     def get_default_eval(self):
 | |
|         try:
 | |
|             default_eval = eval(self.default)
 | |
|             # assert else None -> None, not "None", see [#33431]
 | |
|             assert(type(default_eval) in {str, float, int, bool, tuple, list})
 | |
|         except:
 | |
|             default_eval = self.default
 | |
| 
 | |
|         return default_eval
 | |
| 
 | |
|     def execute(self, context):
 | |
|         from rna_prop_ui import (
 | |
|             rna_idprop_ui_prop_get,
 | |
|             rna_idprop_ui_prop_clear,
 | |
|             rna_idprop_ui_prop_update,
 | |
|             rna_idprop_ui_prop_default_set,
 | |
|             rna_idprop_value_item_type,
 | |
|         )
 | |
| 
 | |
|         data_path = self.data_path
 | |
|         prop = self.property
 | |
| 
 | |
|         prop_old = getattr(self, "_last_prop", [None])[0]
 | |
| 
 | |
|         if prop_old is None:
 | |
|             self.report({'ERROR'}, "Direct execution not supported")
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         value_eval = self.get_value_eval()
 | |
|         default_eval = self.get_default_eval()
 | |
| 
 | |
|         # First remove
 | |
|         item = eval("context.%s" % data_path)
 | |
|         prop_type_old = type(item[prop_old])
 | |
| 
 | |
|         rna_idprop_ui_prop_clear(item, prop_old)
 | |
|         exec_str = "del item[%r]" % prop_old
 | |
|         # print(exec_str)
 | |
|         exec(exec_str)
 | |
| 
 | |
|         # Reassign
 | |
|         exec_str = "item[%r] = %s" % (prop, repr(value_eval))
 | |
|         # print(exec_str)
 | |
|         exec(exec_str)
 | |
| 
 | |
|         exec_str = "item.property_overridable_library_set('[\"%s\"]', %s)" % (prop, self.is_overridable_library)
 | |
|         exec(exec_str)
 | |
| 
 | |
|         rna_idprop_ui_prop_update(item, prop)
 | |
| 
 | |
|         self._last_prop[:] = [prop]
 | |
| 
 | |
|         prop_value = item[prop]
 | |
|         prop_type_new = type(prop_value)
 | |
|         prop_type, is_array = rna_idprop_value_item_type(prop_value)
 | |
| 
 | |
|         prop_ui = rna_idprop_ui_prop_get(item, prop)
 | |
| 
 | |
|         if prop_type in {float, int}:
 | |
|             prop_ui["min"] = prop_type(self.min)
 | |
|             prop_ui["max"] = prop_type(self.max)
 | |
| 
 | |
|             if self.use_soft_limits:
 | |
|                 prop_ui["soft_min"] = prop_type(self.soft_min)
 | |
|                 prop_ui["soft_max"] = prop_type(self.soft_max)
 | |
|             else:
 | |
|                 prop_ui["soft_min"] = prop_type(self.min)
 | |
|                 prop_ui["soft_max"] = prop_type(self.max)
 | |
| 
 | |
|         if prop_type == float and is_array and self.subtype != 'NONE':
 | |
|             prop_ui["subtype"] = self.subtype
 | |
|         else:
 | |
|             prop_ui.pop("subtype", None)
 | |
| 
 | |
|         prop_ui["description"] = self.description
 | |
| 
 | |
|         rna_idprop_ui_prop_default_set(item, prop, default_eval)
 | |
| 
 | |
|         # If we have changed the type of the property, update its potential anim curves!
 | |
|         if prop_type_old != prop_type_new:
 | |
|             data_path = '["%s"]' % bpy.utils.escape_identifier(prop)
 | |
|             done = set()
 | |
| 
 | |
|             def _update(fcurves):
 | |
|                 for fcu in fcurves:
 | |
|                     if fcu not in done and fcu.data_path == data_path:
 | |
|                         fcu.update_autoflags(item)
 | |
|                         done.add(fcu)
 | |
| 
 | |
|             def _update_strips(strips):
 | |
|                 for st in strips:
 | |
|                     if st.type == 'CLIP' and st.action:
 | |
|                         _update(st.action.fcurves)
 | |
|                     elif st.type == 'META':
 | |
|                         _update_strips(st.strips)
 | |
| 
 | |
|             adt = getattr(item, "animation_data", None)
 | |
|             if adt is not None:
 | |
|                 if adt.action:
 | |
|                     _update(adt.action.fcurves)
 | |
|                 if adt.drivers:
 | |
|                     _update(adt.drivers)
 | |
|                 if adt.nla_tracks:
 | |
|                     for nt in adt.nla_tracks:
 | |
|                         _update_strips(nt.strips)
 | |
| 
 | |
|         # otherwise existing buttons which reference freed
 | |
|         # memory may crash blender [#26510]
 | |
|         # context.area.tag_redraw()
 | |
|         for win in context.window_manager.windows:
 | |
|             for area in win.screen.areas:
 | |
|                 area.tag_redraw()
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
|     def invoke(self, context, _event):
 | |
|         from rna_prop_ui import (
 | |
|             rna_idprop_ui_prop_get,
 | |
|             rna_idprop_value_to_python,
 | |
|             rna_idprop_value_item_type
 | |
|         )
 | |
| 
 | |
|         data_path = self.data_path
 | |
| 
 | |
|         if not data_path:
 | |
|             self.report({'ERROR'}, "Data path not set")
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         self._last_prop = [self.property]
 | |
| 
 | |
|         item = eval("context.%s" % data_path)
 | |
| 
 | |
|         # retrieve overridable static
 | |
|         exec_str = "item.is_property_overridable_library('[\"%s\"]')" % (self.property)
 | |
|         self.is_overridable_library = bool(eval(exec_str))
 | |
| 
 | |
|         # default default value
 | |
|         prop_type, is_array = rna_idprop_value_item_type(self.get_value_eval())
 | |
|         if prop_type in {int, float}:
 | |
|             self.default = str(prop_type(0))
 | |
|         else:
 | |
|             self.default = ""
 | |
| 
 | |
|         # setup defaults
 | |
|         prop_ui = rna_idprop_ui_prop_get(item, self.property, False)  # don't create
 | |
|         if prop_ui:
 | |
|             self.min = prop_ui.get("min", -1000000000)
 | |
|             self.max = prop_ui.get("max", 1000000000)
 | |
|             self.description = prop_ui.get("description", "")
 | |
| 
 | |
|             defval = prop_ui.get("default", None)
 | |
|             if defval is not None:
 | |
|                 self.default = str(rna_idprop_value_to_python(defval))
 | |
| 
 | |
|             self.soft_min = prop_ui.get("soft_min", self.min)
 | |
|             self.soft_max = prop_ui.get("soft_max", self.max)
 | |
|             self.use_soft_limits = (
 | |
|                 self.min != self.soft_min or
 | |
|                 self.max != self.soft_max
 | |
|             )
 | |
| 
 | |
|             subtype = prop_ui.get("subtype", None)
 | |
|         else:
 | |
|             subtype = None
 | |
| 
 | |
|         self._init_subtype(prop_type, is_array, subtype)
 | |
| 
 | |
|         # store for comparison
 | |
|         self._cmp_props = self._cmp_props_get()
 | |
| 
 | |
|         wm = context.window_manager
 | |
|         return wm.invoke_props_dialog(self)
 | |
| 
 | |
|     def check(self, _context):
 | |
|         cmp_props = self._cmp_props_get()
 | |
|         changed = False
 | |
|         if self._cmp_props != cmp_props:
 | |
|             if cmp_props["use_soft_limits"]:
 | |
|                 if cmp_props["soft_range"] != self._cmp_props["soft_range"]:
 | |
|                     self.min = min(self.min, self.soft_min)
 | |
|                     self.max = max(self.max, self.soft_max)
 | |
|                     changed = True
 | |
|                 if cmp_props["hard_range"] != self._cmp_props["hard_range"]:
 | |
|                     self.soft_min = max(self.min, self.soft_min)
 | |
|                     self.soft_max = min(self.max, self.soft_max)
 | |
|                     changed = True
 | |
|             else:
 | |
|                 if cmp_props["soft_range"] != cmp_props["hard_range"]:
 | |
|                     self.soft_min = self.min
 | |
|                     self.soft_max = self.max
 | |
|                     changed = True
 | |
| 
 | |
|             changed |= (cmp_props["use_soft_limits"] != self._cmp_props["use_soft_limits"])
 | |
| 
 | |
|             if changed:
 | |
|                 cmp_props = self._cmp_props_get()
 | |
| 
 | |
|             self._cmp_props = cmp_props
 | |
| 
 | |
|         return changed
 | |
| 
 | |
|     def draw(self, _context):
 | |
|         from rna_prop_ui import (
 | |
|             rna_idprop_value_item_type,
 | |
|         )
 | |
| 
 | |
|         layout = self.layout
 | |
|         layout.prop(self, "property")
 | |
|         layout.prop(self, "value")
 | |
| 
 | |
|         value = self.get_value_eval()
 | |
|         proptype, is_array = rna_idprop_value_item_type(value)
 | |
| 
 | |
|         row = layout.row()
 | |
|         row.enabled = proptype in {int, float}
 | |
|         row.prop(self, "default")
 | |
| 
 | |
|         row = layout.row(align=True)
 | |
|         row.prop(self, "min")
 | |
|         row.prop(self, "max")
 | |
| 
 | |
|         row = layout.row()
 | |
|         row.prop(self, "use_soft_limits")
 | |
|         if bpy.app.use_override_library:
 | |
|             row.prop(self, "is_overridable_library")
 | |
| 
 | |
|         row = layout.row(align=True)
 | |
|         row.enabled = self.use_soft_limits
 | |
|         row.prop(self, "soft_min", text="Soft Min")
 | |
|         row.prop(self, "soft_max", text="Soft Max")
 | |
|         layout.prop(self, "description")
 | |
| 
 | |
|         if is_array and proptype == float:
 | |
|             layout.prop(self, "subtype")
 | |
| 
 | |
| 
 | |
| class WM_OT_properties_add(Operator):
 | |
|     bl_idname = "wm.properties_add"
 | |
|     bl_label = "Add Property"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path
 | |
| 
 | |
|     def execute(self, context):
 | |
|         from rna_prop_ui import (
 | |
|             rna_idprop_ui_create,
 | |
|         )
 | |
| 
 | |
|         data_path = self.data_path
 | |
|         item = eval("context.%s" % data_path)
 | |
| 
 | |
|         def unique_name(names):
 | |
|             prop = "prop"
 | |
|             prop_new = prop
 | |
|             i = 1
 | |
|             while prop_new in names:
 | |
|                 prop_new = prop + str(i)
 | |
|                 i += 1
 | |
| 
 | |
|             return prop_new
 | |
| 
 | |
|         prop = unique_name({
 | |
|             *item.keys(),
 | |
|             *type(item).bl_rna.properties.keys(),
 | |
|         })
 | |
| 
 | |
|         rna_idprop_ui_create(item, prop, default=1.0)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_properties_context_change(Operator):
 | |
|     """Jump to a different tab inside the properties editor"""
 | |
|     bl_idname = "wm.properties_context_change"
 | |
|     bl_label = ""
 | |
|     bl_options = {'INTERNAL'}
 | |
| 
 | |
|     context: StringProperty(
 | |
|         name="Context",
 | |
|         maxlen=64,
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         context.space_data.context = self.context
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_properties_remove(Operator):
 | |
|     """Internal use (edit a property data_path)"""
 | |
|     bl_idname = "wm.properties_remove"
 | |
|     bl_label = "Remove Property"
 | |
|     bl_options = {'UNDO', 'INTERNAL'}
 | |
| 
 | |
|     data_path: rna_path
 | |
|     property: rna_property
 | |
| 
 | |
|     def execute(self, context):
 | |
|         from rna_prop_ui import (
 | |
|             rna_idprop_ui_prop_clear,
 | |
|             rna_idprop_ui_prop_update,
 | |
|         )
 | |
|         data_path = self.data_path
 | |
|         item = eval("context.%s" % data_path)
 | |
|         prop = self.property
 | |
|         rna_idprop_ui_prop_update(item, prop)
 | |
|         del item[prop]
 | |
|         rna_idprop_ui_prop_clear(item, prop)
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_sysinfo(Operator):
 | |
|     """Generate system information, saved into a text file"""
 | |
| 
 | |
|     bl_idname = "wm.sysinfo"
 | |
|     bl_label = "Save System Info"
 | |
| 
 | |
|     filepath: StringProperty(
 | |
|         subtype='FILE_PATH',
 | |
|         options={'SKIP_SAVE'},
 | |
|     )
 | |
| 
 | |
|     def execute(self, _context):
 | |
|         import sys_info
 | |
|         sys_info.write_sysinfo(self.filepath)
 | |
|         return {'FINISHED'}
 | |
| 
 | |
|     def invoke(self, context, _event):
 | |
|         import os
 | |
| 
 | |
|         if not self.filepath:
 | |
|             self.filepath = os.path.join(
 | |
|                 os.path.expanduser("~"), "system-info.txt")
 | |
| 
 | |
|         wm = context.window_manager
 | |
|         wm.fileselect_add(self)
 | |
|         return {'RUNNING_MODAL'}
 | |
| 
 | |
| 
 | |
| class WM_OT_operator_cheat_sheet(Operator):
 | |
|     """List all the Operators in a text-block, useful for scripting"""
 | |
|     bl_idname = "wm.operator_cheat_sheet"
 | |
|     bl_label = "Operator Cheat Sheet"
 | |
| 
 | |
|     def execute(self, _context):
 | |
|         op_strings = []
 | |
|         tot = 0
 | |
|         for op_module_name in dir(bpy.ops):
 | |
|             op_module = getattr(bpy.ops, op_module_name)
 | |
|             for op_submodule_name in dir(op_module):
 | |
|                 op = getattr(op_module, op_submodule_name)
 | |
|                 text = repr(op)
 | |
|                 if text.split("\n")[-1].startswith("bpy.ops."):
 | |
|                     op_strings.append(text)
 | |
|                     tot += 1
 | |
| 
 | |
|             op_strings.append('')
 | |
| 
 | |
|         textblock = bpy.data.texts.new("OperatorList.txt")
 | |
|         textblock.write('# %d Operators\n\n' % tot)
 | |
|         textblock.write('\n'.join(op_strings))
 | |
|         self.report({'INFO'}, "See OperatorList.txt textblock")
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| # -----------------------------------------------------------------------------
 | |
| # Add-on Operators
 | |
| 
 | |
| class WM_OT_owner_enable(Operator):
 | |
|     """Enable workspace owner ID"""
 | |
|     bl_idname = "wm.owner_enable"
 | |
|     bl_label = "Enable Add-on"
 | |
| 
 | |
|     owner_id: StringProperty(
 | |
|         name="UI Tag",
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         workspace = context.workspace
 | |
|         workspace.owner_ids.new(self.owner_id)
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_owner_disable(Operator):
 | |
|     """Enable workspace owner ID"""
 | |
|     bl_idname = "wm.owner_disable"
 | |
|     bl_label = "Disable UI Tag"
 | |
| 
 | |
|     owner_id: StringProperty(
 | |
|         name="UI Tag",
 | |
|     )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         workspace = context.workspace
 | |
|         owner_id = workspace.owner_ids[self.owner_id]
 | |
|         workspace.owner_ids.remove(owner_id)
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_tool_set_by_id(Operator):
 | |
|     """Set the tool by name (for keymaps)"""
 | |
|     bl_idname = "wm.tool_set_by_id"
 | |
|     bl_label = "Set Tool By Name"
 | |
| 
 | |
|     name: StringProperty(
 | |
|         name="Identifier",
 | |
|         description="Identifier of the tool",
 | |
|     )
 | |
|     cycle: BoolProperty(
 | |
|         name="Cycle",
 | |
|         description="Cycle through tools in this group",
 | |
|         default=False,
 | |
|         options={'SKIP_SAVE'},
 | |
|     )
 | |
|     as_fallback: BoolProperty(
 | |
|         name="Set Fallback",
 | |
|         description="Set the fallback tool instead of the primary tool",
 | |
|         default=False,
 | |
|         options={'SKIP_SAVE', 'HIDDEN'},
 | |
|     )
 | |
| 
 | |
|     space_type: rna_space_type_prop
 | |
| 
 | |
|     if use_toolbar_release_hack:
 | |
|         def invoke(self, context, event):
 | |
|             # Hack :S
 | |
|             if not self.properties.is_property_set("name"):
 | |
|                 WM_OT_toolbar._key_held = False
 | |
|                 return {'PASS_THROUGH'}
 | |
|             elif (WM_OT_toolbar._key_held == event.type) and (event.value != 'RELEASE'):
 | |
|                 return {'PASS_THROUGH'}
 | |
|             WM_OT_toolbar._key_held = None
 | |
| 
 | |
|             return self.execute(context)
 | |
| 
 | |
|     def execute(self, context):
 | |
|         from bl_ui.space_toolsystem_common import (
 | |
|             activate_by_id,
 | |
|             activate_by_id_or_cycle,
 | |
|         )
 | |
| 
 | |
|         if self.properties.is_property_set("space_type"):
 | |
|             space_type = self.space_type
 | |
|         else:
 | |
|             space_type = context.space_data.type
 | |
| 
 | |
|         fn = activate_by_id_or_cycle if self.cycle else activate_by_id
 | |
|         if fn(context, space_type, self.name, as_fallback=self.as_fallback):
 | |
|             if self.as_fallback:
 | |
|                 tool_settings = context.tool_settings
 | |
|                 tool_settings.workspace_tool_type = 'FALLBACK'
 | |
|             return {'FINISHED'}
 | |
|         else:
 | |
|             self.report({'WARNING'}, f"Tool {self.name!r:s} not found for space {space_type!r:s}.")
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_tool_set_by_index(Operator):
 | |
|     """Set the tool by index (for keymaps)"""
 | |
|     bl_idname = "wm.tool_set_by_index"
 | |
|     bl_label = "Set Tool By Index"
 | |
|     index: IntProperty(
 | |
|         name="Index in toolbar",
 | |
|         default=0,
 | |
|     )
 | |
|     cycle: BoolProperty(
 | |
|         name="Cycle",
 | |
|         description="Cycle through tools in this group",
 | |
|         default=False,
 | |
|         options={'SKIP_SAVE'},
 | |
|     )
 | |
| 
 | |
|     expand: BoolProperty(
 | |
|         description="Include tool sub-groups",
 | |
|         default=True,
 | |
|     )
 | |
| 
 | |
|     as_fallback: BoolProperty(
 | |
|         name="Set Fallback",
 | |
|         description="Set the fallback tool instead of the primary",
 | |
|         default=False,
 | |
|         options={'SKIP_SAVE', 'HIDDEN'},
 | |
|     )
 | |
| 
 | |
|     space_type: rna_space_type_prop
 | |
| 
 | |
|     def execute(self, context):
 | |
|         from bl_ui.space_toolsystem_common import (
 | |
|             activate_by_id,
 | |
|             activate_by_id_or_cycle,
 | |
|             item_from_index_active,
 | |
|             item_from_flat_index,
 | |
|         )
 | |
| 
 | |
|         if self.properties.is_property_set("space_type"):
 | |
|             space_type = self.space_type
 | |
|         else:
 | |
|             space_type = context.space_data.type
 | |
| 
 | |
|         fn = item_from_flat_index if self.expand else item_from_index_active
 | |
|         item = fn(context, space_type, self.index)
 | |
|         if item is None:
 | |
|             # Don't report, since the number of tools may change.
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         # Same as: WM_OT_tool_set_by_id
 | |
|         fn = activate_by_id_or_cycle if self.cycle else activate_by_id
 | |
|         if fn(context, space_type, item.idname, as_fallback=self.as_fallback):
 | |
|             if self.as_fallback:
 | |
|                 tool_settings = context.tool_settings
 | |
|                 tool_settings.workspace_tool_type = 'FALLBACK'
 | |
|             return {'FINISHED'}
 | |
|         else:
 | |
|             # Since we already have the tool, this can't happen.
 | |
|             raise Exception("Internal error setting tool")
 | |
| 
 | |
| 
 | |
| class WM_OT_toolbar(Operator):
 | |
|     bl_idname = "wm.toolbar"
 | |
|     bl_label = "Toolbar"
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         return context.space_data is not None
 | |
| 
 | |
|     if use_toolbar_release_hack:
 | |
|         _key_held = None
 | |
| 
 | |
|         def invoke(self, context, event):
 | |
|             WM_OT_toolbar._key_held = event.type
 | |
|             return self.execute(context)
 | |
| 
 | |
|     @staticmethod
 | |
|     def keymap_from_toolbar(context, space_type, use_fallback_keys=True, use_reset=True):
 | |
|         from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
 | |
|         from bl_keymap_utils import keymap_from_toolbar
 | |
| 
 | |
|         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
 | |
|         if cls is None:
 | |
|             return None, None
 | |
| 
 | |
|         return cls, keymap_from_toolbar.generate(
 | |
|             context,
 | |
|             space_type,
 | |
|             use_fallback_keys=use_fallback_keys,
 | |
|             use_reset=use_reset,
 | |
|         )
 | |
| 
 | |
|     def execute(self, context):
 | |
|         space_type = context.space_data.type
 | |
|         cls, keymap = self.keymap_from_toolbar(context, space_type)
 | |
|         if keymap is None:
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         def draw_menu(popover, context):
 | |
|             layout = popover.layout
 | |
|             layout.operator_context = 'INVOKE_REGION_WIN'
 | |
|             cls.draw_cls(layout, context, detect_layout=False, scale_y=1.0)
 | |
| 
 | |
|         wm = context.window_manager
 | |
|         wm.popover(draw_menu, ui_units_x=8, keymap=keymap)
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_toolbar_fallback_pie(Operator):
 | |
|     bl_idname = "wm.toolbar_fallback_pie"
 | |
|     bl_label = "Fallback Tool Pie Menu"
 | |
| 
 | |
|     @classmethod
 | |
|     def poll(cls, context):
 | |
|         return context.space_data is not None
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
|         if not context.preferences.experimental.use_tool_fallback:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
 | |
|         space_type = context.space_data.type
 | |
|         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
 | |
|         if cls is None:
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         # It's possible we don't have the fallback tool available.
 | |
|         # This can happen in the image editor for example when there is no selection
 | |
|         # in painting modes.
 | |
|         item, _ = cls._tool_get_by_id(context, cls.tool_fallback_id)
 | |
|         if item is None:
 | |
|             print("Tool", cls.tool_fallback_id, "not active in", cls)
 | |
|             return {'PASS_THROUGH'}
 | |
| 
 | |
|         def draw_cb(self, context):
 | |
|             from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
 | |
|             ToolSelectPanelHelper.draw_fallback_tool_items_for_pie_menu(self.layout, context)
 | |
| 
 | |
|         wm = context.window_manager
 | |
|         wm.popup_menu_pie(draw_func=draw_cb, title="Fallback Tool", event=event)
 | |
|         return {'FINISHED'}
 | |
| 
 | |
| 
 | |
| class WM_OT_toolbar_prompt(Operator):
 | |
|     """Leader key like functionality for accessing tools"""
 | |
|     bl_idname = "wm.toolbar_prompt"
 | |
|     bl_label = "Toolbar Prompt"
 | |
| 
 | |
|     @staticmethod
 | |
|     def _status_items_generate(cls, keymap, context):
 | |
|         from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
 | |
| 
 | |
|         # The keymap doesn't have the same order the tools are declared in,
 | |
|         # while we could support this, it's simpler to apply order here.
 | |
|         tool_map_id_to_order = {}
 | |
|         # Map the
 | |
|         tool_map_id_to_label = {}
 | |
|         for item in ToolSelectPanelHelper._tools_flatten(cls.tools_from_context(context)):
 | |
|             if item is not None:
 | |
|                 tool_map_id_to_label[item.idname] = item.label
 | |
|                 tool_map_id_to_order[item.idname] = len(tool_map_id_to_order)
 | |
| 
 | |
|         status_items = []
 | |
| 
 | |
|         for item in keymap.keymap_items:
 | |
|             name = item.name
 | |
|             key_str = item.to_string()
 | |
|             # These are duplicated from regular numbers.
 | |
|             if key_str.startswith("Numpad "):
 | |
|                 continue
 | |
|             properties = item.properties
 | |
|             idname = item.idname
 | |
|             if idname == "wm.tool_set_by_id":
 | |
|                 tool_idname = properties["name"]
 | |
|                 name = tool_map_id_to_label[tool_idname]
 | |
|                 name = name.replace("Annotate ", "")
 | |
|             else:
 | |
|                 continue
 | |
| 
 | |
|             status_items.append((tool_idname, name, item))
 | |
| 
 | |
|         status_items.sort(
 | |
|             key=lambda a: tool_map_id_to_order[a[0]]
 | |
|         )
 | |
|         return status_items
 | |
| 
 | |
|     def modal(self, context, event):
 | |
|         event_type = event.type
 | |
|         event_value = event.value
 | |
| 
 | |
|         if event_type in {
 | |
|                 'LEFTMOUSE', 'RIGHTMOUSE', 'MIDDLEMOUSE',
 | |
|                 'WHEELDOWNMOUSE', 'WHEELUPMOUSE', 'WHEELINMOUSE', 'WHEELOUTMOUSE',
 | |
|                 'ESC',
 | |
|         }:
 | |
|             context.workspace.status_text_set(None)
 | |
|             return {'CANCELLED', 'PASS_THROUGH'}
 | |
| 
 | |
|         keymap = self._keymap
 | |
|         item = keymap.keymap_items.match_event(event)
 | |
|         if item is not None:
 | |
|             idname = item.idname
 | |
|             properties = item.properties
 | |
|             if idname == "wm.tool_set_by_id":
 | |
|                 tool_idname = properties["name"]
 | |
|                 bpy.ops.wm.tool_set_by_id(name=tool_idname)
 | |
| 
 | |
|             context.workspace.status_text_set(None)
 | |
|             return {'FINISHED'}
 | |
| 
 | |
|         # Pressing entry even again exists, as long as it's not mapped to a key (for convenience).
 | |
|         if event_type == self._init_event_type:
 | |
|             if event_value == 'RELEASE':
 | |
|                 if not (event.ctrl or event.alt or event.shift or event.oskey):
 | |
|                     context.workspace.status_text_set(None)
 | |
|                     return {'CANCELLED'}
 | |
| 
 | |
|         return {'RUNNING_MODAL'}
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
|         space_data = context.space_data
 | |
|         if space_data is None:
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         space_type = space_data.type
 | |
|         cls, keymap = WM_OT_toolbar.keymap_from_toolbar(
 | |
|             context,
 | |
|             space_type,
 | |
|             use_fallback_keys=False,
 | |
|             use_reset=False,
 | |
|         )
 | |
|         if (keymap is None) or (not keymap.keymap_items):
 | |
|             return {'CANCELLED'}
 | |
| 
 | |
|         self._init_event_type = event.type
 | |
| 
 | |
|         # Strip Left/Right, since "Left Alt" isn't especially useful.
 | |
|         init_event_type_as_text = self._init_event_type.title().split("_")
 | |
|         if init_event_type_as_text[0] in {"Left", "Right"}:
 | |
|             del init_event_type_as_text[0]
 | |
|         init_event_type_as_text = " ".join(init_event_type_as_text)
 | |
| 
 | |
|         status_items = self._status_items_generate(cls, keymap, context)
 | |
| 
 | |
|         def status_text_fn(self, context):
 | |
| 
 | |
|             layout = self.layout
 | |
|             if True:
 | |
|                 box = layout.row(align=True).box()
 | |
|                 box.scale_x = 0.8
 | |
|                 box.label(text=init_event_type_as_text)
 | |
| 
 | |
|             flow = layout.grid_flow(columns=len(status_items), align=True, row_major=True)
 | |
|             for _, name, item in status_items:
 | |
|                 row = flow.row(align=True)
 | |
|                 row.template_event_from_keymap_item(item, text=name)
 | |
| 
 | |
|         self._keymap = keymap
 | |
| 
 | |
|         context.workspace.status_text_set(status_text_fn)
 | |
| 
 | |
|         context.window_manager.modal_handler_add(self)
 | |
|         return {'RUNNING_MODAL'}
 | |
| 
 | |
| 
 | |
| class BatchRenameAction(bpy.types.PropertyGroup):
 | |
|     # category: StringProperty()
 | |
|     type: EnumProperty(
 | |
|         name="Operation",
 | |
|         items=(
 | |
|             ('REPLACE', "Find/Replace", "Replace text in the name"),
 | |
|             ('SET', "Set Name", "Set a new name or prefix/suffix the existing one"),
 | |
|             ('STRIP', "Strip Characters", "Strip leading/trailing text from the name"),
 | |
|             ('CASE', "Change Case", "Change case of each name"),
 | |
|         ),
 | |
|     )
 | |
| 
 | |
|     # We could split these into sub-properties, however it's not so important.
 | |
| 
 | |
|     # type: 'SET'.
 | |
|     set_name: StringProperty(name="Name")
 | |
|     set_method: EnumProperty(
 | |
|         name="Method",
 | |
|         items=(
 | |
|             ('NEW', "New", ""),
 | |
|             ('PREFIX', "Prefix", ""),
 | |
|             ('SUFFIX', "Suffix", ""),
 | |
|         ),
 | |
|         default='SUFFIX',
 | |
|     )
 | |
| 
 | |
|     # type: 'STRIP'.
 | |
|     strip_chars: EnumProperty(
 | |
|         name="Strip Characters",
 | |
|         options={'ENUM_FLAG'},
 | |
|         items=(
 | |
|             ('SPACE', "Spaces", ""),
 | |
|             ('DIGIT', "Digits", ""),
 | |
|             ('PUNCT', "Punctuation", ""),
 | |
|         ),
 | |
|     )
 | |
| 
 | |
|     # type: 'STRIP'.
 | |
|     strip_part: EnumProperty(
 | |
|         name="Strip Part",
 | |
|         options={'ENUM_FLAG'},
 | |
|         items=(
 | |
|             ('START', "Start", ""),
 | |
|             ('END', "End", ""),
 | |
|         ),
 | |
|     )
 | |
| 
 | |
|     # type: 'REPLACE'.
 | |
|     replace_src: StringProperty(name="Find")
 | |
|     replace_dst: StringProperty(name="Replace")
 | |
|     replace_match_case: BoolProperty(name="Case Sensitive")
 | |
|     use_replace_regex_src: BoolProperty(
 | |
|         name="Regular Expression Find",
 | |
|         description="Use regular expressions to match text in the 'Find' field"
 | |
|     )
 | |
|     use_replace_regex_dst: BoolProperty(
 | |
|         name="Regular Expression Replace",
 | |
|         description="Use regular expression for the replacement text (supporting groups)"
 | |
|     )
 | |
| 
 | |
|     # type: 'CASE'.
 | |
|     case_method: EnumProperty(
 | |
|         name="Case",
 | |
|         items=(
 | |
|             ('UPPER', "Upper Case", ""),
 | |
|             ('LOWER', "Lower Case", ""),
 | |
|             ('TITLE', "Title Case", ""),
 | |
|         ),
 | |
|     )
 | |
| 
 | |
|     # Weak, add/remove as properties.
 | |
|     op_add: BoolProperty()
 | |
|     op_remove: BoolProperty()
 | |
| 
 | |
| 
 | |
| class WM_OT_batch_rename(Operator):
 | |
|     bl_idname = "wm.batch_rename"
 | |
|     bl_label = "Batch Rename"
 | |
| 
 | |
|     bl_options = {'UNDO'}
 | |
| 
 | |
|     data_type: EnumProperty(
 | |
|         name="Type",
 | |
|         items=(
 | |
|             ('OBJECT', "Objects", ""),
 | |
|             ('MATERIAL', "Materials", ""),
 | |
|             None,
 | |
|             # Enum identifiers are compared with 'object.type'.
 | |
|             ('MESH', "Meshes", ""),
 | |
|             ('CURVE', "Curves", ""),
 | |
|             ('META', "Meta Balls", ""),
 | |
|             ('ARMATURE', "Armatures", ""),
 | |
|             ('LATTICE', "Lattices", ""),
 | |
|             ('GPENCIL', "Grease Pencils", ""),
 | |
|             ('CAMERA', "Cameras", ""),
 | |
|             ('SPEAKER', "Speakers", ""),
 | |
|             ('LIGHT_PROBE', "Light Probes", ""),
 | |
|             None,
 | |
|             ('BONE', "Bones", ""),
 | |
|             ('NODE', "Nodes", ""),
 | |
|             ('SEQUENCE_STRIP', "Sequence Strips", ""),
 | |
|         ),
 | |
|         description="Type of data to rename",
 | |
|     )
 | |
| 
 | |
|     data_source: EnumProperty(
 | |
|         name="Source",
 | |
|         items=(
 | |
|             ('SELECT', "Selected", ""),
 | |
|             ('ALL', "All", ""),
 | |
|         ),
 | |
|     )
 | |
| 
 | |
|     actions: CollectionProperty(type=BatchRenameAction)
 | |
| 
 | |
|     @staticmethod
 | |
|     def _data_from_context(context, data_type, only_selected, check_context=False):
 | |
| 
 | |
|         mode = context.mode
 | |
|         scene = context.scene
 | |
|         space = context.space_data
 | |
|         space_type = None if (space is None) else space.type
 | |
| 
 | |
|         data = None
 | |
|         if space_type == 'SEQUENCE_EDITOR':
 | |
|             data_type_test = 'SEQUENCE_STRIP'
 | |
|             if check_context:
 | |
|                 return data_type_test
 | |
|             if data_type == data_type_test:
 | |
|                 data = (
 | |
|                     # TODO, we don't have access to seqbasep, this won't work when inside metas.
 | |
|                     [seq for seq in context.scene.sequence_editor.sequences_all if seq.select]
 | |
|                     if only_selected else
 | |
|                     context.scene.sequence_editor.sequences_all,
 | |
|                     "name",
 | |
|                     "Strip(s)",
 | |
|                 )
 | |
|         elif space_type == 'NODE_EDITOR':
 | |
|             data_type_test = 'NODE'
 | |
|             if check_context:
 | |
|                 return data_type_test
 | |
|             if data_type == data_type_test:
 | |
|                 data = (
 | |
|                     context.selected_nodes
 | |
|                     if only_selected else
 | |
|                     list(space.node_tree.nodes),
 | |
|                     "name",
 | |
|                     "Node(s)",
 | |
|                 )
 | |
|         else:
 | |
|             if mode == 'POSE' or (mode == 'WEIGHT_PAINT' and context.pose_object):
 | |
|                 data_type_test = 'BONE'
 | |
|                 if check_context:
 | |
|                     return data_type_test
 | |
|                 if data_type == data_type_test:
 | |
|                     data = (
 | |
|                         [pchan.bone for pchan in context.selected_pose_bones]
 | |
|                         if only_selected else
 | |
|                         [pbone.bone for ob in context.objects_in_mode_unique_data for pbone in ob.pose.bones],
 | |
|                         "name",
 | |
|                         "Bone(s)",
 | |
|                     )
 | |
|             elif mode == 'EDIT_ARMATURE':
 | |
|                 data_type_test = 'BONE'
 | |
|                 if check_context:
 | |
|                     return data_type_test
 | |
|                 if data_type == data_type_test:
 | |
|                     data = (
 | |
|                         context.selected_editable_bones
 | |
|                         if only_selected else
 | |
|                         [ebone for ob in context.objects_in_mode_unique_data for ebone in ob.data.edit_bones],
 | |
|                         "name",
 | |
|                         "Edit Bone(s)",
 | |
|                     )
 | |
| 
 | |
|         if check_context:
 | |
|             return 'OBJECT'
 | |
| 
 | |
|         object_data_type_attrs_map = {
 | |
|             'MESH': ("meshes", "Mesh(es)"),
 | |
|             'CURVE': ("curves", "Curve(s)"),
 | |
|             'META': ("metaballs", "MetaBall(s)"),
 | |
|             'ARMATURE': ("armatures", "Armature(s)"),
 | |
|             'LATTICE': ("lattices", "Lattice(s)"),
 | |
|             'GPENCIL': ("grease_pencils", "Grease Pencil(s)"),
 | |
|             'CAMERA': ("cameras", "Camera(s)"),
 | |
|             'SPEAKER': ("speakers", "Speaker(s)"),
 | |
|             'LIGHT_PROBE': ("light_probes", "LightProbe(s)"),
 | |
|         }
 | |
| 
 | |
|         # Finish with space types.
 | |
|         if data is None:
 | |
| 
 | |
|             if data_type == 'OBJECT':
 | |
|                 data = (
 | |
|                     context.selected_editable_objects
 | |
|                     if only_selected else
 | |
|                     [id for id in bpy.data.objects if id.library is None],
 | |
|                     "name",
 | |
|                     "Object(s)",
 | |
|                 )
 | |
|             elif data_type == 'MATERIAL':
 | |
|                 data = (
 | |
|                     tuple(set(
 | |
|                         slot.material
 | |
|                         for ob in context.selected_objects
 | |
|                         for slot in ob.material_slots
 | |
|                         if slot.material is not None
 | |
|                     ))
 | |
|                     if only_selected else
 | |
|                     [id for id in bpy.data.materials if id.library is None],
 | |
|                     "name",
 | |
|                     "Material(s)",
 | |
|                 )
 | |
|             elif data_type in object_data_type_attrs_map.keys():
 | |
|                 attr, descr = object_data_type_attrs_map[data_type]
 | |
|                 data = (
 | |
|                     tuple(set(
 | |
|                         id
 | |
|                         for ob in context.selected_objects
 | |
|                         if ob.type == data_type
 | |
|                         for id in (ob.data,)
 | |
|                         if id is not None and id.library is None
 | |
|                     ))
 | |
|                     if only_selected else
 | |
|                     [id for id in getattr(bpy.data, attr) if id.library is None],
 | |
|                     "name",
 | |
|                     descr,
 | |
|                 )
 | |
| 
 | |
|         return data
 | |
| 
 | |
|     @staticmethod
 | |
|     def _apply_actions(actions, name):
 | |
|         import string
 | |
|         import re
 | |
| 
 | |
|         for action in actions:
 | |
|             ty = action.type
 | |
|             if ty == 'SET':
 | |
|                 text = action.set_name
 | |
|                 method = action.set_method
 | |
|                 if method == 'NEW':
 | |
|                     name = text
 | |
|                 elif method == 'PREFIX':
 | |
|                     name = text + name
 | |
|                 elif method == 'SUFFIX':
 | |
|                     name = name + text
 | |
|                 else:
 | |
|                     assert(0)
 | |
| 
 | |
|             elif ty == 'STRIP':
 | |
|                 chars = action.strip_chars
 | |
|                 chars_strip = (
 | |
|                     "{:s}{:s}{:s}"
 | |
|                 ).format(
 | |
|                     string.punctuation if 'PUNCT' in chars else "",
 | |
|                     string.digits if 'DIGIT' in chars else "",
 | |
|                     " " if 'SPACE' in chars else "",
 | |
|                 )
 | |
|                 part = action.strip_part
 | |
|                 if 'START' in part:
 | |
|                     name = name.lstrip(chars_strip)
 | |
|                 if 'END' in part:
 | |
|                     name = name.rstrip(chars_strip)
 | |
| 
 | |
|             elif ty == 'REPLACE':
 | |
|                 if action.use_replace_regex_src:
 | |
|                     replace_src = action.replace_src
 | |
|                     if action.use_replace_regex_dst:
 | |
|                         replace_dst = action.replace_dst
 | |
|                     else:
 | |
|                         replace_dst = action.replace_dst.replace("\\", "\\\\")
 | |
|                 else:
 | |
|                     replace_src = re.escape(action.replace_src)
 | |
|                     replace_dst = action.replace_dst.replace("\\", "\\\\")
 | |
|                 name = re.sub(
 | |
|                     replace_src,
 | |
|                     replace_dst,
 | |
|                     name,
 | |
|                     flags=(
 | |
|                         0 if action.replace_match_case else
 | |
|                         re.IGNORECASE
 | |
|                     ),
 | |
|                 )
 | |
|             elif ty == 'CASE':
 | |
|                 method = action.case_method
 | |
|                 if method == 'UPPER':
 | |
|                     name = name.upper()
 | |
|                 elif method == 'LOWER':
 | |
|                     name = name.lower()
 | |
|                 elif method == 'TITLE':
 | |
|                     name = name.title()
 | |
|                 else:
 | |
|                     assert(0)
 | |
|             else:
 | |
|                 assert(0)
 | |
|         return name
 | |
| 
 | |
|     def _data_update(self, context):
 | |
|         only_selected = self.data_source == 'SELECT'
 | |
| 
 | |
|         self._data = self._data_from_context(context, self.data_type, only_selected)
 | |
|         if self._data is None:
 | |
|             self.data_type = self._data_from_context(context, None, False, check_context=True)
 | |
|             self._data = self._data_from_context(context, self.data_type, only_selected)
 | |
| 
 | |
|         self._data_source_prev = self.data_source
 | |
|         self._data_type_prev = self.data_type
 | |
| 
 | |
|     def draw(self, context):
 | |
|         import re
 | |
| 
 | |
|         layout = self.layout
 | |
| 
 | |
|         split = layout.split(factor=0.5)
 | |
|         split.label(text="Data Type:")
 | |
|         split.prop(self, "data_type", text="")
 | |
| 
 | |
|         split = layout.split(factor=0.5)
 | |
|         split.label(text="Rename {:d} {:s}:".format(len(self._data[0]), self._data[2]))
 | |
|         split.row().prop(self, "data_source", expand=True)
 | |
| 
 | |
|         for action in self.actions:
 | |
|             box = layout.box()
 | |
| 
 | |
|             row = box.row(align=True)
 | |
|             row.prop(action, "type", text="")
 | |
|             row.prop(action, "op_add", text="", icon='ADD')
 | |
|             row.prop(action, "op_remove", text="", icon='REMOVE')
 | |
| 
 | |
|             ty = action.type
 | |
|             if ty == 'SET':
 | |
|                 box.prop(action, "set_method")
 | |
|                 box.prop(action, "set_name")
 | |
|             elif ty == 'STRIP':
 | |
|                 box.row().prop(action, "strip_chars")
 | |
|                 box.row().prop(action, "strip_part")
 | |
|             elif ty == 'REPLACE':
 | |
| 
 | |
|                 row = box.row(align=True)
 | |
|                 re_error_src = None
 | |
|                 if action.use_replace_regex_src:
 | |
|                     try:
 | |
|                         re.compile(action.replace_src)
 | |
|                     except Exception as ex:
 | |
|                         re_error_src = str(ex)
 | |
|                         row.alert = True
 | |
|                 row.prop(action, "replace_src")
 | |
|                 row.prop(action, "use_replace_regex_src", text="", icon='SORTBYEXT')
 | |
|                 if re_error_src is not None:
 | |
|                     box.label(text=re_error_src)
 | |
| 
 | |
|                 re_error_dst = None
 | |
|                 row = box.row(align=True)
 | |
|                 if action.use_replace_regex_src:
 | |
|                     if action.use_replace_regex_dst:
 | |
|                         if re_error_src is None:
 | |
|                             try:
 | |
|                                 re.sub(action.replace_src, action.replace_dst, "")
 | |
|                             except Exception as ex:
 | |
|                                 re_error_dst = str(ex)
 | |
|                                 row.alert = True
 | |
| 
 | |
|                 row.prop(action, "replace_dst")
 | |
|                 rowsub = row.row(align=True)
 | |
|                 rowsub.active = action.use_replace_regex_src
 | |
|                 rowsub.prop(action, "use_replace_regex_dst", text="", icon='SORTBYEXT')
 | |
|                 if re_error_dst is not None:
 | |
|                     box.label(text=re_error_dst)
 | |
| 
 | |
|                 row = box.row()
 | |
|                 row.prop(action, "replace_match_case")
 | |
|             elif ty == 'CASE':
 | |
|                 box.row().prop(action, "case_method", expand=True)
 | |
| 
 | |
|     def check(self, context):
 | |
|         changed = False
 | |
|         for i, action in enumerate(self.actions):
 | |
|             if action.op_add:
 | |
|                 action.op_add = False
 | |
|                 self.actions.add()
 | |
|                 if i + 2 != len(self.actions):
 | |
|                     self.actions.move(len(self.actions) - 1, i + 1)
 | |
|                 changed = True
 | |
|                 break
 | |
|             if action.op_remove:
 | |
|                 action.op_remove = False
 | |
|                 if len(self.actions) > 1:
 | |
|                     self.actions.remove(i)
 | |
|                 changed = True
 | |
|                 break
 | |
| 
 | |
|         if (
 | |
|                 (self._data_source_prev != self.data_source) or
 | |
|                 (self._data_type_prev != self.data_type)
 | |
|         ):
 | |
|             self._data_update(context)
 | |
|             changed = True
 | |
| 
 | |
|         return changed
 | |
| 
 | |
|     def execute(self, context):
 | |
|         import re
 | |
| 
 | |
|         seq, attr, descr = self._data
 | |
| 
 | |
|         actions = self.actions
 | |
| 
 | |
|         # Sanitize actions.
 | |
|         for action in actions:
 | |
|             if action.use_replace_regex_src:
 | |
|                 try:
 | |
|                     re.compile(action.replace_src)
 | |
|                 except Exception as ex:
 | |
|                     self.report({'ERROR'}, "Invalid regular expression (find): " + str(ex))
 | |
|                     return {'CANCELLED'}
 | |
| 
 | |
|                 if action.use_replace_regex_dst:
 | |
|                     try:
 | |
|                         re.sub(action.replace_src, action.replace_dst, "")
 | |
|                     except Exception as ex:
 | |
|                         self.report({'ERROR'}, "Invalid regular expression (replace): " + str(ex))
 | |
|                         return {'CANCELLED'}
 | |
| 
 | |
|         total_len = 0
 | |
|         change_len = 0
 | |
|         for item in seq:
 | |
|             name_src = getattr(item, attr)
 | |
|             name_dst = self._apply_actions(actions, name_src)
 | |
|             if name_src != name_dst:
 | |
|                 setattr(item, attr, name_dst)
 | |
|                 change_len += 1
 | |
|             total_len += 1
 | |
| 
 | |
|         self.report({'INFO'}, "Renamed {:d} of {:d} {:s}".format(change_len, total_len, descr))
 | |
| 
 | |
|         return {'FINISHED'}
 | |
| 
 | |
|     def invoke(self, context, event):
 | |
| 
 | |
|         self._data_update(context)
 | |
| 
 | |
|         if not self.actions:
 | |
|             self.actions.add()
 | |
|         wm = context.window_manager
 | |
|         return wm.invoke_props_dialog(self, width=400)
 | |
| 
 | |
| 
 | |
| class WM_MT_splash(Menu):
 | |
|     bl_label = "Splash"
 | |
| 
 | |
|     def draw_setup(self, context):
 | |
|         wm = context.window_manager
 | |
|         # prefs = context.preferences
 | |
| 
 | |
|         layout = self.layout
 | |
| 
 | |
|         layout.operator_context = 'EXEC_DEFAULT'
 | |
| 
 | |
|         layout.label(text="Quick Setup")
 | |
| 
 | |
|         split = layout.split(factor=0.25)
 | |
|         split.label()
 | |
|         split = split.split(factor=2.0 / 3.0)
 | |
| 
 | |
|         col = split.column()
 | |
| 
 | |
|         col.label()
 | |
| 
 | |
|         sub = col.split(factor=0.35)
 | |
|         row = sub.row()
 | |
|         row.alignment = 'RIGHT'
 | |
|         row.label(text="Shortcuts")
 | |
|         text = bpy.path.display_name(wm.keyconfigs.active.name)
 | |
|         if not text:
 | |
|             text = "Blender"
 | |
|         sub.menu("USERPREF_MT_keyconfigs", text=text)
 | |
| 
 | |
|         kc = wm.keyconfigs.active
 | |
|         kc_prefs = kc.preferences
 | |
|         has_select_mouse = hasattr(kc_prefs, "select_mouse")
 | |
|         if has_select_mouse:
 | |
|             sub = col.split(factor=0.35)
 | |
|             row = sub.row()
 | |
|             row.alignment = 'RIGHT'
 | |
|             row.label(text="Select With")
 | |
|             sub.row().prop(kc_prefs, "select_mouse", expand=True)
 | |
|             has_select_mouse = True
 | |
| 
 | |
|         has_spacebar_action = hasattr(kc_prefs, "spacebar_action")
 | |
|         if has_spacebar_action:
 | |
|             sub = col.split(factor=0.35)
 | |
|             row = sub.row()
 | |
|             row.alignment = 'RIGHT'
 | |
|             row.label(text="Spacebar")
 | |
|             sub.row().prop(kc_prefs, "spacebar_action", expand=True)
 | |
|             has_select_mouse = True
 | |
| 
 | |
|         col.separator()
 | |
| 
 | |
|         sub = col.split(factor=0.35)
 | |
|         row = sub.row()
 | |
|         row.alignment = 'RIGHT'
 | |
|         row.label(text="Theme")
 | |
|         label = bpy.types.USERPREF_MT_interface_theme_presets.bl_label
 | |
|         if label == "Presets":
 | |
|             label = "Blender Dark"
 | |
|         sub.menu("USERPREF_MT_interface_theme_presets", text=label)
 | |
| 
 | |
|         # We need to make switching to a language easier first
 | |
|         #sub = col.split(factor=0.35)
 | |
|         #row = sub.row()
 | |
|         #row.alignment = 'RIGHT'
 | |
|         # row.label(text="Language:")
 | |
|         #prefs = context.preferences
 | |
|         #sub.prop(prefs.system, "language", text="")
 | |
| 
 | |
|         # Keep height constant
 | |
|         if not has_select_mouse:
 | |
|             col.label()
 | |
|         if not has_spacebar_action:
 | |
|             col.label()
 | |
| 
 | |
|         layout.label()
 | |
| 
 | |
|         row = layout.row()
 | |
| 
 | |
|         sub = row.row()
 | |
|         if bpy.types.PREFERENCES_OT_copy_prev.poll(context):
 | |
|             old_version = bpy.types.PREFERENCES_OT_copy_prev.previous_version()
 | |
|             sub.operator("preferences.copy_prev", text="Load %d.%d Settings" % old_version)
 | |
|             sub.operator("wm.save_userpref", text="Save New Settings")
 | |
|         else:
 | |
|             sub.label()
 | |
|             sub.label()
 | |
|             sub.operator("wm.save_userpref", text="Next")
 | |
| 
 | |
|         layout.separator()
 | |
|         layout.separator()
 | |
| 
 | |
|     def draw(self, context):
 | |
|         # Draw setup screen if no preferences have been saved yet.
 | |
|         import os
 | |
| 
 | |
|         userconfig_path = bpy.utils.user_resource('CONFIG')
 | |
|         userdef_path = os.path.join(userconfig_path, "userpref.blend")
 | |
| 
 | |
|         if not os.path.isfile(userdef_path):
 | |
|             self.draw_setup(context)
 | |
|             return
 | |
| 
 | |
|         # Pass
 | |
|         layout = self.layout
 | |
|         layout.operator_context = 'EXEC_DEFAULT'
 | |
|         layout.emboss = 'PULLDOWN_MENU'
 | |
| 
 | |
|         split = layout.split()
 | |
| 
 | |
|         # Templates
 | |
|         col1 = split.column()
 | |
|         col1.label(text="New File")
 | |
| 
 | |
|         bpy.types.TOPBAR_MT_file_new.draw_ex(col1, context, use_splash=True)
 | |
| 
 | |
|         # Recent
 | |
|         col2 = split.column()
 | |
|         col2_title = col2.row()
 | |
| 
 | |
|         found_recent = col2.template_recent_files()
 | |
| 
 | |
|         if found_recent:
 | |
|             col2_title.label(text="Recent Files")
 | |
|         else:
 | |
| 
 | |
|             # Links if no recent files
 | |
|             col2_title.label(text="Getting Started")
 | |
| 
 | |
|             col2.operator("wm.url_open_preset", text="Manual", icon='URL').type = 'MANUAL'
 | |
|             col2.operator("wm.url_open_preset", text="Blender Website", icon='URL').type = 'BLENDER'
 | |
|             col2.operator("wm.url_open_preset", text="Credits", icon='URL').type = 'CREDITS'
 | |
| 
 | |
|         layout.separator()
 | |
| 
 | |
|         split = layout.split()
 | |
| 
 | |
|         col1 = split.column()
 | |
|         sub = col1.row()
 | |
|         sub.operator_context = 'INVOKE_DEFAULT'
 | |
|         sub.operator("wm.open_mainfile", text="Open...", icon='FILE_FOLDER')
 | |
|         col1.operator("wm.recover_last_session", icon='RECOVER_LAST')
 | |
| 
 | |
|         col2 = split.column()
 | |
| 
 | |
|         col2.operator("wm.url_open_preset", text="Release Notes", icon='URL').type = 'RELEASE_NOTES'
 | |
|         col2.operator("wm.url_open_preset", text="Development Fund", icon='FUND').type = 'FUND'
 | |
| 
 | |
|         layout.separator()
 | |
|         layout.separator()
 | |
| 
 | |
| 
 | |
| class WM_OT_drop_blend_file(Operator):
 | |
|     bl_idname = "wm.drop_blend_file"
 | |
|     bl_label = "Handle dropped .blend file"
 | |
|     bl_options = {'INTERNAL'}
 | |
| 
 | |
|     filepath: StringProperty()
 | |
| 
 | |
|     def invoke(self, context, _event):
 | |
|         context.window_manager.popup_menu(self.draw_menu, title=bpy.path.basename(self.filepath), icon='QUESTION')
 | |
|         return {'FINISHED'}
 | |
| 
 | |
|     def draw_menu(self, menu, _context):
 | |
|         layout = menu.layout
 | |
| 
 | |
|         col = layout.column()
 | |
|         col.operator_context = 'INVOKE_DEFAULT'
 | |
|         props = col.operator("wm.open_mainfile", text="Open", icon='FILE_FOLDER')
 | |
|         props.filepath = self.filepath
 | |
|         props.display_file_selector = False
 | |
| 
 | |
|         layout.separator()
 | |
|         col = layout.column()
 | |
|         col.operator_context = 'INVOKE_DEFAULT'
 | |
|         col.operator("wm.link", text="Link...", icon='LINK_BLEND').filepath = self.filepath
 | |
|         col.operator("wm.append", text="Append...", icon='APPEND_BLEND').filepath = self.filepath
 | |
| 
 | |
| 
 | |
| classes = (
 | |
|     WM_OT_context_collection_boolean_set,
 | |
|     WM_OT_context_cycle_array,
 | |
|     WM_OT_context_cycle_enum,
 | |
|     WM_OT_context_cycle_int,
 | |
|     WM_OT_context_menu_enum,
 | |
|     WM_OT_context_modal_mouse,
 | |
|     WM_OT_context_pie_enum,
 | |
|     WM_OT_context_scale_float,
 | |
|     WM_OT_context_scale_int,
 | |
|     WM_OT_context_set_boolean,
 | |
|     WM_OT_context_set_enum,
 | |
|     WM_OT_context_set_float,
 | |
|     WM_OT_context_set_id,
 | |
|     WM_OT_context_set_int,
 | |
|     WM_OT_context_set_string,
 | |
|     WM_OT_context_set_value,
 | |
|     WM_OT_context_toggle,
 | |
|     WM_OT_context_toggle_enum,
 | |
|     WM_OT_doc_view,
 | |
|     WM_OT_doc_view_manual,
 | |
|     WM_OT_drop_blend_file,
 | |
|     WM_OT_operator_cheat_sheet,
 | |
|     WM_OT_operator_pie_enum,
 | |
|     WM_OT_path_open,
 | |
|     WM_OT_properties_add,
 | |
|     WM_OT_properties_context_change,
 | |
|     WM_OT_properties_edit,
 | |
|     WM_OT_properties_remove,
 | |
|     WM_OT_sysinfo,
 | |
|     WM_OT_owner_disable,
 | |
|     WM_OT_owner_enable,
 | |
|     WM_OT_url_open,
 | |
|     WM_OT_url_open_preset,
 | |
|     WM_OT_tool_set_by_id,
 | |
|     WM_OT_tool_set_by_index,
 | |
|     WM_OT_toolbar,
 | |
|     WM_OT_toolbar_fallback_pie,
 | |
|     WM_OT_toolbar_prompt,
 | |
|     BatchRenameAction,
 | |
|     WM_OT_batch_rename,
 | |
|     WM_MT_splash,
 | |
| )
 |