1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
22 from bpy.types import (
25 OperatorFileListElement
27 from bpy.props import (
36 from bpy.app.translations import pgettext_tip as tip_
39 rna_path_prop = StringProperty(
40 name="Context Attributes",
41 description="RNA context string",
45 rna_reverse_prop = BoolProperty(
47 description="Cycle backwards",
51 rna_wrap_prop = BoolProperty(
53 description="Wrap back to the first/last values",
57 rna_relative_prop = BoolProperty(
59 description="Apply relative to the current value (delta)",
63 rna_space_type_prop = EnumProperty(
66 (e.identifier, e.name, "", e. value)
67 for e in bpy.types.Space.bl_rna.properties["type"].enum_items
73 def context_path_validate(context, data_path):
75 value = eval("context.%s" % data_path) if data_path else Ellipsis
76 except AttributeError as ex:
77 if str(ex).startswith("'NoneType'"):
78 # One of the items in the rna path is None, just ignore this
81 # We have a real error in the rna path, don't ignore that
87 def operator_value_is_undo(value):
88 if value in {None, Ellipsis}:
91 # typical properties or objects
92 id_data = getattr(value, "id_data", Ellipsis)
96 elif id_data is Ellipsis:
97 # handle mathutils types
98 id_data = getattr(getattr(value, "owner", None), "id_data", None)
103 # return True if its a non window ID type
104 return (isinstance(id_data, bpy.types.ID) and
105 (not isinstance(id_data, (bpy.types.WindowManager,
111 def operator_path_is_undo(context, data_path):
112 # note that if we have data paths that use strings this could fail
113 # luckily we don't do this!
115 # When we can't find the data owner assume no undo is needed.
116 data_path_head = data_path.rpartition(".")[0]
118 if not data_path_head:
121 value = context_path_validate(context, data_path_head)
123 return operator_value_is_undo(value)
126 def operator_path_undo_return(context, data_path):
127 return {'FINISHED'} if operator_path_is_undo(context, data_path) else {'CANCELLED'}
130 def operator_value_undo_return(value):
131 return {'FINISHED'} if operator_value_is_undo(value) else {'CANCELLED'}
134 def execute_context_assign(self, context):
135 data_path = self.data_path
136 if context_path_validate(context, data_path) is Ellipsis:
137 return {'PASS_THROUGH'}
139 if getattr(self, "relative", False):
140 exec("context.%s += self.value" % data_path)
142 exec("context.%s = self.value" % data_path)
144 return operator_path_undo_return(context, data_path)
147 def module_filesystem_remove(path_base, module_name):
149 module_name = os.path.splitext(module_name)[0]
150 for f in os.listdir(path_base):
151 f_base = os.path.splitext(f)[0]
152 if f_base == module_name:
153 f_full = os.path.join(path_base, f)
155 if os.path.isdir(f_full):
161 class BRUSH_OT_active_index_set(Operator):
162 """Set active sculpt/paint brush from it's number"""
163 bl_idname = "brush.active_index_set"
164 bl_label = "Set Brush Number"
166 mode: StringProperty(
168 description="Paint mode to set brush for",
173 description="Brush number",
177 "sculpt": "use_paint_sculpt",
178 "vertex_paint": "use_paint_vertex",
179 "weight_paint": "use_paint_weight",
180 "image_paint": "use_paint_image",
183 def execute(self, context):
184 attr = self._attr_dict.get(self.mode)
188 toolsettings = context.tool_settings
189 for i, brush in enumerate((cur for cur in bpy.data.brushes if getattr(cur, attr))):
191 getattr(toolsettings, self.mode).brush = brush
197 class WM_OT_context_set_boolean(Operator):
198 """Set a context value"""
199 bl_idname = "wm.context_set_boolean"
200 bl_label = "Context Set Boolean"
201 bl_options = {'UNDO', 'INTERNAL'}
203 data_path: rna_path_prop
206 description="Assignment value",
210 execute = execute_context_assign
213 class WM_OT_context_set_int(Operator): # same as enum
214 """Set a context value"""
215 bl_idname = "wm.context_set_int"
216 bl_label = "Context Set"
217 bl_options = {'UNDO', 'INTERNAL'}
219 data_path: rna_path_prop
222 description="Assign value",
225 relative: rna_relative_prop
227 execute = execute_context_assign
230 class WM_OT_context_scale_float(Operator):
231 """Scale a float context value"""
232 bl_idname = "wm.context_scale_float"
233 bl_label = "Context Scale Float"
234 bl_options = {'UNDO', 'INTERNAL'}
236 data_path: rna_path_prop
237 value: FloatProperty(
239 description="Assign value",
243 def execute(self, context):
244 data_path = self.data_path
245 if context_path_validate(context, data_path) is Ellipsis:
246 return {'PASS_THROUGH'}
250 if value == 1.0: # nothing to do
253 exec("context.%s *= value" % data_path)
255 return operator_path_undo_return(context, data_path)
258 class WM_OT_context_scale_int(Operator):
259 """Scale an int context value"""
260 bl_idname = "wm.context_scale_int"
261 bl_label = "Context Scale Int"
262 bl_options = {'UNDO', 'INTERNAL'}
264 data_path: rna_path_prop
265 value: FloatProperty(
267 description="Assign value",
270 always_step: BoolProperty(
272 description="Always adjust the value by a minimum of 1 when 'value' is not 1.0",
276 def execute(self, context):
277 data_path = self.data_path
278 if context_path_validate(context, data_path) is Ellipsis:
279 return {'PASS_THROUGH'}
283 if value == 1.0: # nothing to do
286 if getattr(self, "always_step", False):
293 exec("context.%s = %s(round(context.%s * value), context.%s + %s)" %
294 (data_path, func, data_path, data_path, add))
296 exec("context.%s *= value" % data_path)
298 return operator_path_undo_return(context, data_path)
301 class WM_OT_context_set_float(Operator): # same as enum
302 """Set a context value"""
303 bl_idname = "wm.context_set_float"
304 bl_label = "Context Set Float"
305 bl_options = {'UNDO', 'INTERNAL'}
307 data_path: rna_path_prop
308 value: FloatProperty(
310 description="Assignment value",
313 relative: rna_relative_prop
315 execute = execute_context_assign
318 class WM_OT_context_set_string(Operator): # same as enum
319 """Set a context value"""
320 bl_idname = "wm.context_set_string"
321 bl_label = "Context Set String"
322 bl_options = {'UNDO', 'INTERNAL'}
324 data_path: rna_path_prop
325 value: StringProperty(
327 description="Assign value",
331 execute = execute_context_assign
334 class WM_OT_context_set_enum(Operator):
335 """Set a context value"""
336 bl_idname = "wm.context_set_enum"
337 bl_label = "Context Set Enum"
338 bl_options = {'UNDO', 'INTERNAL'}
340 data_path: rna_path_prop
341 value: StringProperty(
343 description="Assignment value (as a string)",
347 execute = execute_context_assign
350 class WM_OT_context_set_value(Operator):
351 """Set a context value"""
352 bl_idname = "wm.context_set_value"
353 bl_label = "Context Set Value"
354 bl_options = {'UNDO', 'INTERNAL'}
356 data_path: rna_path_prop
357 value: StringProperty(
359 description="Assignment value (as a string)",
363 def execute(self, context):
364 data_path = self.data_path
365 if context_path_validate(context, data_path) is Ellipsis:
366 return {'PASS_THROUGH'}
367 exec("context.%s = %s" % (data_path, self.value))
368 return operator_path_undo_return(context, data_path)
371 class WM_OT_context_toggle(Operator):
372 """Toggle a context value"""
373 bl_idname = "wm.context_toggle"
374 bl_label = "Context Toggle"
375 bl_options = {'UNDO', 'INTERNAL'}
377 data_path: rna_path_prop
379 def execute(self, context):
380 data_path = self.data_path
382 if context_path_validate(context, data_path) is Ellipsis:
383 return {'PASS_THROUGH'}
385 exec("context.%s = not (context.%s)" % (data_path, data_path))
387 return operator_path_undo_return(context, data_path)
390 class WM_OT_context_toggle_enum(Operator):
391 """Toggle a context value"""
392 bl_idname = "wm.context_toggle_enum"
393 bl_label = "Context Toggle Values"
394 bl_options = {'UNDO', 'INTERNAL'}
396 data_path: rna_path_prop
397 value_1: StringProperty(
399 description="Toggle enum",
402 value_2: StringProperty(
404 description="Toggle enum",
408 def execute(self, context):
409 data_path = self.data_path
411 if context_path_validate(context, data_path) is Ellipsis:
412 return {'PASS_THROUGH'}
414 # failing silently is not ideal, but we don't want errors for shortcut
415 # keys that some values that are only available in a particular context
417 exec("context.%s = ('%s', '%s')[context.%s != '%s']" %
418 (data_path, self.value_1,
419 self.value_2, data_path,
423 return {'PASS_THROUGH'}
425 return operator_path_undo_return(context, data_path)
428 class WM_OT_context_cycle_int(Operator):
429 """Set a context value (useful for cycling active material, """ \
430 """vertex keys, groups, etc.)"""
431 bl_idname = "wm.context_cycle_int"
432 bl_label = "Context Int Cycle"
433 bl_options = {'UNDO', 'INTERNAL'}
435 data_path: rna_path_prop
436 reverse: rna_reverse_prop
439 def execute(self, context):
440 data_path = self.data_path
441 value = context_path_validate(context, data_path)
442 if value is Ellipsis:
443 return {'PASS_THROUGH'}
450 exec("context.%s = value" % data_path)
453 if value != eval("context.%s" % data_path):
454 # relies on rna clamping integers out of the range
456 value = (1 << 31) - 1
460 exec("context.%s = value" % data_path)
462 return operator_path_undo_return(context, data_path)
465 class WM_OT_context_cycle_enum(Operator):
466 """Toggle a context value"""
467 bl_idname = "wm.context_cycle_enum"
468 bl_label = "Context Enum Cycle"
469 bl_options = {'UNDO', 'INTERNAL'}
471 data_path: rna_path_prop
472 reverse: rna_reverse_prop
475 def execute(self, context):
476 data_path = self.data_path
477 value = context_path_validate(context, data_path)
478 if value is Ellipsis:
479 return {'PASS_THROUGH'}
483 # Have to get rna enum values
484 rna_struct_str, rna_prop_str = data_path.rsplit('.', 1)
485 i = rna_prop_str.find('[')
487 # just in case we get "context.foo.bar[0]"
489 rna_prop_str = rna_prop_str[0:i]
491 rna_struct = eval("context.%s.rna_type" % rna_struct_str)
493 rna_prop = rna_struct.properties[rna_prop_str]
495 if type(rna_prop) != bpy.types.EnumProperty:
496 raise Exception("expected an enum property")
498 enums = rna_struct.properties[rna_prop_str].enum_items.keys()
499 orig_index = enums.index(orig_value)
501 # Have the info we need, advance to the next item.
503 # When wrap's disabled we may set the value to its self,
504 # this is done to ensure update callbacks run.
507 advance_enum = enums[-1] if self.wrap else enums[0]
509 advance_enum = enums[orig_index - 1]
511 if orig_index == len(enums) - 1:
512 advance_enum = enums[0] if self.wrap else enums[-1]
514 advance_enum = enums[orig_index + 1]
517 exec("context.%s = advance_enum" % data_path)
518 return operator_path_undo_return(context, data_path)
521 class WM_OT_context_cycle_array(Operator):
522 """Set a context array value """ \
523 """(useful for cycling the active mesh edit mode)"""
524 bl_idname = "wm.context_cycle_array"
525 bl_label = "Context Array Cycle"
526 bl_options = {'UNDO', 'INTERNAL'}
528 data_path: rna_path_prop
529 reverse: rna_reverse_prop
531 def execute(self, context):
532 data_path = self.data_path
533 value = context_path_validate(context, data_path)
534 if value is Ellipsis:
535 return {'PASS_THROUGH'}
539 array.insert(0, array.pop())
541 array.append(array.pop(0))
544 exec("context.%s = cycle(context.%s[:])" % (data_path, data_path))
546 return operator_path_undo_return(context, data_path)
549 class WM_OT_context_menu_enum(Operator):
550 bl_idname = "wm.context_menu_enum"
551 bl_label = "Context Enum Menu"
552 bl_options = {'UNDO', 'INTERNAL'}
554 data_path: rna_path_prop
556 def execute(self, context):
557 data_path = self.data_path
558 value = context_path_validate(context, data_path)
560 if value is Ellipsis:
561 return {'PASS_THROUGH'}
563 base_path, prop_string = data_path.rsplit(".", 1)
564 value_base = context_path_validate(context, base_path)
565 prop = value_base.bl_rna.properties[prop_string]
567 def draw_cb(self, context):
569 layout.prop(value_base, prop_string, expand=True)
571 context.window_manager.popup_menu(draw_func=draw_cb, title=prop.name, icon=prop.icon)
576 class WM_OT_context_pie_enum(Operator):
577 bl_idname = "wm.context_pie_enum"
578 bl_label = "Context Enum Pie"
579 bl_options = {'UNDO', 'INTERNAL'}
581 data_path: rna_path_prop
583 def invoke(self, context, event):
584 wm = context.window_manager
585 data_path = self.data_path
586 value = context_path_validate(context, data_path)
588 if value is Ellipsis:
589 return {'PASS_THROUGH'}
591 base_path, prop_string = data_path.rsplit(".", 1)
592 value_base = context_path_validate(context, base_path)
593 prop = value_base.bl_rna.properties[prop_string]
595 def draw_cb(self, context):
597 layout.prop(value_base, prop_string, expand=True)
599 wm.popup_menu_pie(draw_func=draw_cb, title=prop.name, icon=prop.icon, event=event)
604 class WM_OT_operator_pie_enum(Operator):
605 bl_idname = "wm.operator_pie_enum"
606 bl_label = "Operator Enum Pie"
607 bl_options = {'UNDO', 'INTERNAL'}
609 data_path: StringProperty(
611 description="Operator name (in python as string)",
614 prop_string: StringProperty(
616 description="Property name (as a string)",
620 def invoke(self, context, event):
621 wm = context.window_manager
623 data_path = self.data_path
624 prop_string = self.prop_string
626 # same as eval("bpy.ops." + data_path)
627 op_mod_str, ob_id_str = data_path.split(".", 1)
628 op = getattr(getattr(bpy.ops, op_mod_str), ob_id_str)
629 del op_mod_str, ob_id_str
632 op_rna = op.get_rna_type()
634 self.report({'ERROR'}, "Operator not found: bpy.ops.%s" % data_path)
637 def draw_cb(self, context):
639 pie = layout.menu_pie()
640 pie.operator_enum(data_path, prop_string)
642 wm.popup_menu_pie(draw_func=draw_cb, title=op_rna.name, event=event)
647 class WM_OT_context_set_id(Operator):
648 """Set a context value to an ID data-block"""
649 bl_idname = "wm.context_set_id"
650 bl_label = "Set Library ID"
651 bl_options = {'UNDO', 'INTERNAL'}
653 data_path: rna_path_prop
654 value: StringProperty(
656 description="Assign value",
660 def execute(self, context):
662 data_path = self.data_path
664 # match the pointer type from the target property to bpy.data.*
665 # so we lookup the correct list.
666 data_path_base, data_path_prop = data_path.rsplit(".", 1)
667 data_prop_rna = eval("context.%s" % data_path_base).rna_type.properties[data_path_prop]
668 data_prop_rna_type = data_prop_rna.fixed_type
672 for prop in bpy.data.rna_type.properties:
673 if prop.rna_type.identifier == "CollectionProperty":
674 if prop.fixed_type == data_prop_rna_type:
675 id_iter = prop.identifier
679 value_id = getattr(bpy.data, id_iter).get(value)
680 exec("context.%s = value_id" % data_path)
682 return operator_path_undo_return(context, data_path)
685 doc_id = StringProperty(
691 data_path_iter = StringProperty(
692 description="The data path relative to the context, must point to an iterable")
694 data_path_item = StringProperty(
695 description="The data path from each iterable to the value (int or float)")
698 class WM_OT_context_collection_boolean_set(Operator):
699 """Set boolean values for a collection of items"""
700 bl_idname = "wm.context_collection_boolean_set"
701 bl_label = "Context Collection Boolean Set"
702 bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
704 data_path_iter: data_path_iter
705 data_path_item: data_path_item
709 items=(('TOGGLE', "Toggle", ""),
710 ('ENABLE', "Enable", ""),
711 ('DISABLE', "Disable", ""),
715 def execute(self, context):
716 data_path_iter = self.data_path_iter
717 data_path_item = self.data_path_item
719 items = list(getattr(context, data_path_iter))
724 value_orig = eval("item." + data_path_item)
728 if value_orig is True:
730 elif value_orig is False:
733 self.report({'WARNING'}, "Non boolean value found: %s[ ].%s" %
734 (data_path_iter, data_path_item))
737 items_ok.append(item)
739 # avoid undo push when nothing to do
743 if self.type == 'ENABLE':
745 elif self.type == 'DISABLE':
750 exec_str = "item.%s = %s" % (data_path_item, is_set)
751 for item in items_ok:
754 return operator_value_undo_return(item)
757 class WM_OT_context_modal_mouse(Operator):
758 """Adjust arbitrary values with mouse input"""
759 bl_idname = "wm.context_modal_mouse"
760 bl_label = "Context Modal Mouse"
761 bl_options = {'GRAB_CURSOR', 'BLOCKING', 'UNDO', 'INTERNAL'}
763 data_path_iter: data_path_iter
764 data_path_item: data_path_item
765 header_text: StringProperty(
767 description="Text to display in header during scale",
770 input_scale: FloatProperty(
771 description="Scale the mouse movement by this value before applying the delta",
774 invert: BoolProperty(
775 description="Invert the mouse input",
778 initial_x: IntProperty(options={'HIDDEN'})
780 def _values_store(self, context):
781 data_path_iter = self.data_path_iter
782 data_path_item = self.data_path_item
784 self._values = values = {}
786 for item in getattr(context, data_path_iter):
788 value_orig = eval("item." + data_path_item)
792 # check this can be set, maybe this is library data.
794 exec("item.%s = %s" % (data_path_item, value_orig))
798 values[item] = value_orig
800 def _values_delta(self, delta):
801 delta *= self.input_scale
805 data_path_item = self.data_path_item
806 for item, value_orig in self._values.items():
807 if type(value_orig) == int:
808 exec("item.%s = int(%d)" % (data_path_item, round(value_orig + delta)))
810 exec("item.%s = %f" % (data_path_item, value_orig + delta))
812 def _values_restore(self):
813 data_path_item = self.data_path_item
814 for item, value_orig in self._values.items():
815 exec("item.%s = %s" % (data_path_item, value_orig))
819 def _values_clear(self):
822 def modal(self, context, event):
823 event_type = event.type
825 if event_type == 'MOUSEMOVE':
826 delta = event.mouse_x - self.initial_x
827 self._values_delta(delta)
828 header_text = self.header_text
830 if len(self._values) == 1:
831 (item, ) = self._values.keys()
832 header_text = header_text % eval("item.%s" % self.data_path_item)
834 header_text = (self.header_text % delta) + " (delta)"
835 context.area.header_text_set(header_text)
837 elif 'LEFTMOUSE' == event_type:
838 item = next(iter(self._values.keys()))
840 context.area.header_text_set(None)
841 return operator_value_undo_return(item)
843 elif event_type in {'RIGHTMOUSE', 'ESC'}:
844 self._values_restore()
845 context.area.header_text_set(None)
848 return {'RUNNING_MODAL'}
850 def invoke(self, context, event):
851 self._values_store(context)
854 self.report({'WARNING'}, "Nothing to operate on: %s[ ].%s" %
855 (self.data_path_iter, self.data_path_item))
859 self.initial_x = event.mouse_x
861 context.window_manager.modal_handler_add(self)
862 return {'RUNNING_MODAL'}
865 class WM_OT_url_open(Operator):
866 """Open a website in the web-browser"""
867 bl_idname = "wm.url_open"
869 bl_options = {'INTERNAL'}
873 description="URL to open",
876 def execute(self, context):
878 webbrowser.open(self.url)
882 class WM_OT_path_open(Operator):
883 """Open a path in a file browser"""
884 bl_idname = "wm.path_open"
886 bl_options = {'INTERNAL'}
888 filepath: StringProperty(
890 options={'SKIP_SAVE'},
893 def execute(self, context):
898 filepath = self.filepath
901 self.report({'ERROR'}, "File path was not set")
904 filepath = bpy.path.abspath(filepath)
905 filepath = os.path.normpath(filepath)
907 if not os.path.exists(filepath):
908 self.report({'ERROR'}, "File '%s' not found" % filepath)
911 if sys.platform[:3] == "win":
912 os.startfile(filepath)
913 elif sys.platform == "darwin":
914 subprocess.check_call(["open", filepath])
917 subprocess.check_call(["xdg-open", filepath])
919 # xdg-open *should* be supported by recent Gnome, KDE, Xfce
921 traceback.print_exc()
926 def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""):
928 def operator_exists_pair(a, b):
929 # Not fast, this is only for docs.
930 return b in dir(getattr(bpy.ops, a))
932 def operator_exists_single(a):
933 a, b = a.partition("_OT_")[::2]
934 return operator_exists_pair(a.lower(), b)
936 id_split = doc_id.split(".")
939 if len(id_split) == 1: # rna, class
941 url = "%s/bpy.types.%s.html" % (url_prefix, id_split[0])
943 rna = "bpy.types.%s" % id_split[0]
945 elif len(id_split) == 2: # rna, class.prop
946 class_name, class_prop = id_split
948 # an operator (common case - just button referencing an op)
949 if operator_exists_pair(class_name, class_prop):
952 "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
953 (url_prefix, class_name, class_name, class_prop)
956 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
957 elif operator_exists_single(class_name):
958 # note: ignore the prop name since we don't have a way to link into it
959 class_name, class_prop = class_name.split("_OT_", 1)
960 class_name = class_name.lower()
963 "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
964 (url_prefix, class_name, class_name, class_prop)
967 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
969 # an RNA setting, common case
970 rna_class = getattr(bpy.types, class_name)
972 # detect if this is a inherited member and use that name instead
973 rna_parent = rna_class.bl_rna
974 rna_prop = rna_parent.properties.get(class_prop)
976 rna_parent = rna_parent.base
977 while rna_parent and rna_prop == rna_parent.properties.get(class_prop):
978 class_name = rna_parent.identifier
979 rna_parent = rna_parent.base
983 "%s/bpy.types.%s.html#bpy.types.%s.%s" %
984 (url_prefix, class_name, class_name, class_prop)
987 rna = "bpy.types.%s.%s" % (class_name, class_prop)
989 # We assume this is custom property, only try to generate generic url/rna_id...
991 url = ("%s/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items" % (url_prefix,))
993 rna = "bpy.types.bpy_struct"
995 return url if do_url else rna
998 class WM_OT_doc_view_manual(Operator):
999 """Load online manual"""
1000 bl_idname = "wm.doc_view_manual"
1001 bl_label = "View Manual"
1006 def _find_reference(rna_id, url_mapping, verbose=True):
1008 print("online manual check for: '%s'... " % rna_id)
1009 from fnmatch import fnmatchcase
1010 # XXX, for some reason all RNA ID's are stored lowercase
1011 # Adding case into all ID's isn't worth the hassle so force lowercase.
1012 rna_id = rna_id.lower()
1013 for pattern, url_suffix in url_mapping:
1014 if fnmatchcase(rna_id, pattern):
1016 print(" match found: '%s' --> '%s'" % (pattern, url_suffix))
1019 print("match not found")
1023 def _lookup_rna_url(rna_id, verbose=True):
1024 for prefix, url_manual_mapping in bpy.utils.manual_map():
1025 rna_ref = WM_OT_doc_view_manual._find_reference(rna_id, url_manual_mapping, verbose=verbose)
1026 if rna_ref is not None:
1027 url = prefix + rna_ref
1030 def execute(self, context):
1031 rna_id = _wm_doc_get_id(self.doc_id, do_url=False)
1033 return {'PASS_THROUGH'}
1035 url = self._lookup_rna_url(rna_id)
1040 "No reference available %r, "
1041 "Update info in 'rna_manual_reference.py' "
1042 "or callback to bpy.utils.manual_map()" %
1045 return {'CANCELLED'}
1048 webbrowser.open(url)
1052 class WM_OT_doc_view(Operator):
1053 """Load online reference docs"""
1054 bl_idname = "wm.doc_view"
1055 bl_label = "View Documentation"
1058 if bpy.app.version_cycle == "release":
1059 _prefix = ("https://docs.blender.org/api/blender_python_api_current")
1061 _prefix = ("https://docs.blender.org/api/blender_python_api_master")
1063 def execute(self, context):
1064 url = _wm_doc_get_id(self.doc_id, do_url=True, url_prefix=self._prefix)
1066 return {'PASS_THROUGH'}
1069 webbrowser.open(url)
1074 rna_path = StringProperty(
1075 name="Property Edit",
1076 description="Property data_path edit",
1081 rna_value = StringProperty(
1082 name="Property Value",
1083 description="Property value edit",
1087 rna_property = StringProperty(
1088 name="Property Name",
1089 description="Property name edit",
1093 rna_min = FloatProperty(
1099 rna_max = FloatProperty(
1105 rna_use_soft_limits = BoolProperty(
1106 name="Use Soft Limits",
1109 rna_is_overridable_static = BoolProperty(
1110 name="Is Statically Overridable",
1115 class WM_OT_properties_edit(Operator):
1116 bl_idname = "wm.properties_edit"
1117 bl_label = "Edit Property"
1118 # register only because invoke_props_popup requires.
1119 bl_options = {'REGISTER', 'INTERNAL'}
1122 property: rna_property
1126 use_soft_limits: rna_use_soft_limits
1127 is_overridable_static: rna_is_overridable_static
1130 description: StringProperty(
1134 def _cmp_props_get(self):
1135 # Changing these properties will refresh the UI
1137 "use_soft_limits": self.use_soft_limits,
1138 "soft_range": (self.soft_min, self.soft_max),
1139 "hard_range": (self.min, self.max),
1142 def execute(self, context):
1143 from rna_prop_ui import (
1144 rna_idprop_ui_prop_get,
1145 rna_idprop_ui_prop_clear,
1146 rna_idprop_ui_prop_update,
1149 data_path = self.data_path
1151 prop = self.property
1153 prop_old = getattr(self, "_last_prop", [None])[0]
1155 if prop_old is None:
1156 self.report({'ERROR'}, "Direct execution not supported")
1157 return {'CANCELLED'}
1160 value_eval = eval(value)
1161 # assert else None -> None, not "None", see [#33431]
1162 assert(type(value_eval) in {str, float, int, bool, tuple, list})
1167 item = eval("context.%s" % data_path)
1168 prop_type_old = type(item[prop_old])
1170 rna_idprop_ui_prop_clear(item, prop_old)
1171 exec_str = "del item[%r]" % prop_old
1176 exec_str = "item[%r] = %s" % (prop, repr(value_eval))
1180 exec_str = "item.property_overridable_static_set('[\"%s\"]', %s)" % (prop, self.is_overridable_static)
1183 rna_idprop_ui_prop_update(item, prop)
1185 self._last_prop[:] = [prop]
1187 prop_type = type(item[prop])
1189 prop_ui = rna_idprop_ui_prop_get(item, prop)
1191 if prop_type in {float, int}:
1192 prop_ui["min"] = prop_type(self.min)
1193 prop_ui["max"] = prop_type(self.max)
1195 if self.use_soft_limits:
1196 prop_ui["soft_min"] = prop_type(self.soft_min)
1197 prop_ui["soft_max"] = prop_type(self.soft_max)
1199 prop_ui["soft_min"] = prop_type(self.min)
1200 prop_ui["soft_max"] = prop_type(self.max)
1202 prop_ui["description"] = self.description
1204 # If we have changed the type of the property, update its potential anim curves!
1205 if prop_type_old != prop_type:
1206 data_path = '["%s"]' % bpy.utils.escape_identifier(prop)
1209 def _update(fcurves):
1211 if fcu not in done and fcu.data_path == data_path:
1212 fcu.update_autoflags(item)
1215 def _update_strips(strips):
1217 if st.type == 'CLIP' and st.action:
1218 _update(st.action.fcurves)
1219 elif st.type == 'META':
1220 _update_strips(st.strips)
1222 adt = getattr(item, "animation_data", None)
1225 _update(adt.action.fcurves)
1227 _update(adt.drivers)
1229 for nt in adt.nla_tracks:
1230 _update_strips(nt.strips)
1232 # otherwise existing buttons which reference freed
1233 # memory may crash blender [#26510]
1234 # context.area.tag_redraw()
1235 for win in context.window_manager.windows:
1236 for area in win.screen.areas:
1241 def invoke(self, context, event):
1242 from rna_prop_ui import rna_idprop_ui_prop_get
1244 data_path = self.data_path
1247 self.report({'ERROR'}, "Data path not set")
1248 return {'CANCELLED'}
1250 self._last_prop = [self.property]
1252 item = eval("context.%s" % data_path)
1255 prop_ui = rna_idprop_ui_prop_get(item, self.property, False) # don't create
1257 self.min = prop_ui.get("min", -1000000000)
1258 self.max = prop_ui.get("max", 1000000000)
1259 self.description = prop_ui.get("description", "")
1261 self.soft_min = prop_ui.get("soft_min", self.min)
1262 self.soft_max = prop_ui.get("soft_max", self.max)
1263 self.use_soft_limits = (
1264 self.min != self.soft_min or
1265 self.max != self.soft_max
1268 # store for comparison
1269 self._cmp_props = self._cmp_props_get()
1271 wm = context.window_manager
1272 return wm.invoke_props_dialog(self)
1274 def check(self, context):
1275 cmp_props = self._cmp_props_get()
1277 if self._cmp_props != cmp_props:
1278 if cmp_props["use_soft_limits"]:
1279 if cmp_props["soft_range"] != self._cmp_props["soft_range"]:
1280 self.min = min(self.min, self.soft_min)
1281 self.max = max(self.max, self.soft_max)
1283 if cmp_props["hard_range"] != self._cmp_props["hard_range"]:
1284 self.soft_min = max(self.min, self.soft_min)
1285 self.soft_max = min(self.max, self.soft_max)
1288 if cmp_props["soft_range"] != cmp_props["hard_range"]:
1289 self.soft_min = self.min
1290 self.soft_max = self.max
1293 changed |= (cmp_props["use_soft_limits"] != self._cmp_props["use_soft_limits"])
1296 cmp_props = self._cmp_props_get()
1298 self._cmp_props = cmp_props
1302 def draw(self, context):
1303 layout = self.layout
1304 layout.prop(self, "property")
1305 layout.prop(self, "value")
1306 row = layout.row(align=True)
1307 row.prop(self, "min")
1308 row.prop(self, "max")
1311 row.prop(self, "use_soft_limits")
1312 row.prop(self, "is_overridable_static")
1314 row = layout.row(align=True)
1315 row.enabled = self.use_soft_limits
1316 row.prop(self, "soft_min", text="Soft Min")
1317 row.prop(self, "soft_max", text="Soft Max")
1318 layout.prop(self, "description")
1321 class WM_OT_properties_add(Operator):
1322 bl_idname = "wm.properties_add"
1323 bl_label = "Add Property"
1324 bl_options = {'UNDO', 'INTERNAL'}
1328 def execute(self, context):
1329 from rna_prop_ui import (
1330 rna_idprop_ui_prop_get,
1331 rna_idprop_ui_prop_update,
1334 data_path = self.data_path
1335 item = eval("context.%s" % data_path)
1337 def unique_name(names):
1341 while prop_new in names:
1342 prop_new = prop + str(i)
1347 prop = unique_name({
1349 *type(item).bl_rna.properties.keys(),
1353 rna_idprop_ui_prop_update(item, prop)
1355 # not essential, but without this we get [#31661]
1356 prop_ui = rna_idprop_ui_prop_get(item, prop)
1357 prop_ui["soft_min"] = prop_ui["min"] = 0.0
1358 prop_ui["soft_max"] = prop_ui["max"] = 1.0
1363 class WM_OT_properties_context_change(Operator):
1364 """Jump to a different tab inside the properties editor"""
1365 bl_idname = "wm.properties_context_change"
1367 bl_options = {'INTERNAL'}
1369 context: StringProperty(
1374 def execute(self, context):
1375 context.space_data.context = self.context
1379 class WM_OT_properties_remove(Operator):
1380 """Internal use (edit a property data_path)"""
1381 bl_idname = "wm.properties_remove"
1382 bl_label = "Remove Property"
1383 bl_options = {'UNDO', 'INTERNAL'}
1386 property: rna_property
1388 def execute(self, context):
1389 from rna_prop_ui import (
1390 rna_idprop_ui_prop_clear,
1391 rna_idprop_ui_prop_update,
1393 data_path = self.data_path
1394 item = eval("context.%s" % data_path)
1395 prop = self.property
1396 rna_idprop_ui_prop_update(item, prop)
1398 rna_idprop_ui_prop_clear(item, prop)
1403 class WM_OT_keyconfig_activate(Operator):
1404 bl_idname = "wm.keyconfig_activate"
1405 bl_label = "Activate Keyconfig"
1407 filepath: StringProperty(
1408 subtype='FILE_PATH',
1411 def execute(self, context):
1412 if bpy.utils.keyconfig_set(self.filepath, report=self.report):
1415 return {'CANCELLED'}
1418 class WM_OT_appconfig_default(Operator):
1419 bl_idname = "wm.appconfig_default"
1420 bl_label = "Default Application Configuration"
1422 def execute(self, context):
1425 context.window_manager.keyconfigs.active = context.window_manager.keyconfigs.default
1427 filepath = os.path.join(bpy.utils.preset_paths("interaction")[0], "blender.py")
1429 if os.path.exists(filepath):
1430 bpy.ops.script.execute_preset(
1432 menu_idname="USERPREF_MT_interaction_presets",
1438 class WM_OT_appconfig_activate(Operator):
1439 bl_idname = "wm.appconfig_activate"
1440 bl_label = "Activate Application Configuration"
1442 filepath: StringProperty(
1443 subtype='FILE_PATH',
1446 def execute(self, context):
1448 filepath = self.filepath
1449 bpy.utils.keyconfig_set(filepath)
1450 dirname, filename = os.path.split(filepath)
1451 filepath = os.path.normpath(os.path.join(dirname, os.pardir, "interaction", filename))
1452 if os.path.exists(filepath):
1453 bpy.ops.script.execute_preset(
1455 menu_idname="USERPREF_MT_interaction_presets",
1461 class WM_OT_sysinfo(Operator):
1462 """Generate system information, saved into a text file"""
1464 bl_idname = "wm.sysinfo"
1465 bl_label = "Save System Info"
1467 filepath: StringProperty(
1468 subtype='FILE_PATH',
1469 options={'SKIP_SAVE'},
1472 def execute(self, context):
1474 sys_info.write_sysinfo(self.filepath)
1477 def invoke(self, context, event):
1480 if not self.filepath:
1481 self.filepath = os.path.join(
1482 os.path.expanduser("~"), "system-info.txt")
1484 wm = context.window_manager
1485 wm.fileselect_add(self)
1486 return {'RUNNING_MODAL'}
1489 class WM_OT_copy_prev_settings(Operator):
1490 """Copy settings from previous version"""
1491 bl_idname = "wm.copy_prev_settings"
1492 bl_label = "Copy Previous Settings"
1495 def previous_version():
1496 ver = bpy.app.version
1497 ver_old = ((ver[0] * 100) + ver[1]) - 1
1498 return ver_old // 100, ver_old % 100
1502 ver = bpy.app.version
1503 ver_old = ((ver[0] * 100) + ver[1]) - 1
1504 return bpy.utils.resource_path('USER', ver_old // 100, ver_old % 100)
1508 return bpy.utils.resource_path('USER')
1511 def poll(cls, context):
1514 old = cls._old_path()
1515 new = cls._new_path()
1516 if os.path.isdir(old) and not os.path.isdir(new):
1519 old_userpref = os.path.join(old, "config", "userpref.blend")
1520 new_userpref = os.path.join(new, "config", "userpref.blend")
1521 return os.path.isfile(old_userpref) and not os.path.isfile(new_userpref)
1523 def execute(self, context):
1526 shutil.copytree(self._old_path(), self._new_path(), symlinks=True)
1528 # reload recent-files.txt
1529 bpy.ops.wm.read_history()
1531 # don't loose users work if they open the splash later.
1532 if bpy.data.is_saved is bpy.data.is_dirty is False:
1533 bpy.ops.wm.read_homefile()
1535 self.report({'INFO'}, "Reload Start-Up file to restore settings")
1540 class WM_OT_keyconfig_test(Operator):
1541 """Test key-config for conflicts"""
1542 bl_idname = "wm.keyconfig_test"
1543 bl_label = "Test Key Configuration for Conflicts"
1545 def execute(self, context):
1546 from bpy_extras import keyconfig_utils
1548 wm = context.window_manager
1549 kc = wm.keyconfigs.default
1551 if keyconfig_utils.keyconfig_test(kc):
1557 class WM_OT_keyconfig_import(Operator):
1558 """Import key configuration from a python script"""
1559 bl_idname = "wm.keyconfig_import"
1560 bl_label = "Import Key Configuration..."
1562 filepath: StringProperty(
1563 subtype='FILE_PATH',
1564 default="keymap.py",
1566 filter_folder: BoolProperty(
1567 name="Filter folders",
1571 filter_text: BoolProperty(
1576 filter_python: BoolProperty(
1577 name="Filter python",
1581 keep_original: BoolProperty(
1582 name="Keep original",
1583 description="Keep original file after copying to configuration folder",
1587 def execute(self, context):
1589 from os.path import basename
1592 if not self.filepath:
1593 self.report({'ERROR'}, "Filepath not set")
1594 return {'CANCELLED'}
1596 config_name = basename(self.filepath)
1598 path = bpy.utils.user_resource('SCRIPTS', os.path.join("presets", "keyconfig"), create=True)
1599 path = os.path.join(path, config_name)
1602 if self.keep_original:
1603 shutil.copy(self.filepath, path)
1605 shutil.move(self.filepath, path)
1606 except Exception as ex:
1607 self.report({'ERROR'}, "Installing keymap failed: %s" % ex)
1608 return {'CANCELLED'}
1610 # sneaky way to check we're actually running the code.
1611 if bpy.utils.keyconfig_set(path, report=self.report):
1614 return {'CANCELLED'}
1616 def invoke(self, context, event):
1617 wm = context.window_manager
1618 wm.fileselect_add(self)
1619 return {'RUNNING_MODAL'}
1621 # This operator is also used by interaction presets saving - AddPresetBase
1624 class WM_OT_keyconfig_export(Operator):
1625 """Export key configuration to a python script"""
1626 bl_idname = "wm.keyconfig_export"
1627 bl_label = "Export Key Configuration..."
1632 description="Write all keymaps (not just user modified)",
1634 filepath: StringProperty(
1635 subtype='FILE_PATH',
1636 default="keymap.py",
1638 filter_folder: BoolProperty(
1639 name="Filter folders",
1643 filter_text: BoolProperty(
1648 filter_python: BoolProperty(
1649 name="Filter python",
1654 def execute(self, context):
1655 from bpy_extras import keyconfig_utils
1657 if not self.filepath:
1658 raise Exception("Filepath not set")
1660 if not self.filepath.endswith(".py"):
1661 self.filepath += ".py"
1663 wm = context.window_manager
1665 keyconfig_utils.keyconfig_export_as_data(
1667 wm.keyconfigs.active,
1669 all_keymaps=self.all,
1674 def invoke(self, context, event):
1675 wm = context.window_manager
1676 wm.fileselect_add(self)
1677 return {'RUNNING_MODAL'}
1680 class WM_OT_keymap_restore(Operator):
1681 """Restore key map(s)"""
1682 bl_idname = "wm.keymap_restore"
1683 bl_label = "Restore Key Map(s)"
1687 description="Restore all keymaps to default",
1690 def execute(self, context):
1691 wm = context.window_manager
1694 for km in wm.keyconfigs.user.keymaps:
1695 km.restore_to_default()
1698 km.restore_to_default()
1703 class WM_OT_keyitem_restore(Operator):
1704 """Restore key map item"""
1705 bl_idname = "wm.keyitem_restore"
1706 bl_label = "Restore Key Map Item"
1708 item_id: IntProperty(
1709 name="Item Identifier",
1710 description="Identifier of the item to remove",
1714 def poll(cls, context):
1715 keymap = getattr(context, "keymap", None)
1718 def execute(self, context):
1720 kmi = km.keymap_items.from_id(self.item_id)
1722 if (not kmi.is_user_defined) and kmi.is_user_modified:
1723 km.restore_item_to_default(kmi)
1728 class WM_OT_keyitem_add(Operator):
1729 """Add key map item"""
1730 bl_idname = "wm.keyitem_add"
1731 bl_label = "Add Key Map Item"
1733 def execute(self, context):
1737 km.keymap_items.new_modal("", 'A', 'PRESS')
1739 km.keymap_items.new("none", 'A', 'PRESS')
1741 # clear filter and expand keymap so we can see the newly added item
1742 if context.space_data.filter_text != "":
1743 context.space_data.filter_text = ""
1744 km.show_expanded_items = True
1745 km.show_expanded_children = True
1750 class WM_OT_keyitem_remove(Operator):
1751 """Remove key map item"""
1752 bl_idname = "wm.keyitem_remove"
1753 bl_label = "Remove Key Map Item"
1755 item_id: IntProperty(
1756 name="Item Identifier",
1757 description="Identifier of the item to remove",
1761 def poll(cls, context):
1762 return hasattr(context, "keymap")
1764 def execute(self, context):
1766 kmi = km.keymap_items.from_id(self.item_id)
1767 km.keymap_items.remove(kmi)
1771 class WM_OT_keyconfig_remove(Operator):
1772 """Remove key config"""
1773 bl_idname = "wm.keyconfig_remove"
1774 bl_label = "Remove Key Config"
1777 def poll(cls, context):
1778 wm = context.window_manager
1779 keyconf = wm.keyconfigs.active
1780 return keyconf and keyconf.is_user_defined
1782 def execute(self, context):
1783 wm = context.window_manager
1784 keyconfig = wm.keyconfigs.active
1785 wm.keyconfigs.remove(keyconfig)
1789 class WM_OT_operator_cheat_sheet(Operator):
1790 """List all the Operators in a text-block, useful for scripting"""
1791 bl_idname = "wm.operator_cheat_sheet"
1792 bl_label = "Operator Cheat Sheet"
1794 def execute(self, context):
1797 for op_module_name in dir(bpy.ops):
1798 op_module = getattr(bpy.ops, op_module_name)
1799 for op_submodule_name in dir(op_module):
1800 op = getattr(op_module, op_submodule_name)
1802 if text.split("\n")[-1].startswith("bpy.ops."):
1803 op_strings.append(text)
1806 op_strings.append('')
1808 textblock = bpy.data.texts.new("OperatorList.txt")
1809 textblock.write('# %d Operators\n\n' % tot)
1810 textblock.write('\n'.join(op_strings))
1811 self.report({'INFO'}, "See OperatorList.txt textblock")
1815 # -----------------------------------------------------------------------------
1818 class WM_OT_addon_enable(Operator):
1819 """Enable an add-on"""
1820 bl_idname = "wm.addon_enable"
1821 bl_label = "Enable Add-on"
1823 module: StringProperty(
1825 description="Module name of the add-on to enable",
1828 def execute(self, context):
1836 err_str = traceback.format_exc()
1839 mod = addon_utils.enable(self.module, default_set=True, handle_error=err_cb)
1842 info = addon_utils.module_bl_info(mod)
1844 info_ver = info.get("blender", (0, 0, 0))
1846 if info_ver > bpy.app.version:
1849 "This script was written Blender "
1850 "version %d.%d.%d and might not "
1851 "function (correctly), "
1852 "though it is enabled" %
1859 self.report({'ERROR'}, err_str)
1861 return {'CANCELLED'}
1864 class WM_OT_addon_disable(Operator):
1865 """Disable an add-on"""
1866 bl_idname = "wm.addon_disable"
1867 bl_label = "Disable Add-on"
1869 module: StringProperty(
1871 description="Module name of the add-on to disable",
1874 def execute(self, context):
1882 err_str = traceback.format_exc()
1885 addon_utils.disable(self.module, default_set=True, handle_error=err_cb)
1888 self.report({'ERROR'}, err_str)
1893 class WM_OT_owner_enable(Operator):
1894 """Enable workspace owner ID"""
1895 bl_idname = "wm.owner_enable"
1896 bl_label = "Enable Add-on"
1898 owner_id: StringProperty(
1902 def execute(self, context):
1903 workspace = context.workspace
1904 workspace.owner_ids.new(self.owner_id)
1908 class WM_OT_owner_disable(Operator):
1909 """Enable workspace owner ID"""
1910 bl_idname = "wm.owner_disable"
1911 bl_label = "Disable UI Tag"
1913 owner_id: StringProperty(
1917 def execute(self, context):
1918 workspace = context.workspace
1919 owner_id = workspace.owner_ids[self.owner_id]
1920 workspace.owner_ids.remove(owner_id)
1924 class WM_OT_theme_install(Operator):
1925 """Load and apply a Blender XML theme file"""
1926 bl_idname = "wm.theme_install"
1927 bl_label = "Install Theme..."
1929 overwrite: BoolProperty(
1931 description="Remove existing theme file if exists",
1934 filepath: StringProperty(
1935 subtype='FILE_PATH',
1937 filter_folder: BoolProperty(
1938 name="Filter folders",
1942 filter_glob: StringProperty(
1947 def execute(self, context):
1952 xmlfile = self.filepath
1954 path_themes = bpy.utils.user_resource('SCRIPTS', "presets/interface_theme", create=True)
1957 self.report({'ERROR'}, "Failed to get themes path")
1958 return {'CANCELLED'}
1960 path_dest = os.path.join(path_themes, os.path.basename(xmlfile))
1962 if not self.overwrite:
1963 if os.path.exists(path_dest):
1964 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
1965 return {'CANCELLED'}
1968 shutil.copyfile(xmlfile, path_dest)
1969 bpy.ops.script.execute_preset(
1971 menu_idname="USERPREF_MT_interface_theme_presets",
1975 traceback.print_exc()
1976 return {'CANCELLED'}
1980 def invoke(self, context, event):
1981 wm = context.window_manager
1982 wm.fileselect_add(self)
1983 return {'RUNNING_MODAL'}
1986 class WM_OT_addon_refresh(Operator):
1987 """Scan add-on directories for new modules"""
1988 bl_idname = "wm.addon_refresh"
1989 bl_label = "Refresh"
1991 def execute(self, context):
1994 addon_utils.modules_refresh()
1999 # Note: shares some logic with WM_OT_app_template_install
2000 # but not enough to de-duplicate. Fixed here may apply to both.
2001 class WM_OT_addon_install(Operator):
2002 """Install an add-on"""
2003 bl_idname = "wm.addon_install"
2004 bl_label = "Install Add-on from File..."
2006 overwrite: BoolProperty(
2008 description="Remove existing add-ons with the same ID",
2011 target: EnumProperty(
2013 items=(('DEFAULT', "Default", ""),
2014 ('PREFS', "User Prefs", "")),
2017 filepath: StringProperty(
2018 subtype='FILE_PATH',
2020 filter_folder: BoolProperty(
2021 name="Filter folders",
2025 filter_python: BoolProperty(
2026 name="Filter python",
2030 filter_glob: StringProperty(
2031 default="*.py;*.zip",
2035 def execute(self, context):
2042 pyfile = self.filepath
2044 if self.target == 'DEFAULT':
2045 # don't use bpy.utils.script_paths("addons") because we may not be able to write to it.
2046 path_addons = bpy.utils.user_resource('SCRIPTS', "addons", create=True)
2048 path_addons = context.user_preferences.filepaths.script_directory
2050 path_addons = os.path.join(path_addons, "addons")
2053 self.report({'ERROR'}, "Failed to get add-ons path")
2054 return {'CANCELLED'}
2056 if not os.path.isdir(path_addons):
2058 os.makedirs(path_addons, exist_ok=True)
2060 traceback.print_exc()
2062 # Check if we are installing from a target path,
2063 # doing so causes 2+ addons of same name or when the same from/to
2064 # location is used, removal of the file!
2066 pyfile_dir = os.path.dirname(pyfile)
2067 for addon_path in addon_utils.paths():
2068 if os.path.samefile(pyfile_dir, addon_path):
2069 self.report({'ERROR'}, "Source file is in the add-on search path: %r" % addon_path)
2070 return {'CANCELLED'}
2073 # done checking for exceptional case
2075 addons_old = {mod.__name__ for mod in addon_utils.modules()}
2077 # check to see if the file is in compressed format (.zip)
2078 if zipfile.is_zipfile(pyfile):
2080 file_to_extract = zipfile.ZipFile(pyfile, 'r')
2082 traceback.print_exc()
2083 return {'CANCELLED'}
2086 for f in file_to_extract.namelist():
2087 module_filesystem_remove(path_addons, f)
2089 for f in file_to_extract.namelist():
2090 path_dest = os.path.join(path_addons, os.path.basename(f))
2091 if os.path.exists(path_dest):
2092 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
2093 return {'CANCELLED'}
2095 try: # extract the file to "addons"
2096 file_to_extract.extractall(path_addons)
2098 traceback.print_exc()
2099 return {'CANCELLED'}
2102 path_dest = os.path.join(path_addons, os.path.basename(pyfile))
2105 module_filesystem_remove(path_addons, os.path.basename(pyfile))
2106 elif os.path.exists(path_dest):
2107 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
2108 return {'CANCELLED'}
2110 # if not compressed file just copy into the addon path
2112 shutil.copyfile(pyfile, path_dest)
2114 traceback.print_exc()
2115 return {'CANCELLED'}
2117 addons_new = {mod.__name__ for mod in addon_utils.modules()} - addons_old
2118 addons_new.discard("modules")
2120 # disable any addons we may have enabled previously and removed.
2121 # this is unlikely but do just in case. bug [#23978]
2122 for new_addon in addons_new:
2123 addon_utils.disable(new_addon, default_set=True)
2125 # possible the zip contains multiple addons, we could disallow this
2126 # but for now just use the first
2127 for mod in addon_utils.modules(refresh=False):
2128 if mod.__name__ in addons_new:
2129 info = addon_utils.module_bl_info(mod)
2131 # show the newly installed addon.
2132 context.window_manager.addon_filter = 'All'
2133 context.window_manager.addon_search = info["name"]
2136 # in case a new module path was created to install this addon.
2137 bpy.utils.refresh_script_paths()
2141 tip_("Modules Installed (%s) from %r into %r") %
2142 (", ".join(sorted(addons_new)), pyfile, path_addons)
2145 self.report({'INFO'}, msg)
2149 def invoke(self, context, event):
2150 wm = context.window_manager
2151 wm.fileselect_add(self)
2152 return {'RUNNING_MODAL'}
2155 class WM_OT_addon_remove(Operator):
2156 """Delete the add-on from the file system"""
2157 bl_idname = "wm.addon_remove"
2158 bl_label = "Remove Add-on"
2160 module: StringProperty(
2162 description="Module name of the add-on to remove",
2166 def path_from_addon(module):
2170 for mod in addon_utils.modules():
2171 if mod.__name__ == module:
2172 filepath = mod.__file__
2173 if os.path.exists(filepath):
2174 if os.path.splitext(os.path.basename(filepath))[0] == "__init__":
2175 return os.path.dirname(filepath), True
2177 return filepath, False
2180 def execute(self, context):
2184 path, isdir = WM_OT_addon_remove.path_from_addon(self.module)
2186 self.report({'WARNING'}, "Add-on path %r could not be found" % path)
2187 return {'CANCELLED'}
2189 # in case its enabled
2190 addon_utils.disable(self.module, default_set=True)
2198 addon_utils.modules_refresh()
2200 context.area.tag_redraw()
2203 # lame confirmation check
2204 def draw(self, context):
2205 self.layout.label(text="Remove Add-on: %r?" % self.module)
2206 path, _isdir = WM_OT_addon_remove.path_from_addon(self.module)
2207 self.layout.label(text="Path: %r" % path)
2209 def invoke(self, context, event):
2210 wm = context.window_manager
2211 return wm.invoke_props_dialog(self, width=600)
2214 class WM_OT_addon_expand(Operator):
2215 """Display information and preferences for this add-on"""
2216 bl_idname = "wm.addon_expand"
2218 bl_options = {'INTERNAL'}
2220 module: StringProperty(
2222 description="Module name of the add-on to expand",
2225 def execute(self, context):
2228 module_name = self.module
2230 mod = addon_utils.addons_fake_modules.get(module_name)
2232 info = addon_utils.module_bl_info(mod)
2233 info["show_expanded"] = not info["show_expanded"]
2238 class WM_OT_addon_userpref_show(Operator):
2239 """Show add-on user preferences"""
2240 bl_idname = "wm.addon_userpref_show"
2242 bl_options = {'INTERNAL'}
2244 module: StringProperty(
2246 description="Module name of the add-on to expand",
2249 def execute(self, context):
2252 module_name = self.module
2254 modules = addon_utils.modules(refresh=False)
2255 mod = addon_utils.addons_fake_modules.get(module_name)
2257 info = addon_utils.module_bl_info(mod)
2258 info["show_expanded"] = True
2260 context.user_preferences.active_section = 'ADDONS'
2261 context.window_manager.addon_filter = 'All'
2262 context.window_manager.addon_search = info["name"]
2263 bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
2268 # Note: shares some logic with WM_OT_addon_install
2269 # but not enough to de-duplicate. Fixes here may apply to both.
2270 class WM_OT_app_template_install(Operator):
2271 """Install an application-template"""
2272 bl_idname = "wm.app_template_install"
2273 bl_label = "Install Template from File..."
2275 overwrite: BoolProperty(
2277 description="Remove existing template with the same ID",
2281 filepath: StringProperty(
2282 subtype='FILE_PATH',
2284 filter_folder: BoolProperty(
2285 name="Filter folders",
2289 filter_glob: StringProperty(
2294 def execute(self, context):
2299 filepath = self.filepath
2301 path_app_templates = bpy.utils.user_resource(
2302 'SCRIPTS', os.path.join("startup", "bl_app_templates_user"),
2306 if not path_app_templates:
2307 self.report({'ERROR'}, "Failed to get add-ons path")
2308 return {'CANCELLED'}
2310 if not os.path.isdir(path_app_templates):
2312 os.makedirs(path_app_templates, exist_ok=True)
2314 traceback.print_exc()
2316 app_templates_old = set(os.listdir(path_app_templates))
2318 # check to see if the file is in compressed format (.zip)
2319 if zipfile.is_zipfile(filepath):
2321 file_to_extract = zipfile.ZipFile(filepath, 'r')
2323 traceback.print_exc()
2324 return {'CANCELLED'}
2327 for f in file_to_extract.namelist():
2328 module_filesystem_remove(path_app_templates, f)
2330 for f in file_to_extract.namelist():
2331 path_dest = os.path.join(path_app_templates, os.path.basename(f))
2332 if os.path.exists(path_dest):
2333 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
2334 return {'CANCELLED'}
2336 try: # extract the file to "bl_app_templates_user"
2337 file_to_extract.extractall(path_app_templates)
2339 traceback.print_exc()
2340 return {'CANCELLED'}
2343 # Only support installing zipfiles
2344 self.report({'WARNING'}, "Expected a zip-file %r\n" % filepath)
2345 return {'CANCELLED'}
2347 app_templates_new = set(os.listdir(path_app_templates)) - app_templates_old
2349 # in case a new module path was created to install this addon.
2350 bpy.utils.refresh_script_paths()
2354 tip_("Template Installed (%s) from %r into %r") %
2355 (", ".join(sorted(app_templates_new)), filepath, path_app_templates)
2358 self.report({'INFO'}, msg)
2362 def invoke(self, context, event):
2363 wm = context.window_manager
2364 wm.fileselect_add(self)
2365 return {'RUNNING_MODAL'}
2368 class WM_OT_tool_set_by_name(Operator):
2369 """Set the tool by name (for keymaps)"""
2370 bl_idname = "wm.tool_set_by_name"
2371 bl_label = "Set Tool By Name"
2373 name: StringProperty(
2375 description="Display name of the tool",
2377 cycle: BoolProperty(
2379 description="Cycle through tools in this group",
2381 options={'SKIP_SAVE'},
2384 space_type: rna_space_type_prop
2386 def execute(self, context):
2387 from bl_ui.space_toolsystem_common import (
2389 activate_by_name_or_cycle,
2392 if self.properties.is_property_set("space_type"):
2393 space_type = self.space_type
2395 space_type = context.space_data.type
2397 fn = activate_by_name_or_cycle if self.cycle else activate_by_name
2398 if fn(context, space_type, self.name):
2401 self.report({'WARNING'}, f"Tool {self.name!r:s} not found for space {space_type!r:s}.")
2402 return {'CANCELLED'}
2405 class WM_OT_toolbar(Operator):
2406 bl_idname = "wm.toolbar"
2407 bl_label = "Toolbar"
2410 def poll(cls, context):
2411 return context.space_data is not None
2413 def execute(self, context):
2414 from bl_ui.space_toolsystem_common import (
2415 ToolSelectPanelHelper,
2416 keymap_from_context,
2418 space_type = context.space_data.type
2420 cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
2422 self.report({'WARNING'}, f"Toolbar not found for {space_type!r}")
2423 return {'CANCELLED'}
2425 wm = context.window_manager
2426 keymap = keymap_from_context(context, space_type)
2428 def draw_menu(popover, context):
2429 layout = popover.layout
2430 layout.operator_context = 'INVOKE_REGION_WIN'
2431 cls.draw_cls(layout, context, detect_layout=False, scale_y=1.0)
2433 wm.popover(draw_menu, ui_units_x=8, keymap=keymap)
2437 # Studio Light operations
2438 class WM_OT_studiolight_install(Operator):
2439 """Install a user defined studio light"""
2440 bl_idname = "wm.studiolight_install"
2441 bl_label = "Install Custom Studio Light"
2443 files: CollectionProperty(
2445 type=OperatorFileListElement,
2447 directory: StringProperty(
2450 filter_folder: BoolProperty(
2451 name="Filter folders",
2455 filter_glob: StringProperty(
2456 default="*.png;*.jpg;*.hdr;*.exr",
2459 orientation: EnumProperty(
2461 ('MATCAP', "MatCap", ""),
2462 ('WORLD', "World", ""),
2463 ('CAMERA', "Camera", ""),
2467 def execute(self, context):
2471 userpref = context.user_preferences
2473 filepaths = [pathlib.Path(self.directory, e.name) for e in self.files]
2474 path_studiolights = bpy.utils.user_resource('DATAFILES')
2476 if not path_studiolights:
2477 self.report({'ERROR'}, "Failed to get Studio Light path")
2478 return {'CANCELLED'}
2480 path_studiolights = pathlib.Path(path_studiolights, "studiolights", self.orientation.lower())
2481 if not path_studiolights.exists():
2483 path_studiolights.mkdir(parents=True, exist_ok=True)
2485 traceback.print_exc()
2487 for filepath in filepaths:
2488 shutil.copy(str(filepath), str(path_studiolights))
2489 userpref.studio_lights.new(str(path_studiolights.joinpath(filepath.name)), self.orientation)
2493 tip_("StudioLight Installed %r into %r") %
2494 (", ".join(str(x.name) for x in self.files), str(path_studiolights))
2497 self.report({'INFO'}, msg)
2500 def invoke(self, context, event):
2501 wm = context.window_manager
2502 wm.fileselect_add(self)
2503 return {'RUNNING_MODAL'}
2506 class WM_OT_studiolight_uninstall(Operator):
2507 bl_idname = 'wm.studiolight_uninstall'
2508 bl_label = "Uninstall Studio Light"
2509 index: bpy.props.IntProperty()
2511 def _remove_path(self, path):
2515 def execute(self, context):
2517 userpref = context.user_preferences
2518 for studio_light in userpref.studio_lights:
2519 if studio_light.index == self.index:
2520 if studio_light.path:
2521 self._remove_path(pathlib.Path(studio_light.path))
2522 if studio_light.path_irr_cache:
2523 self._remove_path(pathlib.Path(studio_light.path_irr_cache))
2524 if studio_light.path_sh_cache:
2525 self._remove_path(pathlib.Path(studio_light.path_sh_cache))
2526 userpref.studio_lights.remove(studio_light)
2528 return {'CANCELLED'}
2531 class WM_OT_studiolight_userpref_show(Operator):
2532 """Show light user preferences"""
2533 bl_idname = "wm.studiolight_userpref_show"
2535 bl_options = {'INTERNAL'}
2537 def execute(self, context):
2538 context.user_preferences.active_section = 'LIGHTS'
2539 bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
2543 class WM_MT_splash(Menu):
2546 def draw_setup(self, context):
2547 layout = self.layout
2548 layout.operator_context = 'EXEC_DEFAULT'
2550 layout.label(text="Quick Setup")
2552 split = layout.split(factor=0.25)
2554 split = split.split(factor=2.0 / 3.0)
2556 col = split.column()
2558 sub = col.column(align=True)
2559 sub.label(text="Input and Shortcuts:")
2560 text = bpy.path.display_name(context.window_manager.keyconfigs.active.name)
2562 text = "Blender (default)"
2563 sub.menu("USERPREF_MT_appconfigs", text=text)
2567 sub = col.column(align=True)
2568 sub.label(text="Theme:")
2569 label = bpy.types.USERPREF_MT_interface_theme_presets.bl_label
2570 if label == "Presets":
2571 label = "Blender Dark"
2572 sub.menu("USERPREF_MT_interface_theme_presets", text=label)
2574 # We need to make switching to a language easier first
2575 #sub = col.column(align=False)
2576 # sub.label(text="Language:")
2577 #userpref = context.user_preferences
2578 #sub.prop(userpref.system, "language", text="")
2587 if bpy.types.WM_OT_copy_prev_settings.poll(context):
2588 old_version = bpy.types.WM_OT_copy_prev_settings.previous_version()
2589 sub.operator("wm.copy_prev_settings", text="Load %d.%d Settings" % old_version)
2590 sub.operator("wm.save_userpref", text="Save New Settings")
2594 sub.operator("wm.save_userpref", text="Next")
2598 def draw(self, context):
2599 # Draw setup screen if no user preferences have been saved yet.
2602 user_path = bpy.utils.resource_path('USER')
2603 userdef_path = os.path.join(user_path, "config", "userpref.blend")
2605 if not os.path.isfile(userdef_path):
2606 self.draw_setup(context)
2610 layout = self.layout
2611 layout.operator_context = 'EXEC_DEFAULT'
2612 layout.emboss = 'PULLDOWN_MENU'
2614 split = layout.split()
2617 col1 = split.column()
2618 col1.label(text="New File")
2620 bpy.types.TOPBAR_MT_file_new.draw_ex(col1, context, use_splash=True)
2623 col2 = split.column()
2624 col2_title = col2.row()
2626 found_recent = col2.template_recent_files()
2629 col2_title.label(text="Recent Files")
2631 # Links if no recent files
2632 col2_title.label(text="Getting Started")
2635 "wm.url_open", text="Manual", icon='URL'
2636 ).url = "https://docs.blender.org/manual/en/dev/"
2638 "wm.url_open", text="Release Notes", icon='URL',
2639 ).url = "https://www.blender.org/download/releases/%d-%d/" % bpy.app.version[:2]
2641 "wm.url_open", text="Blender Website", icon='URL',
2642 ).url = "https://www.blender.org"
2644 "wm.url_open", text="Credits", icon='URL',
2645 ).url = "https://www.blender.org/about/credits/"
2649 split = layout.split()
2651 col1 = split.column()
2653 sub.operator_context = 'INVOKE_DEFAULT'
2654 sub.operator("wm.open_mainfile", text="Open...", icon='FILE_FOLDER')
2655 col1.operator("wm.recover_last_session", icon='RECOVER_LAST')
2657 col2 = split.column()
2660 "wm.url_open", text="Release Notes", icon='URL',
2661 ).url = "https://www.blender.org/download/releases/%d-%d/" % bpy.app.version[:2]
2663 "wm.url_open", text="Development Fund", icon='URL'
2664 ).url = "https://fund.blender.org"
2667 "wm.url_open", text="Development Fund", icon='URL'
2668 ).url = "https://fund.blender.org"
2670 "wm.url_open", text="Donate", icon='URL'
2671 ).url = "https://www.blender.org/foundation/donation-payment/"
2676 class WM_OT_drop_blend_file(Operator):
2677 bl_idname = "wm.drop_blend_file"
2678 bl_label = "Handle dropped .blend file"
2679 bl_options = {'INTERNAL'}
2681 filepath: StringProperty()
2683 def invoke(self, context, event):
2684 context.window_manager.popup_menu(self.draw_menu, title=bpy.path.basename(self.filepath), icon='QUESTION')
2687 def draw_menu(self, menu, context):
2688 layout = menu.layout
2690 col = layout.column()
2691 col.operator_context = 'EXEC_DEFAULT'
2692 col.operator("wm.open_mainfile", text="Open", icon='FILE_FOLDER').filepath = self.filepath
2695 col = layout.column()
2696 col.operator_context = 'INVOKE_DEFAULT'
2697 col.operator("wm.link", text="Link...", icon='LINK_BLEND').filepath = self.filepath
2698 col.operator("wm.append", text="Append...", icon='APPEND_BLEND').filepath = self.filepath
2701 BRUSH_OT_active_index_set,
2702 WM_OT_addon_disable,
2705 WM_OT_addon_install,
2706 WM_OT_addon_refresh,
2708 WM_OT_addon_userpref_show,
2709 WM_OT_app_template_install,
2710 WM_OT_appconfig_activate,
2711 WM_OT_appconfig_default,
2712 WM_OT_context_collection_boolean_set,
2713 WM_OT_context_cycle_array,
2714 WM_OT_context_cycle_enum,
2715 WM_OT_context_cycle_int,
2716 WM_OT_context_menu_enum,
2717 WM_OT_context_modal_mouse,
2718 WM_OT_context_pie_enum,
2719 WM_OT_context_scale_float,
2720 WM_OT_context_scale_int,
2721 WM_OT_context_set_boolean,
2722 WM_OT_context_set_enum,
2723 WM_OT_context_set_float,
2724 WM_OT_context_set_id,
2725 WM_OT_context_set_int,
2726 WM_OT_context_set_string,
2727 WM_OT_context_set_value,
2728 WM_OT_context_toggle,
2729 WM_OT_context_toggle_enum,
2730 WM_OT_copy_prev_settings,
2732 WM_OT_doc_view_manual,
2733 WM_OT_drop_blend_file,
2734 WM_OT_keyconfig_activate,
2735 WM_OT_keyconfig_export,
2736 WM_OT_keyconfig_import,
2737 WM_OT_keyconfig_remove,
2738 WM_OT_keyconfig_test,
2740 WM_OT_keyitem_remove,
2741 WM_OT_keyitem_restore,
2742 WM_OT_keymap_restore,
2743 WM_OT_operator_cheat_sheet,
2744 WM_OT_operator_pie_enum,
2746 WM_OT_properties_add,
2747 WM_OT_properties_context_change,
2748 WM_OT_properties_edit,
2749 WM_OT_properties_remove,
2751 WM_OT_theme_install,
2752 WM_OT_owner_disable,
2755 WM_OT_studiolight_install,
2756 WM_OT_studiolight_uninstall,
2757 WM_OT_studiolight_userpref_show,
2758 WM_OT_tool_set_by_name,