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_
38 # FIXME, we need a way to detect key repeat events.
39 # unfortunately checking event previous values isn't reliable.
40 use_toolbar_release_hack = True
43 rna_path_prop = StringProperty(
44 name="Context Attributes",
45 description="RNA context string",
49 rna_reverse_prop = BoolProperty(
51 description="Cycle backwards",
55 rna_wrap_prop = BoolProperty(
57 description="Wrap back to the first/last values",
61 rna_relative_prop = BoolProperty(
63 description="Apply relative to the current value (delta)",
67 rna_space_type_prop = EnumProperty(
70 (e.identifier, e.name, "", e. value)
71 for e in bpy.types.Space.bl_rna.properties["type"].enum_items
77 def context_path_validate(context, data_path):
79 value = eval("context.%s" % data_path) if data_path else Ellipsis
80 except AttributeError as ex:
81 if str(ex).startswith("'NoneType'"):
82 # One of the items in the rna path is None, just ignore this
85 # We have a real error in the rna path, don't ignore that
91 def operator_value_is_undo(value):
92 if value in {None, Ellipsis}:
95 # typical properties or objects
96 id_data = getattr(value, "id_data", Ellipsis)
100 elif id_data is Ellipsis:
101 # handle mathutils types
102 id_data = getattr(getattr(value, "owner", None), "id_data", None)
107 # return True if its a non window ID type
108 return (isinstance(id_data, bpy.types.ID) and
109 (not isinstance(id_data, (bpy.types.WindowManager,
115 def operator_path_is_undo(context, data_path):
116 # note that if we have data paths that use strings this could fail
117 # luckily we don't do this!
119 # When we can't find the data owner assume no undo is needed.
120 data_path_head = data_path.rpartition(".")[0]
122 if not data_path_head:
125 value = context_path_validate(context, data_path_head)
127 return operator_value_is_undo(value)
130 def operator_path_undo_return(context, data_path):
131 return {'FINISHED'} if operator_path_is_undo(context, data_path) else {'CANCELLED'}
134 def operator_value_undo_return(value):
135 return {'FINISHED'} if operator_value_is_undo(value) else {'CANCELLED'}
138 def execute_context_assign(self, context):
139 data_path = self.data_path
140 if context_path_validate(context, data_path) is Ellipsis:
141 return {'PASS_THROUGH'}
143 if getattr(self, "relative", False):
144 exec("context.%s += self.value" % data_path)
146 exec("context.%s = self.value" % data_path)
148 return operator_path_undo_return(context, data_path)
151 def module_filesystem_remove(path_base, module_name):
153 module_name = os.path.splitext(module_name)[0]
154 for f in os.listdir(path_base):
155 f_base = os.path.splitext(f)[0]
156 if f_base == module_name:
157 f_full = os.path.join(path_base, f)
159 if os.path.isdir(f_full):
165 class WM_OT_context_set_boolean(Operator):
166 """Set a context value"""
167 bl_idname = "wm.context_set_boolean"
168 bl_label = "Context Set Boolean"
169 bl_options = {'UNDO', 'INTERNAL'}
171 data_path: rna_path_prop
174 description="Assignment value",
178 execute = execute_context_assign
181 class WM_OT_context_set_int(Operator): # same as enum
182 """Set a context value"""
183 bl_idname = "wm.context_set_int"
184 bl_label = "Context Set"
185 bl_options = {'UNDO', 'INTERNAL'}
187 data_path: rna_path_prop
190 description="Assign value",
193 relative: rna_relative_prop
195 execute = execute_context_assign
198 class WM_OT_context_scale_float(Operator):
199 """Scale a float context value"""
200 bl_idname = "wm.context_scale_float"
201 bl_label = "Context Scale Float"
202 bl_options = {'UNDO', 'INTERNAL'}
204 data_path: rna_path_prop
205 value: FloatProperty(
207 description="Assign value",
211 def execute(self, context):
212 data_path = self.data_path
213 if context_path_validate(context, data_path) is Ellipsis:
214 return {'PASS_THROUGH'}
218 if value == 1.0: # nothing to do
221 exec("context.%s *= value" % data_path)
223 return operator_path_undo_return(context, data_path)
226 class WM_OT_context_scale_int(Operator):
227 """Scale an int context value"""
228 bl_idname = "wm.context_scale_int"
229 bl_label = "Context Scale Int"
230 bl_options = {'UNDO', 'INTERNAL'}
232 data_path: rna_path_prop
233 value: FloatProperty(
235 description="Assign value",
238 always_step: BoolProperty(
240 description="Always adjust the value by a minimum of 1 when 'value' is not 1.0",
244 def execute(self, context):
245 data_path = self.data_path
246 if context_path_validate(context, data_path) is Ellipsis:
247 return {'PASS_THROUGH'}
251 if value == 1.0: # nothing to do
254 if getattr(self, "always_step", False):
261 exec("context.%s = %s(round(context.%s * value), context.%s + %s)" %
262 (data_path, func, data_path, data_path, add))
264 exec("context.%s *= value" % data_path)
266 return operator_path_undo_return(context, data_path)
269 class WM_OT_context_set_float(Operator): # same as enum
270 """Set a context value"""
271 bl_idname = "wm.context_set_float"
272 bl_label = "Context Set Float"
273 bl_options = {'UNDO', 'INTERNAL'}
275 data_path: rna_path_prop
276 value: FloatProperty(
278 description="Assignment value",
281 relative: rna_relative_prop
283 execute = execute_context_assign
286 class WM_OT_context_set_string(Operator): # same as enum
287 """Set a context value"""
288 bl_idname = "wm.context_set_string"
289 bl_label = "Context Set String"
290 bl_options = {'UNDO', 'INTERNAL'}
292 data_path: rna_path_prop
293 value: StringProperty(
295 description="Assign value",
299 execute = execute_context_assign
302 class WM_OT_context_set_enum(Operator):
303 """Set a context value"""
304 bl_idname = "wm.context_set_enum"
305 bl_label = "Context Set Enum"
306 bl_options = {'UNDO', 'INTERNAL'}
308 data_path: rna_path_prop
309 value: StringProperty(
311 description="Assignment value (as a string)",
315 execute = execute_context_assign
318 class WM_OT_context_set_value(Operator):
319 """Set a context value"""
320 bl_idname = "wm.context_set_value"
321 bl_label = "Context Set Value"
322 bl_options = {'UNDO', 'INTERNAL'}
324 data_path: rna_path_prop
325 value: StringProperty(
327 description="Assignment value (as a string)",
331 def execute(self, context):
332 data_path = self.data_path
333 if context_path_validate(context, data_path) is Ellipsis:
334 return {'PASS_THROUGH'}
335 exec("context.%s = %s" % (data_path, self.value))
336 return operator_path_undo_return(context, data_path)
339 class WM_OT_context_toggle(Operator):
340 """Toggle a context value"""
341 bl_idname = "wm.context_toggle"
342 bl_label = "Context Toggle"
343 bl_options = {'UNDO', 'INTERNAL'}
345 data_path: rna_path_prop
347 def execute(self, context):
348 data_path = self.data_path
350 if context_path_validate(context, data_path) is Ellipsis:
351 return {'PASS_THROUGH'}
353 exec("context.%s = not (context.%s)" % (data_path, data_path))
355 return operator_path_undo_return(context, data_path)
358 class WM_OT_context_toggle_enum(Operator):
359 """Toggle a context value"""
360 bl_idname = "wm.context_toggle_enum"
361 bl_label = "Context Toggle Values"
362 bl_options = {'UNDO', 'INTERNAL'}
364 data_path: rna_path_prop
365 value_1: StringProperty(
367 description="Toggle enum",
370 value_2: StringProperty(
372 description="Toggle enum",
376 def execute(self, context):
377 data_path = self.data_path
379 if context_path_validate(context, data_path) is Ellipsis:
380 return {'PASS_THROUGH'}
382 # failing silently is not ideal, but we don't want errors for shortcut
383 # keys that some values that are only available in a particular context
385 exec("context.%s = ('%s', '%s')[context.%s != '%s']" %
386 (data_path, self.value_1,
387 self.value_2, data_path,
391 return {'PASS_THROUGH'}
393 return operator_path_undo_return(context, data_path)
396 class WM_OT_context_cycle_int(Operator):
397 """Set a context value (useful for cycling active material, """ \
398 """vertex keys, groups, etc.)"""
399 bl_idname = "wm.context_cycle_int"
400 bl_label = "Context Int Cycle"
401 bl_options = {'UNDO', 'INTERNAL'}
403 data_path: rna_path_prop
404 reverse: rna_reverse_prop
407 def execute(self, context):
408 data_path = self.data_path
409 value = context_path_validate(context, data_path)
410 if value is Ellipsis:
411 return {'PASS_THROUGH'}
418 exec("context.%s = value" % data_path)
421 if value != eval("context.%s" % data_path):
422 # relies on rna clamping integers out of the range
424 value = (1 << 31) - 1
428 exec("context.%s = value" % data_path)
430 return operator_path_undo_return(context, data_path)
433 class WM_OT_context_cycle_enum(Operator):
434 """Toggle a context value"""
435 bl_idname = "wm.context_cycle_enum"
436 bl_label = "Context Enum Cycle"
437 bl_options = {'UNDO', 'INTERNAL'}
439 data_path: rna_path_prop
440 reverse: rna_reverse_prop
443 def execute(self, context):
444 data_path = self.data_path
445 value = context_path_validate(context, data_path)
446 if value is Ellipsis:
447 return {'PASS_THROUGH'}
451 # Have to get rna enum values
452 rna_struct_str, rna_prop_str = data_path.rsplit('.', 1)
453 i = rna_prop_str.find('[')
455 # just in case we get "context.foo.bar[0]"
457 rna_prop_str = rna_prop_str[0:i]
459 rna_struct = eval("context.%s.rna_type" % rna_struct_str)
461 rna_prop = rna_struct.properties[rna_prop_str]
463 if type(rna_prop) != bpy.types.EnumProperty:
464 raise Exception("expected an enum property")
466 enums = rna_struct.properties[rna_prop_str].enum_items.keys()
467 orig_index = enums.index(orig_value)
469 # Have the info we need, advance to the next item.
471 # When wrap's disabled we may set the value to its self,
472 # this is done to ensure update callbacks run.
475 advance_enum = enums[-1] if self.wrap else enums[0]
477 advance_enum = enums[orig_index - 1]
479 if orig_index == len(enums) - 1:
480 advance_enum = enums[0] if self.wrap else enums[-1]
482 advance_enum = enums[orig_index + 1]
485 exec("context.%s = advance_enum" % data_path)
486 return operator_path_undo_return(context, data_path)
489 class WM_OT_context_cycle_array(Operator):
490 """Set a context array value """ \
491 """(useful for cycling the active mesh edit mode)"""
492 bl_idname = "wm.context_cycle_array"
493 bl_label = "Context Array Cycle"
494 bl_options = {'UNDO', 'INTERNAL'}
496 data_path: rna_path_prop
497 reverse: rna_reverse_prop
499 def execute(self, context):
500 data_path = self.data_path
501 value = context_path_validate(context, data_path)
502 if value is Ellipsis:
503 return {'PASS_THROUGH'}
507 array.insert(0, array.pop())
509 array.append(array.pop(0))
512 exec("context.%s = cycle(context.%s[:])" % (data_path, data_path))
514 return operator_path_undo_return(context, data_path)
517 class WM_OT_context_menu_enum(Operator):
518 bl_idname = "wm.context_menu_enum"
519 bl_label = "Context Enum Menu"
520 bl_options = {'UNDO', 'INTERNAL'}
522 data_path: rna_path_prop
524 def execute(self, context):
525 data_path = self.data_path
526 value = context_path_validate(context, data_path)
528 if value is Ellipsis:
529 return {'PASS_THROUGH'}
531 base_path, prop_string = data_path.rsplit(".", 1)
532 value_base = context_path_validate(context, base_path)
533 prop = value_base.bl_rna.properties[prop_string]
535 def draw_cb(self, context):
537 layout.prop(value_base, prop_string, expand=True)
539 context.window_manager.popup_menu(draw_func=draw_cb, title=prop.name, icon=prop.icon)
544 class WM_OT_context_pie_enum(Operator):
545 bl_idname = "wm.context_pie_enum"
546 bl_label = "Context Enum Pie"
547 bl_options = {'UNDO', 'INTERNAL'}
549 data_path: rna_path_prop
551 def invoke(self, context, event):
552 wm = context.window_manager
553 data_path = self.data_path
554 value = context_path_validate(context, data_path)
556 if value is Ellipsis:
557 return {'PASS_THROUGH'}
559 base_path, prop_string = data_path.rsplit(".", 1)
560 value_base = context_path_validate(context, base_path)
561 prop = value_base.bl_rna.properties[prop_string]
563 def draw_cb(self, context):
565 layout.prop(value_base, prop_string, expand=True)
567 wm.popup_menu_pie(draw_func=draw_cb, title=prop.name, icon=prop.icon, event=event)
572 class WM_OT_operator_pie_enum(Operator):
573 bl_idname = "wm.operator_pie_enum"
574 bl_label = "Operator Enum Pie"
575 bl_options = {'UNDO', 'INTERNAL'}
577 data_path: StringProperty(
579 description="Operator name (in python as string)",
582 prop_string: StringProperty(
584 description="Property name (as a string)",
588 def invoke(self, context, event):
589 wm = context.window_manager
591 data_path = self.data_path
592 prop_string = self.prop_string
594 # same as eval("bpy.ops." + data_path)
595 op_mod_str, ob_id_str = data_path.split(".", 1)
596 op = getattr(getattr(bpy.ops, op_mod_str), ob_id_str)
597 del op_mod_str, ob_id_str
600 op_rna = op.get_rna_type()
602 self.report({'ERROR'}, "Operator not found: bpy.ops.%s" % data_path)
605 def draw_cb(self, context):
607 pie = layout.menu_pie()
608 pie.operator_enum(data_path, prop_string)
610 wm.popup_menu_pie(draw_func=draw_cb, title=op_rna.name, event=event)
615 class WM_OT_context_set_id(Operator):
616 """Set a context value to an ID data-block"""
617 bl_idname = "wm.context_set_id"
618 bl_label = "Set Library ID"
619 bl_options = {'UNDO', 'INTERNAL'}
621 data_path: rna_path_prop
622 value: StringProperty(
624 description="Assign value",
628 def execute(self, context):
630 data_path = self.data_path
632 # match the pointer type from the target property to bpy.data.*
633 # so we lookup the correct list.
634 data_path_base, data_path_prop = data_path.rsplit(".", 1)
635 data_prop_rna = eval("context.%s" % data_path_base).rna_type.properties[data_path_prop]
636 data_prop_rna_type = data_prop_rna.fixed_type
640 for prop in bpy.data.rna_type.properties:
641 if prop.rna_type.identifier == "CollectionProperty":
642 if prop.fixed_type == data_prop_rna_type:
643 id_iter = prop.identifier
647 value_id = getattr(bpy.data, id_iter).get(value)
648 exec("context.%s = value_id" % data_path)
650 return operator_path_undo_return(context, data_path)
653 doc_id = StringProperty(
659 data_path_iter = StringProperty(
660 description="The data path relative to the context, must point to an iterable")
662 data_path_item = StringProperty(
663 description="The data path from each iterable to the value (int or float)")
666 class WM_OT_context_collection_boolean_set(Operator):
667 """Set boolean values for a collection of items"""
668 bl_idname = "wm.context_collection_boolean_set"
669 bl_label = "Context Collection Boolean Set"
670 bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
672 data_path_iter: data_path_iter
673 data_path_item: data_path_item
677 items=(('TOGGLE', "Toggle", ""),
678 ('ENABLE', "Enable", ""),
679 ('DISABLE', "Disable", ""),
683 def execute(self, context):
684 data_path_iter = self.data_path_iter
685 data_path_item = self.data_path_item
687 items = list(getattr(context, data_path_iter))
692 value_orig = eval("item." + data_path_item)
696 if value_orig is True:
698 elif value_orig is False:
701 self.report({'WARNING'}, "Non boolean value found: %s[ ].%s" %
702 (data_path_iter, data_path_item))
705 items_ok.append(item)
707 # avoid undo push when nothing to do
711 if self.type == 'ENABLE':
713 elif self.type == 'DISABLE':
718 exec_str = "item.%s = %s" % (data_path_item, is_set)
719 for item in items_ok:
722 return operator_value_undo_return(item)
725 class WM_OT_context_modal_mouse(Operator):
726 """Adjust arbitrary values with mouse input"""
727 bl_idname = "wm.context_modal_mouse"
728 bl_label = "Context Modal Mouse"
729 bl_options = {'GRAB_CURSOR', 'BLOCKING', 'UNDO', 'INTERNAL'}
731 data_path_iter: data_path_iter
732 data_path_item: data_path_item
733 header_text: StringProperty(
735 description="Text to display in header during scale",
738 input_scale: FloatProperty(
739 description="Scale the mouse movement by this value before applying the delta",
742 invert: BoolProperty(
743 description="Invert the mouse input",
746 initial_x: IntProperty(options={'HIDDEN'})
748 def _values_store(self, context):
749 data_path_iter = self.data_path_iter
750 data_path_item = self.data_path_item
752 self._values = values = {}
754 for item in getattr(context, data_path_iter):
756 value_orig = eval("item." + data_path_item)
760 # check this can be set, maybe this is library data.
762 exec("item.%s = %s" % (data_path_item, value_orig))
766 values[item] = value_orig
768 def _values_delta(self, delta):
769 delta *= self.input_scale
773 data_path_item = self.data_path_item
774 for item, value_orig in self._values.items():
775 if type(value_orig) == int:
776 exec("item.%s = int(%d)" % (data_path_item, round(value_orig + delta)))
778 exec("item.%s = %f" % (data_path_item, value_orig + delta))
780 def _values_restore(self):
781 data_path_item = self.data_path_item
782 for item, value_orig in self._values.items():
783 exec("item.%s = %s" % (data_path_item, value_orig))
787 def _values_clear(self):
790 def modal(self, context, event):
791 event_type = event.type
793 if event_type == 'MOUSEMOVE':
794 delta = event.mouse_x - self.initial_x
795 self._values_delta(delta)
796 header_text = self.header_text
798 if len(self._values) == 1:
799 (item, ) = self._values.keys()
800 header_text = header_text % eval("item.%s" % self.data_path_item)
802 header_text = (self.header_text % delta) + " (delta)"
803 context.area.header_text_set(header_text)
805 elif 'LEFTMOUSE' == event_type:
806 item = next(iter(self._values.keys()))
808 context.area.header_text_set(None)
809 return operator_value_undo_return(item)
811 elif event_type in {'RIGHTMOUSE', 'ESC'}:
812 self._values_restore()
813 context.area.header_text_set(None)
816 return {'RUNNING_MODAL'}
818 def invoke(self, context, event):
819 self._values_store(context)
822 self.report({'WARNING'}, "Nothing to operate on: %s[ ].%s" %
823 (self.data_path_iter, self.data_path_item))
827 self.initial_x = event.mouse_x
829 context.window_manager.modal_handler_add(self)
830 return {'RUNNING_MODAL'}
833 class WM_OT_url_open(Operator):
834 """Open a website in the web-browser"""
835 bl_idname = "wm.url_open"
837 bl_options = {'INTERNAL'}
841 description="URL to open",
844 def execute(self, context):
846 webbrowser.open(self.url)
850 class WM_OT_path_open(Operator):
851 """Open a path in a file browser"""
852 bl_idname = "wm.path_open"
854 bl_options = {'INTERNAL'}
856 filepath: StringProperty(
858 options={'SKIP_SAVE'},
861 def execute(self, context):
866 filepath = self.filepath
869 self.report({'ERROR'}, "File path was not set")
872 filepath = bpy.path.abspath(filepath)
873 filepath = os.path.normpath(filepath)
875 if not os.path.exists(filepath):
876 self.report({'ERROR'}, "File '%s' not found" % filepath)
879 if sys.platform[:3] == "win":
880 os.startfile(filepath)
881 elif sys.platform == "darwin":
882 subprocess.check_call(["open", filepath])
885 subprocess.check_call(["xdg-open", filepath])
887 # xdg-open *should* be supported by recent Gnome, KDE, Xfce
889 traceback.print_exc()
894 def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""):
896 def operator_exists_pair(a, b):
897 # Not fast, this is only for docs.
898 return b in dir(getattr(bpy.ops, a))
900 def operator_exists_single(a):
901 a, b = a.partition("_OT_")[::2]
902 return operator_exists_pair(a.lower(), b)
904 id_split = doc_id.split(".")
907 if len(id_split) == 1: # rna, class
909 url = "%s/bpy.types.%s.html" % (url_prefix, id_split[0])
911 rna = "bpy.types.%s" % id_split[0]
913 elif len(id_split) == 2: # rna, class.prop
914 class_name, class_prop = id_split
916 # an operator (common case - just button referencing an op)
917 if operator_exists_pair(class_name, class_prop):
920 "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
921 (url_prefix, class_name, class_name, class_prop)
924 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
925 elif operator_exists_single(class_name):
926 # note: ignore the prop name since we don't have a way to link into it
927 class_name, class_prop = class_name.split("_OT_", 1)
928 class_name = class_name.lower()
931 "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
932 (url_prefix, class_name, class_name, class_prop)
935 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
937 # an RNA setting, common case
938 rna_class = getattr(bpy.types, class_name)
940 # detect if this is a inherited member and use that name instead
941 rna_parent = rna_class.bl_rna
942 rna_prop = rna_parent.properties.get(class_prop)
944 rna_parent = rna_parent.base
945 while rna_parent and rna_prop == rna_parent.properties.get(class_prop):
946 class_name = rna_parent.identifier
947 rna_parent = rna_parent.base
951 "%s/bpy.types.%s.html#bpy.types.%s.%s" %
952 (url_prefix, class_name, class_name, class_prop)
955 rna = "bpy.types.%s.%s" % (class_name, class_prop)
957 # We assume this is custom property, only try to generate generic url/rna_id...
959 url = ("%s/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items" % (url_prefix,))
961 rna = "bpy.types.bpy_struct"
963 return url if do_url else rna
966 class WM_OT_doc_view_manual(Operator):
967 """Load online manual"""
968 bl_idname = "wm.doc_view_manual"
969 bl_label = "View Manual"
974 def _find_reference(rna_id, url_mapping, verbose=True):
976 print("online manual check for: '%s'... " % rna_id)
977 from fnmatch import fnmatchcase
978 # XXX, for some reason all RNA ID's are stored lowercase
979 # Adding case into all ID's isn't worth the hassle so force lowercase.
980 rna_id = rna_id.lower()
981 for pattern, url_suffix in url_mapping:
982 if fnmatchcase(rna_id, pattern):
984 print(" match found: '%s' --> '%s'" % (pattern, url_suffix))
987 print("match not found")
991 def _lookup_rna_url(rna_id, verbose=True):
992 for prefix, url_manual_mapping in bpy.utils.manual_map():
993 rna_ref = WM_OT_doc_view_manual._find_reference(rna_id, url_manual_mapping, verbose=verbose)
994 if rna_ref is not None:
995 url = prefix + rna_ref
998 def execute(self, context):
999 rna_id = _wm_doc_get_id(self.doc_id, do_url=False)
1001 return {'PASS_THROUGH'}
1003 url = self._lookup_rna_url(rna_id)
1008 "No reference available %r, "
1009 "Update info in 'rna_manual_reference.py' "
1010 "or callback to bpy.utils.manual_map()" %
1013 return {'CANCELLED'}
1016 webbrowser.open(url)
1020 class WM_OT_doc_view(Operator):
1021 """Load online reference docs"""
1022 bl_idname = "wm.doc_view"
1023 bl_label = "View Documentation"
1026 if bpy.app.version_cycle == "release":
1027 _prefix = ("https://docs.blender.org/api/current")
1029 _prefix = ("https://docs.blender.org/api/blender2.8")
1031 def execute(self, context):
1032 url = _wm_doc_get_id(self.doc_id, do_url=True, url_prefix=self._prefix)
1034 return {'PASS_THROUGH'}
1037 webbrowser.open(url)
1042 rna_path = StringProperty(
1043 name="Property Edit",
1044 description="Property data_path edit",
1049 rna_value = StringProperty(
1050 name="Property Value",
1051 description="Property value edit",
1055 rna_property = StringProperty(
1056 name="Property Name",
1057 description="Property name edit",
1061 rna_min = FloatProperty(
1067 rna_max = FloatProperty(
1073 rna_use_soft_limits = BoolProperty(
1074 name="Use Soft Limits",
1077 rna_is_overridable_static = BoolProperty(
1078 name="Is Statically Overridable",
1083 class WM_OT_properties_edit(Operator):
1084 bl_idname = "wm.properties_edit"
1085 bl_label = "Edit Property"
1086 # register only because invoke_props_popup requires.
1087 bl_options = {'REGISTER', 'INTERNAL'}
1090 property: rna_property
1094 use_soft_limits: rna_use_soft_limits
1095 is_overridable_static: rna_is_overridable_static
1098 description: StringProperty(
1102 def _cmp_props_get(self):
1103 # Changing these properties will refresh the UI
1105 "use_soft_limits": self.use_soft_limits,
1106 "soft_range": (self.soft_min, self.soft_max),
1107 "hard_range": (self.min, self.max),
1110 def execute(self, context):
1111 from rna_prop_ui import (
1112 rna_idprop_ui_prop_get,
1113 rna_idprop_ui_prop_clear,
1114 rna_idprop_ui_prop_update,
1117 data_path = self.data_path
1119 prop = self.property
1121 prop_old = getattr(self, "_last_prop", [None])[0]
1123 if prop_old is None:
1124 self.report({'ERROR'}, "Direct execution not supported")
1125 return {'CANCELLED'}
1128 value_eval = eval(value)
1129 # assert else None -> None, not "None", see [#33431]
1130 assert(type(value_eval) in {str, float, int, bool, tuple, list})
1135 item = eval("context.%s" % data_path)
1136 prop_type_old = type(item[prop_old])
1138 rna_idprop_ui_prop_clear(item, prop_old)
1139 exec_str = "del item[%r]" % prop_old
1144 exec_str = "item[%r] = %s" % (prop, repr(value_eval))
1148 exec_str = "item.property_overridable_static_set('[\"%s\"]', %s)" % (prop, self.is_overridable_static)
1151 rna_idprop_ui_prop_update(item, prop)
1153 self._last_prop[:] = [prop]
1155 prop_type = type(item[prop])
1157 prop_ui = rna_idprop_ui_prop_get(item, prop)
1159 if prop_type in {float, int}:
1160 prop_ui["min"] = prop_type(self.min)
1161 prop_ui["max"] = prop_type(self.max)
1163 if self.use_soft_limits:
1164 prop_ui["soft_min"] = prop_type(self.soft_min)
1165 prop_ui["soft_max"] = prop_type(self.soft_max)
1167 prop_ui["soft_min"] = prop_type(self.min)
1168 prop_ui["soft_max"] = prop_type(self.max)
1170 prop_ui["description"] = self.description
1172 # If we have changed the type of the property, update its potential anim curves!
1173 if prop_type_old != prop_type:
1174 data_path = '["%s"]' % bpy.utils.escape_identifier(prop)
1177 def _update(fcurves):
1179 if fcu not in done and fcu.data_path == data_path:
1180 fcu.update_autoflags(item)
1183 def _update_strips(strips):
1185 if st.type == 'CLIP' and st.action:
1186 _update(st.action.fcurves)
1187 elif st.type == 'META':
1188 _update_strips(st.strips)
1190 adt = getattr(item, "animation_data", None)
1193 _update(adt.action.fcurves)
1195 _update(adt.drivers)
1197 for nt in adt.nla_tracks:
1198 _update_strips(nt.strips)
1200 # otherwise existing buttons which reference freed
1201 # memory may crash blender [#26510]
1202 # context.area.tag_redraw()
1203 for win in context.window_manager.windows:
1204 for area in win.screen.areas:
1209 def invoke(self, context, event):
1210 from rna_prop_ui import rna_idprop_ui_prop_get
1212 data_path = self.data_path
1215 self.report({'ERROR'}, "Data path not set")
1216 return {'CANCELLED'}
1218 self._last_prop = [self.property]
1220 item = eval("context.%s" % data_path)
1223 prop_ui = rna_idprop_ui_prop_get(item, self.property, False) # don't create
1225 self.min = prop_ui.get("min", -1000000000)
1226 self.max = prop_ui.get("max", 1000000000)
1227 self.description = prop_ui.get("description", "")
1229 self.soft_min = prop_ui.get("soft_min", self.min)
1230 self.soft_max = prop_ui.get("soft_max", self.max)
1231 self.use_soft_limits = (
1232 self.min != self.soft_min or
1233 self.max != self.soft_max
1236 # store for comparison
1237 self._cmp_props = self._cmp_props_get()
1239 wm = context.window_manager
1240 return wm.invoke_props_dialog(self)
1242 def check(self, context):
1243 cmp_props = self._cmp_props_get()
1245 if self._cmp_props != cmp_props:
1246 if cmp_props["use_soft_limits"]:
1247 if cmp_props["soft_range"] != self._cmp_props["soft_range"]:
1248 self.min = min(self.min, self.soft_min)
1249 self.max = max(self.max, self.soft_max)
1251 if cmp_props["hard_range"] != self._cmp_props["hard_range"]:
1252 self.soft_min = max(self.min, self.soft_min)
1253 self.soft_max = min(self.max, self.soft_max)
1256 if cmp_props["soft_range"] != cmp_props["hard_range"]:
1257 self.soft_min = self.min
1258 self.soft_max = self.max
1261 changed |= (cmp_props["use_soft_limits"] != self._cmp_props["use_soft_limits"])
1264 cmp_props = self._cmp_props_get()
1266 self._cmp_props = cmp_props
1270 def draw(self, context):
1271 layout = self.layout
1272 layout.prop(self, "property")
1273 layout.prop(self, "value")
1274 row = layout.row(align=True)
1275 row.prop(self, "min")
1276 row.prop(self, "max")
1279 row.prop(self, "use_soft_limits")
1280 row.prop(self, "is_overridable_static")
1282 row = layout.row(align=True)
1283 row.enabled = self.use_soft_limits
1284 row.prop(self, "soft_min", text="Soft Min")
1285 row.prop(self, "soft_max", text="Soft Max")
1286 layout.prop(self, "description")
1289 class WM_OT_properties_add(Operator):
1290 bl_idname = "wm.properties_add"
1291 bl_label = "Add Property"
1292 bl_options = {'UNDO', 'INTERNAL'}
1296 def execute(self, context):
1297 from rna_prop_ui import (
1298 rna_idprop_ui_prop_get,
1299 rna_idprop_ui_prop_update,
1302 data_path = self.data_path
1303 item = eval("context.%s" % data_path)
1305 def unique_name(names):
1309 while prop_new in names:
1310 prop_new = prop + str(i)
1315 prop = unique_name({
1317 *type(item).bl_rna.properties.keys(),
1321 rna_idprop_ui_prop_update(item, prop)
1323 # not essential, but without this we get [#31661]
1324 prop_ui = rna_idprop_ui_prop_get(item, prop)
1325 prop_ui["soft_min"] = prop_ui["min"] = 0.0
1326 prop_ui["soft_max"] = prop_ui["max"] = 1.0
1331 class WM_OT_properties_context_change(Operator):
1332 """Jump to a different tab inside the properties editor"""
1333 bl_idname = "wm.properties_context_change"
1335 bl_options = {'INTERNAL'}
1337 context: StringProperty(
1342 def execute(self, context):
1343 context.space_data.context = self.context
1347 class WM_OT_properties_remove(Operator):
1348 """Internal use (edit a property data_path)"""
1349 bl_idname = "wm.properties_remove"
1350 bl_label = "Remove Property"
1351 bl_options = {'UNDO', 'INTERNAL'}
1354 property: rna_property
1356 def execute(self, context):
1357 from rna_prop_ui import (
1358 rna_idprop_ui_prop_clear,
1359 rna_idprop_ui_prop_update,
1361 data_path = self.data_path
1362 item = eval("context.%s" % data_path)
1363 prop = self.property
1364 rna_idprop_ui_prop_update(item, prop)
1366 rna_idprop_ui_prop_clear(item, prop)
1371 class WM_OT_keyconfig_activate(Operator):
1372 bl_idname = "wm.keyconfig_activate"
1373 bl_label = "Activate Keyconfig"
1375 filepath: StringProperty(
1376 subtype='FILE_PATH',
1379 def execute(self, context):
1380 if bpy.utils.keyconfig_set(self.filepath, report=self.report):
1383 return {'CANCELLED'}
1386 class WM_OT_sysinfo(Operator):
1387 """Generate system information, saved into a text file"""
1389 bl_idname = "wm.sysinfo"
1390 bl_label = "Save System Info"
1392 filepath: StringProperty(
1393 subtype='FILE_PATH',
1394 options={'SKIP_SAVE'},
1397 def execute(self, context):
1399 sys_info.write_sysinfo(self.filepath)
1402 def invoke(self, context, event):
1405 if not self.filepath:
1406 self.filepath = os.path.join(
1407 os.path.expanduser("~"), "system-info.txt")
1409 wm = context.window_manager
1410 wm.fileselect_add(self)
1411 return {'RUNNING_MODAL'}
1414 class WM_OT_copy_prev_settings(Operator):
1415 """Copy settings from previous version"""
1416 bl_idname = "wm.copy_prev_settings"
1417 bl_label = "Copy Previous Settings"
1420 def previous_version():
1421 ver = bpy.app.version
1422 ver_old = ((ver[0] * 100) + ver[1]) - 1
1423 return ver_old // 100, ver_old % 100
1427 ver = bpy.app.version
1428 ver_old = ((ver[0] * 100) + ver[1]) - 1
1429 return bpy.utils.resource_path('USER', ver_old // 100, ver_old % 100)
1433 return bpy.utils.resource_path('USER')
1436 def poll(cls, context):
1439 old = cls._old_path()
1440 new = cls._new_path()
1441 if os.path.isdir(old) and not os.path.isdir(new):
1444 old_userpref = os.path.join(old, "config", "userpref.blend")
1445 new_userpref = os.path.join(new, "config", "userpref.blend")
1446 return os.path.isfile(old_userpref) and not os.path.isfile(new_userpref)
1448 def execute(self, context):
1451 shutil.copytree(self._old_path(), self._new_path(), symlinks=True)
1453 # reload recent-files.txt
1454 bpy.ops.wm.read_history()
1456 # don't loose users work if they open the splash later.
1457 if bpy.data.is_saved is bpy.data.is_dirty is False:
1458 bpy.ops.wm.read_homefile()
1460 self.report({'INFO'}, "Reload Start-Up file to restore settings")
1465 class WM_OT_keyconfig_test(Operator):
1466 """Test key-config for conflicts"""
1467 bl_idname = "wm.keyconfig_test"
1468 bl_label = "Test Key Configuration for Conflicts"
1470 def execute(self, context):
1471 from bpy_extras import keyconfig_utils
1473 wm = context.window_manager
1474 kc = wm.keyconfigs.default
1476 if keyconfig_utils.keyconfig_test(kc):
1482 class WM_OT_keyconfig_import(Operator):
1483 """Import key configuration from a python script"""
1484 bl_idname = "wm.keyconfig_import"
1485 bl_label = "Import Key Configuration..."
1487 filepath: StringProperty(
1488 subtype='FILE_PATH',
1489 default="keymap.py",
1491 filter_folder: BoolProperty(
1492 name="Filter folders",
1496 filter_text: BoolProperty(
1501 filter_python: BoolProperty(
1502 name="Filter python",
1506 keep_original: BoolProperty(
1507 name="Keep original",
1508 description="Keep original file after copying to configuration folder",
1512 def execute(self, context):
1514 from os.path import basename
1517 if not self.filepath:
1518 self.report({'ERROR'}, "Filepath not set")
1519 return {'CANCELLED'}
1521 config_name = basename(self.filepath)
1523 path = bpy.utils.user_resource('SCRIPTS', os.path.join("presets", "keyconfig"), create=True)
1524 path = os.path.join(path, config_name)
1527 if self.keep_original:
1528 shutil.copy(self.filepath, path)
1530 shutil.move(self.filepath, path)
1531 except Exception as ex:
1532 self.report({'ERROR'}, "Installing keymap failed: %s" % ex)
1533 return {'CANCELLED'}
1535 # sneaky way to check we're actually running the code.
1536 if bpy.utils.keyconfig_set(path, report=self.report):
1539 return {'CANCELLED'}
1541 def invoke(self, context, event):
1542 wm = context.window_manager
1543 wm.fileselect_add(self)
1544 return {'RUNNING_MODAL'}
1546 # This operator is also used by interaction presets saving - AddPresetBase
1549 class WM_OT_keyconfig_export(Operator):
1550 """Export key configuration to a python script"""
1551 bl_idname = "wm.keyconfig_export"
1552 bl_label = "Export Key Configuration..."
1557 description="Write all keymaps (not just user modified)",
1559 filepath: StringProperty(
1560 subtype='FILE_PATH',
1561 default="keymap.py",
1563 filter_folder: BoolProperty(
1564 name="Filter folders",
1568 filter_text: BoolProperty(
1573 filter_python: BoolProperty(
1574 name="Filter python",
1579 def execute(self, context):
1580 from bl_keymap_utils.io import keyconfig_export_as_data
1582 if not self.filepath:
1583 raise Exception("Filepath not set")
1585 if not self.filepath.endswith(".py"):
1586 self.filepath += ".py"
1588 wm = context.window_manager
1590 keyconfig_export_as_data(
1592 wm.keyconfigs.active,
1594 all_keymaps=self.all,
1599 def invoke(self, context, event):
1600 wm = context.window_manager
1601 wm.fileselect_add(self)
1602 return {'RUNNING_MODAL'}
1605 class WM_OT_keymap_restore(Operator):
1606 """Restore key map(s)"""
1607 bl_idname = "wm.keymap_restore"
1608 bl_label = "Restore Key Map(s)"
1612 description="Restore all keymaps to default",
1615 def execute(self, context):
1616 wm = context.window_manager
1619 for km in wm.keyconfigs.user.keymaps:
1620 km.restore_to_default()
1623 km.restore_to_default()
1628 class WM_OT_keyitem_restore(Operator):
1629 """Restore key map item"""
1630 bl_idname = "wm.keyitem_restore"
1631 bl_label = "Restore Key Map Item"
1633 item_id: IntProperty(
1634 name="Item Identifier",
1635 description="Identifier of the item to remove",
1639 def poll(cls, context):
1640 keymap = getattr(context, "keymap", None)
1643 def execute(self, context):
1645 kmi = km.keymap_items.from_id(self.item_id)
1647 if (not kmi.is_user_defined) and kmi.is_user_modified:
1648 km.restore_item_to_default(kmi)
1653 class WM_OT_keyitem_add(Operator):
1654 """Add key map item"""
1655 bl_idname = "wm.keyitem_add"
1656 bl_label = "Add Key Map Item"
1658 def execute(self, context):
1662 km.keymap_items.new_modal("", 'A', 'PRESS')
1664 km.keymap_items.new("none", 'A', 'PRESS')
1666 # clear filter and expand keymap so we can see the newly added item
1667 if context.space_data.filter_text != "":
1668 context.space_data.filter_text = ""
1669 km.show_expanded_items = True
1670 km.show_expanded_children = True
1675 class WM_OT_keyitem_remove(Operator):
1676 """Remove key map item"""
1677 bl_idname = "wm.keyitem_remove"
1678 bl_label = "Remove Key Map Item"
1680 item_id: IntProperty(
1681 name="Item Identifier",
1682 description="Identifier of the item to remove",
1686 def poll(cls, context):
1687 return hasattr(context, "keymap")
1689 def execute(self, context):
1691 kmi = km.keymap_items.from_id(self.item_id)
1692 km.keymap_items.remove(kmi)
1696 class WM_OT_keyconfig_remove(Operator):
1697 """Remove key config"""
1698 bl_idname = "wm.keyconfig_remove"
1699 bl_label = "Remove Key Config"
1702 def poll(cls, context):
1703 wm = context.window_manager
1704 keyconf = wm.keyconfigs.active
1705 return keyconf and keyconf.is_user_defined
1707 def execute(self, context):
1708 wm = context.window_manager
1709 keyconfig = wm.keyconfigs.active
1710 wm.keyconfigs.remove(keyconfig)
1714 class WM_OT_operator_cheat_sheet(Operator):
1715 """List all the Operators in a text-block, useful for scripting"""
1716 bl_idname = "wm.operator_cheat_sheet"
1717 bl_label = "Operator Cheat Sheet"
1719 def execute(self, context):
1722 for op_module_name in dir(bpy.ops):
1723 op_module = getattr(bpy.ops, op_module_name)
1724 for op_submodule_name in dir(op_module):
1725 op = getattr(op_module, op_submodule_name)
1727 if text.split("\n")[-1].startswith("bpy.ops."):
1728 op_strings.append(text)
1731 op_strings.append('')
1733 textblock = bpy.data.texts.new("OperatorList.txt")
1734 textblock.write('# %d Operators\n\n' % tot)
1735 textblock.write('\n'.join(op_strings))
1736 self.report({'INFO'}, "See OperatorList.txt textblock")
1740 # -----------------------------------------------------------------------------
1743 class WM_OT_addon_enable(Operator):
1744 """Enable an add-on"""
1745 bl_idname = "wm.addon_enable"
1746 bl_label = "Enable Add-on"
1748 module: StringProperty(
1750 description="Module name of the add-on to enable",
1753 def execute(self, context):
1761 err_str = traceback.format_exc()
1764 mod = addon_utils.enable(self.module, default_set=True, handle_error=err_cb)
1767 info = addon_utils.module_bl_info(mod)
1769 info_ver = info.get("blender", (0, 0, 0))
1771 if info_ver > bpy.app.version:
1774 "This script was written Blender "
1775 "version %d.%d.%d and might not "
1776 "function (correctly), "
1777 "though it is enabled" %
1784 self.report({'ERROR'}, err_str)
1786 return {'CANCELLED'}
1789 class WM_OT_addon_disable(Operator):
1790 """Disable an add-on"""
1791 bl_idname = "wm.addon_disable"
1792 bl_label = "Disable Add-on"
1794 module: StringProperty(
1796 description="Module name of the add-on to disable",
1799 def execute(self, context):
1807 err_str = traceback.format_exc()
1810 addon_utils.disable(self.module, default_set=True, handle_error=err_cb)
1813 self.report({'ERROR'}, err_str)
1818 class WM_OT_owner_enable(Operator):
1819 """Enable workspace owner ID"""
1820 bl_idname = "wm.owner_enable"
1821 bl_label = "Enable Add-on"
1823 owner_id: StringProperty(
1827 def execute(self, context):
1828 workspace = context.workspace
1829 workspace.owner_ids.new(self.owner_id)
1833 class WM_OT_owner_disable(Operator):
1834 """Enable workspace owner ID"""
1835 bl_idname = "wm.owner_disable"
1836 bl_label = "Disable UI Tag"
1838 owner_id: StringProperty(
1842 def execute(self, context):
1843 workspace = context.workspace
1844 owner_id = workspace.owner_ids[self.owner_id]
1845 workspace.owner_ids.remove(owner_id)
1849 class WM_OT_theme_install(Operator):
1850 """Load and apply a Blender XML theme file"""
1851 bl_idname = "wm.theme_install"
1852 bl_label = "Install Theme..."
1854 overwrite: BoolProperty(
1856 description="Remove existing theme file if exists",
1859 filepath: StringProperty(
1860 subtype='FILE_PATH',
1862 filter_folder: BoolProperty(
1863 name="Filter folders",
1867 filter_glob: StringProperty(
1872 def execute(self, context):
1877 xmlfile = self.filepath
1879 path_themes = bpy.utils.user_resource('SCRIPTS', "presets/interface_theme", create=True)
1882 self.report({'ERROR'}, "Failed to get themes path")
1883 return {'CANCELLED'}
1885 path_dest = os.path.join(path_themes, os.path.basename(xmlfile))
1887 if not self.overwrite:
1888 if os.path.exists(path_dest):
1889 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
1890 return {'CANCELLED'}
1893 shutil.copyfile(xmlfile, path_dest)
1894 bpy.ops.script.execute_preset(
1896 menu_idname="USERPREF_MT_interface_theme_presets",
1900 traceback.print_exc()
1901 return {'CANCELLED'}
1905 def invoke(self, context, event):
1906 wm = context.window_manager
1907 wm.fileselect_add(self)
1908 return {'RUNNING_MODAL'}
1911 class WM_OT_addon_refresh(Operator):
1912 """Scan add-on directories for new modules"""
1913 bl_idname = "wm.addon_refresh"
1914 bl_label = "Refresh"
1916 def execute(self, context):
1919 addon_utils.modules_refresh()
1924 # Note: shares some logic with WM_OT_app_template_install
1925 # but not enough to de-duplicate. Fixed here may apply to both.
1926 class WM_OT_addon_install(Operator):
1927 """Install an add-on"""
1928 bl_idname = "wm.addon_install"
1929 bl_label = "Install Add-on from File..."
1931 overwrite: BoolProperty(
1933 description="Remove existing add-ons with the same ID",
1936 target: EnumProperty(
1938 items=(('DEFAULT', "Default", ""),
1939 ('PREFS', "User Prefs", "")),
1942 filepath: StringProperty(
1943 subtype='FILE_PATH',
1945 filter_folder: BoolProperty(
1946 name="Filter folders",
1950 filter_python: BoolProperty(
1951 name="Filter python",
1955 filter_glob: StringProperty(
1956 default="*.py;*.zip",
1960 def execute(self, context):
1967 pyfile = self.filepath
1969 if self.target == 'DEFAULT':
1970 # don't use bpy.utils.script_paths("addons") because we may not be able to write to it.
1971 path_addons = bpy.utils.user_resource('SCRIPTS', "addons", create=True)
1973 path_addons = context.user_preferences.filepaths.script_directory
1975 path_addons = os.path.join(path_addons, "addons")
1978 self.report({'ERROR'}, "Failed to get add-ons path")
1979 return {'CANCELLED'}
1981 if not os.path.isdir(path_addons):
1983 os.makedirs(path_addons, exist_ok=True)
1985 traceback.print_exc()
1987 # Check if we are installing from a target path,
1988 # doing so causes 2+ addons of same name or when the same from/to
1989 # location is used, removal of the file!
1991 pyfile_dir = os.path.dirname(pyfile)
1992 for addon_path in addon_utils.paths():
1993 if os.path.samefile(pyfile_dir, addon_path):
1994 self.report({'ERROR'}, "Source file is in the add-on search path: %r" % addon_path)
1995 return {'CANCELLED'}
1998 # done checking for exceptional case
2000 addons_old = {mod.__name__ for mod in addon_utils.modules()}
2002 # check to see if the file is in compressed format (.zip)
2003 if zipfile.is_zipfile(pyfile):
2005 file_to_extract = zipfile.ZipFile(pyfile, 'r')
2007 traceback.print_exc()
2008 return {'CANCELLED'}
2011 for f in file_to_extract.namelist():
2012 module_filesystem_remove(path_addons, f)
2014 for f in file_to_extract.namelist():
2015 path_dest = os.path.join(path_addons, os.path.basename(f))
2016 if os.path.exists(path_dest):
2017 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
2018 return {'CANCELLED'}
2020 try: # extract the file to "addons"
2021 file_to_extract.extractall(path_addons)
2023 traceback.print_exc()
2024 return {'CANCELLED'}
2027 path_dest = os.path.join(path_addons, os.path.basename(pyfile))
2030 module_filesystem_remove(path_addons, os.path.basename(pyfile))
2031 elif os.path.exists(path_dest):
2032 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
2033 return {'CANCELLED'}
2035 # if not compressed file just copy into the addon path
2037 shutil.copyfile(pyfile, path_dest)
2039 traceback.print_exc()
2040 return {'CANCELLED'}
2042 addons_new = {mod.__name__ for mod in addon_utils.modules()} - addons_old
2043 addons_new.discard("modules")
2045 # disable any addons we may have enabled previously and removed.
2046 # this is unlikely but do just in case. bug [#23978]
2047 for new_addon in addons_new:
2048 addon_utils.disable(new_addon, default_set=True)
2050 # possible the zip contains multiple addons, we could disallow this
2051 # but for now just use the first
2052 for mod in addon_utils.modules(refresh=False):
2053 if mod.__name__ in addons_new:
2054 info = addon_utils.module_bl_info(mod)
2056 # show the newly installed addon.
2057 context.window_manager.addon_filter = 'All'
2058 context.window_manager.addon_search = info["name"]
2061 # in case a new module path was created to install this addon.
2062 bpy.utils.refresh_script_paths()
2066 tip_("Modules Installed (%s) from %r into %r") %
2067 (", ".join(sorted(addons_new)), pyfile, path_addons)
2070 self.report({'INFO'}, msg)
2074 def invoke(self, context, event):
2075 wm = context.window_manager
2076 wm.fileselect_add(self)
2077 return {'RUNNING_MODAL'}
2080 class WM_OT_addon_remove(Operator):
2081 """Delete the add-on from the file system"""
2082 bl_idname = "wm.addon_remove"
2083 bl_label = "Remove Add-on"
2085 module: StringProperty(
2087 description="Module name of the add-on to remove",
2091 def path_from_addon(module):
2095 for mod in addon_utils.modules():
2096 if mod.__name__ == module:
2097 filepath = mod.__file__
2098 if os.path.exists(filepath):
2099 if os.path.splitext(os.path.basename(filepath))[0] == "__init__":
2100 return os.path.dirname(filepath), True
2102 return filepath, False
2105 def execute(self, context):
2109 path, isdir = WM_OT_addon_remove.path_from_addon(self.module)
2111 self.report({'WARNING'}, "Add-on path %r could not be found" % path)
2112 return {'CANCELLED'}
2114 # in case its enabled
2115 addon_utils.disable(self.module, default_set=True)
2123 addon_utils.modules_refresh()
2125 context.area.tag_redraw()
2128 # lame confirmation check
2129 def draw(self, context):
2130 self.layout.label(text="Remove Add-on: %r?" % self.module)
2131 path, _isdir = WM_OT_addon_remove.path_from_addon(self.module)
2132 self.layout.label(text="Path: %r" % path)
2134 def invoke(self, context, event):
2135 wm = context.window_manager
2136 return wm.invoke_props_dialog(self, width=600)
2139 class WM_OT_addon_expand(Operator):
2140 """Display information and preferences for this add-on"""
2141 bl_idname = "wm.addon_expand"
2143 bl_options = {'INTERNAL'}
2145 module: StringProperty(
2147 description="Module name of the add-on to expand",
2150 def execute(self, context):
2153 module_name = self.module
2155 mod = addon_utils.addons_fake_modules.get(module_name)
2157 info = addon_utils.module_bl_info(mod)
2158 info["show_expanded"] = not info["show_expanded"]
2163 class WM_OT_addon_userpref_show(Operator):
2164 """Show add-on user preferences"""
2165 bl_idname = "wm.addon_userpref_show"
2167 bl_options = {'INTERNAL'}
2169 module: StringProperty(
2171 description="Module name of the add-on to expand",
2174 def execute(self, context):
2177 module_name = self.module
2179 _modules = addon_utils.modules(refresh=False)
2180 mod = addon_utils.addons_fake_modules.get(module_name)
2182 info = addon_utils.module_bl_info(mod)
2183 info["show_expanded"] = True
2185 context.user_preferences.active_section = 'ADDONS'
2186 context.window_manager.addon_filter = 'All'
2187 context.window_manager.addon_search = info["name"]
2188 bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
2193 # Note: shares some logic with WM_OT_addon_install
2194 # but not enough to de-duplicate. Fixes here may apply to both.
2195 class WM_OT_app_template_install(Operator):
2196 """Install an application-template"""
2197 bl_idname = "wm.app_template_install"
2198 bl_label = "Install Template from File..."
2200 overwrite: BoolProperty(
2202 description="Remove existing template with the same ID",
2206 filepath: StringProperty(
2207 subtype='FILE_PATH',
2209 filter_folder: BoolProperty(
2210 name="Filter folders",
2214 filter_glob: StringProperty(
2219 def execute(self, context):
2224 filepath = self.filepath
2226 path_app_templates = bpy.utils.user_resource(
2227 'SCRIPTS', os.path.join("startup", "bl_app_templates_user"),
2231 if not path_app_templates:
2232 self.report({'ERROR'}, "Failed to get add-ons path")
2233 return {'CANCELLED'}
2235 if not os.path.isdir(path_app_templates):
2237 os.makedirs(path_app_templates, exist_ok=True)
2239 traceback.print_exc()
2241 app_templates_old = set(os.listdir(path_app_templates))
2243 # check to see if the file is in compressed format (.zip)
2244 if zipfile.is_zipfile(filepath):
2246 file_to_extract = zipfile.ZipFile(filepath, 'r')
2248 traceback.print_exc()
2249 return {'CANCELLED'}
2252 for f in file_to_extract.namelist():
2253 module_filesystem_remove(path_app_templates, f)
2255 for f in file_to_extract.namelist():
2256 path_dest = os.path.join(path_app_templates, os.path.basename(f))
2257 if os.path.exists(path_dest):
2258 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
2259 return {'CANCELLED'}
2261 try: # extract the file to "bl_app_templates_user"
2262 file_to_extract.extractall(path_app_templates)
2264 traceback.print_exc()
2265 return {'CANCELLED'}
2268 # Only support installing zipfiles
2269 self.report({'WARNING'}, "Expected a zip-file %r\n" % filepath)
2270 return {'CANCELLED'}
2272 app_templates_new = set(os.listdir(path_app_templates)) - app_templates_old
2274 # in case a new module path was created to install this addon.
2275 bpy.utils.refresh_script_paths()
2279 tip_("Template Installed (%s) from %r into %r") %
2280 (", ".join(sorted(app_templates_new)), filepath, path_app_templates)
2283 self.report({'INFO'}, msg)
2287 def invoke(self, context, event):
2288 wm = context.window_manager
2289 wm.fileselect_add(self)
2290 return {'RUNNING_MODAL'}
2293 class WM_OT_tool_set_by_name(Operator):
2294 """Set the tool by name (for keymaps)"""
2295 bl_idname = "wm.tool_set_by_name"
2296 bl_label = "Set Tool By Name"
2298 name: StringProperty(
2300 description="Display name of the tool",
2302 cycle: BoolProperty(
2304 description="Cycle through tools in this group",
2306 options={'SKIP_SAVE'},
2309 space_type: rna_space_type_prop
2311 if use_toolbar_release_hack:
2312 def invoke(self, context, event):
2314 if not self.properties.is_property_set("name"):
2315 WM_OT_toolbar._key_held = False
2316 return {'PASS_THROUGH'}
2317 elif (WM_OT_toolbar._key_held == event.type) and (event.value != 'RELEASE'):
2318 return {'PASS_THROUGH'}
2319 WM_OT_toolbar._key_held = None
2321 return self.execute(context)
2323 def execute(self, context):
2324 from bl_ui.space_toolsystem_common import (
2326 activate_by_name_or_cycle,
2329 if self.properties.is_property_set("space_type"):
2330 space_type = self.space_type
2332 space_type = context.space_data.type
2334 fn = activate_by_name_or_cycle if self.cycle else activate_by_name
2335 if fn(context, space_type, self.name):
2338 self.report({'WARNING'}, f"Tool {self.name!r:s} not found for space {space_type!r:s}.")
2339 return {'CANCELLED'}
2342 class WM_OT_toolbar(Operator):
2343 bl_idname = "wm.toolbar"
2344 bl_label = "Toolbar"
2347 def poll(cls, context):
2348 return context.space_data is not None
2350 if use_toolbar_release_hack:
2352 def invoke(self, context, event):
2353 WM_OT_toolbar._key_held = event.type
2354 return self.execute(context)
2356 def execute(self, context):
2357 from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
2358 from bl_keymap_utils import keymap_from_toolbar
2360 space_type = context.space_data.type
2361 cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
2363 return {'CANCELLED'}
2365 wm = context.window_manager
2366 keymap = keymap_from_toolbar.generate(context, space_type)
2368 def draw_menu(popover, context):
2369 layout = popover.layout
2370 layout.operator_context = 'INVOKE_REGION_WIN'
2371 cls.draw_cls(layout, context, detect_layout=False, scale_y=1.0)
2373 wm.popover(draw_menu, ui_units_x=8, keymap=keymap)
2377 # Studio Light operations
2378 class WM_OT_studiolight_install(Operator):
2379 """Install a user defined studio light"""
2380 bl_idname = "wm.studiolight_install"
2381 bl_label = "Install Custom Studio Light"
2383 files: CollectionProperty(
2385 type=OperatorFileListElement,
2387 directory: StringProperty(
2390 filter_folder: BoolProperty(
2391 name="Filter folders",
2395 filter_glob: StringProperty(
2396 default="*.png;*.jpg;*.hdr;*.exr",
2401 ('MATCAP', "MatCap", ""),
2402 ('WORLD', "World", ""),
2403 ('STUDIO', "Studio", ""),
2407 def execute(self, context):
2411 userpref = context.user_preferences
2413 filepaths = [pathlib.Path(self.directory, e.name) for e in self.files]
2414 path_studiolights = bpy.utils.user_resource('DATAFILES')
2416 if not path_studiolights:
2417 self.report({'ERROR'}, "Failed to get Studio Light path")
2418 return {'CANCELLED'}
2420 path_studiolights = pathlib.Path(path_studiolights, "studiolights", self.type.lower())
2421 if not path_studiolights.exists():
2423 path_studiolights.mkdir(parents=True, exist_ok=True)
2425 traceback.print_exc()
2427 for filepath in filepaths:
2428 shutil.copy(str(filepath), str(path_studiolights))
2429 userpref.studio_lights.load(str(path_studiolights.joinpath(filepath.name)), self.type)
2433 tip_("StudioLight Installed %r into %r") %
2434 (", ".join(str(x.name) for x in self.files), str(path_studiolights))
2437 self.report({'INFO'}, msg)
2440 def invoke(self, context, event):
2441 wm = context.window_manager
2442 wm.fileselect_add(self)
2443 return {'RUNNING_MODAL'}
2446 class WM_OT_studiolight_new(Operator):
2447 """Save custom studio light from the studio light editor settings"""
2448 bl_idname = 'wm.studiolight_new'
2449 bl_label = "Save custom Studio light"
2451 filename: StringProperty(
2453 default="StudioLight",
2458 def execute(self, context):
2460 userpref = context.user_preferences
2461 wm = context.window_manager
2463 path_studiolights = bpy.utils.user_resource('DATAFILES')
2465 if not path_studiolights:
2466 self.report({'ERROR'}, "Failed to get Studio Light path")
2467 return {'CANCELLED'}
2469 path_studiolights = pathlib.Path(path_studiolights, "studiolights", "studio")
2470 if not path_studiolights.exists():
2472 path_studiolights.mkdir(parents=True, exist_ok=True)
2474 traceback.print_exc()
2476 finalpath = str(path_studiolights.joinpath(self.filename));
2477 if pathlib.Path(finalpath + ".sl").is_file():
2478 if not self.ask_overide:
2479 self.ask_overide = True
2480 return wm.invoke_props_dialog(self, width=600)
2482 for studio_light in userpref.studio_lights:
2483 if studio_light.name == self.filename + ".sl":
2484 bpy.ops.wm.studiolight_uninstall(index=studio_light.index)
2486 userpref.studio_lights.new(path=finalpath)
2490 tip_("StudioLight Installed %r into %r") %
2491 (self.filename, str(path_studiolights))
2494 self.report({'INFO'}, msg)
2497 def draw(self, context):
2498 layout = self.layout
2499 if self.ask_overide:
2500 layout.label(text="Warning, file already exists. Overwrite existing file?")
2502 layout.prop(self, "filename")
2504 def invoke(self, context, event):
2505 wm = context.window_manager
2506 return wm.invoke_props_dialog(self, width=600)
2509 class WM_OT_studiolight_uninstall(Operator):
2510 """Delete Studio Light"""
2511 bl_idname = 'wm.studiolight_uninstall'
2512 bl_label = "Uninstall Studio Light"
2513 index: bpy.props.IntProperty()
2515 def _remove_path(self, path):
2519 def execute(self, context):
2521 userpref = context.user_preferences
2522 for studio_light in userpref.studio_lights:
2523 if studio_light.index == self.index:
2524 if studio_light.path:
2525 self._remove_path(pathlib.Path(studio_light.path))
2526 if studio_light.path_irr_cache:
2527 self._remove_path(pathlib.Path(studio_light.path_irr_cache))
2528 if studio_light.path_sh_cache:
2529 self._remove_path(pathlib.Path(studio_light.path_sh_cache))
2530 userpref.studio_lights.remove(studio_light)
2532 return {'CANCELLED'}
2535 class WM_OT_studiolight_copy_settings(Operator):
2536 """Copy Studio Light settings to the Studio light editor"""
2537 bl_idname = 'wm.studiolight_copy_settings'
2538 bl_label = "Copy Studio Light settings"
2539 index: bpy.props.IntProperty()
2541 def execute(self, context):
2542 userpref = context.user_preferences
2543 system = userpref.system
2544 for studio_light in userpref.studio_lights:
2545 if studio_light.index == self.index:
2546 system.light_ambient = studio_light.light_ambient
2547 for sys_light, light in zip(system.solid_lights, studio_light.solid_lights):
2548 sys_light.use = light.use
2549 sys_light.diffuse_color = light.diffuse_color
2550 sys_light.specular_color = light.specular_color
2551 sys_light.smooth = light.smooth
2552 sys_light.direction = light.direction
2554 return {'CANCELLED'}
2557 class WM_OT_studiolight_userpref_show(Operator):
2558 """Show light user preferences"""
2559 bl_idname = "wm.studiolight_userpref_show"
2561 bl_options = {'INTERNAL'}
2563 def execute(self, context):
2564 context.user_preferences.active_section = 'LIGHTS'
2565 bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
2569 class WM_MT_splash(Menu):
2572 def draw_setup(self, context):
2573 wm = context.window_manager
2574 # userpref = context.user_preferences
2576 layout = self.layout
2578 layout.operator_context = 'EXEC_DEFAULT'
2580 layout.label(text="Quick Setup")
2582 split = layout.split(factor=0.25)
2584 split = split.split(factor=2.0 / 3.0)
2586 col = split.column()
2590 sub = col.split(factor=0.35)
2592 row.alignment = 'RIGHT'
2593 row.label(text="Shortcuts")
2594 text = bpy.path.display_name(wm.keyconfigs.active.name)
2597 sub.menu("USERPREF_MT_keyconfigs", text=text)
2599 kc = wm.keyconfigs.active
2600 kc_prefs = kc.preferences
2601 has_select_mouse = hasattr(kc_prefs, "select_mouse")
2602 if has_select_mouse:
2603 sub = col.split(factor=0.35)
2605 row.alignment = 'RIGHT'
2606 row.label(text="Select With")
2607 sub.row().prop(kc_prefs, "select_mouse", expand=True)
2608 has_select_mouse = True
2610 has_spacebar_action = hasattr(kc_prefs, "spacebar_action")
2611 if has_spacebar_action:
2612 sub = col.split(factor=0.35)
2614 row.alignment = 'RIGHT'
2615 row.label(text="Spacebar")
2616 sub.row().prop(kc_prefs, "spacebar_action", expand=True)
2617 has_select_mouse = True
2621 sub = col.split(factor=0.35)
2623 row.alignment = 'RIGHT'
2624 row.label(text="Theme")
2625 label = bpy.types.USERPREF_MT_interface_theme_presets.bl_label
2626 if label == "Presets":
2627 label = "Blender Dark"
2628 sub.menu("USERPREF_MT_interface_theme_presets", text=label)
2630 # We need to make switching to a language easier first
2631 #sub = col.split(factor=0.35)
2633 #row.alignment = 'RIGHT'
2634 #row.label(text="Language:")
2635 #userpref = context.user_preferences
2636 #sub.prop(userpref.system, "language", text="")
2638 # Keep height constant
2639 if not has_select_mouse:
2641 if not has_spacebar_action:
2649 if bpy.types.WM_OT_copy_prev_settings.poll(context):
2650 old_version = bpy.types.WM_OT_copy_prev_settings.previous_version()
2651 sub.operator("wm.copy_prev_settings", text="Load %d.%d Settings" % old_version)
2652 sub.operator("wm.save_userpref", text="Save New Settings")
2656 sub.operator("wm.save_userpref", text="Next")
2661 def draw(self, context):
2662 # Draw setup screen if no user preferences have been saved yet.
2665 user_path = bpy.utils.resource_path('USER')
2666 userdef_path = os.path.join(user_path, "config", "userpref.blend")
2668 if not os.path.isfile(userdef_path):
2669 self.draw_setup(context)
2673 layout = self.layout
2674 layout.operator_context = 'EXEC_DEFAULT'
2675 layout.emboss = 'PULLDOWN_MENU'
2677 split = layout.split()
2680 col1 = split.column()
2681 col1.label(text="New File")
2683 bpy.types.TOPBAR_MT_file_new.draw_ex(col1, context, use_splash=True)
2686 col2 = split.column()
2687 col2_title = col2.row()
2689 found_recent = col2.template_recent_files()
2692 col2_title.label(text="Recent Files")
2694 # Links if no recent files
2695 col2_title.label(text="Getting Started")
2698 "wm.url_open", text="Manual", icon='URL'
2699 ).url = "https://docs.blender.org/manual/en/dev/"
2701 "wm.url_open", text="Release Notes", icon='URL',
2702 ).url = "https://www.blender.org/download/releases/%d-%d/" % bpy.app.version[:2]
2704 "wm.url_open", text="Blender Website", icon='URL',
2705 ).url = "https://www.blender.org"
2707 "wm.url_open", text="Credits", icon='URL',
2708 ).url = "https://www.blender.org/about/credits/"
2712 split = layout.split()
2714 col1 = split.column()
2716 sub.operator_context = 'INVOKE_DEFAULT'
2717 sub.operator("wm.open_mainfile", text="Open...", icon='FILE_FOLDER')
2718 col1.operator("wm.recover_last_session", icon='RECOVER_LAST')
2720 col2 = split.column()
2723 "wm.url_open", text="Release Notes", icon='URL',
2724 ).url = "https://www.blender.org/download/releases/%d-%d/" % bpy.app.version[:2]
2726 "wm.url_open", text="Development Fund", icon='URL'
2727 ).url = "https://fund.blender.org"
2730 "wm.url_open", text="Development Fund", icon='URL'
2731 ).url = "https://fund.blender.org"
2733 "wm.url_open", text="Donate", icon='URL'
2734 ).url = "https://www.blender.org/foundation/donation-payment/"
2740 class WM_OT_drop_blend_file(Operator):
2741 bl_idname = "wm.drop_blend_file"
2742 bl_label = "Handle dropped .blend file"
2743 bl_options = {'INTERNAL'}
2745 filepath: StringProperty()
2747 def invoke(self, context, event):
2748 context.window_manager.popup_menu(self.draw_menu, title=bpy.path.basename(self.filepath), icon='QUESTION')
2751 def draw_menu(self, menu, context):
2752 layout = menu.layout
2754 col = layout.column()
2755 col.operator_context = 'EXEC_DEFAULT'
2756 col.operator("wm.open_mainfile", text="Open", icon='FILE_FOLDER').filepath = self.filepath
2759 col = layout.column()
2760 col.operator_context = 'INVOKE_DEFAULT'
2761 col.operator("wm.link", text="Link...", icon='LINK_BLEND').filepath = self.filepath
2762 col.operator("wm.append", text="Append...", icon='APPEND_BLEND').filepath = self.filepath
2765 WM_OT_addon_disable,
2768 WM_OT_addon_install,
2769 WM_OT_addon_refresh,
2771 WM_OT_addon_userpref_show,
2772 WM_OT_app_template_install,
2773 WM_OT_context_collection_boolean_set,
2774 WM_OT_context_cycle_array,
2775 WM_OT_context_cycle_enum,
2776 WM_OT_context_cycle_int,
2777 WM_OT_context_menu_enum,
2778 WM_OT_context_modal_mouse,
2779 WM_OT_context_pie_enum,
2780 WM_OT_context_scale_float,
2781 WM_OT_context_scale_int,
2782 WM_OT_context_set_boolean,
2783 WM_OT_context_set_enum,
2784 WM_OT_context_set_float,
2785 WM_OT_context_set_id,
2786 WM_OT_context_set_int,
2787 WM_OT_context_set_string,
2788 WM_OT_context_set_value,
2789 WM_OT_context_toggle,
2790 WM_OT_context_toggle_enum,
2791 WM_OT_copy_prev_settings,
2793 WM_OT_doc_view_manual,
2794 WM_OT_drop_blend_file,
2795 WM_OT_keyconfig_activate,
2796 WM_OT_keyconfig_export,
2797 WM_OT_keyconfig_import,
2798 WM_OT_keyconfig_remove,
2799 WM_OT_keyconfig_test,
2801 WM_OT_keyitem_remove,
2802 WM_OT_keyitem_restore,
2803 WM_OT_keymap_restore,
2804 WM_OT_operator_cheat_sheet,
2805 WM_OT_operator_pie_enum,
2807 WM_OT_properties_add,
2808 WM_OT_properties_context_change,
2809 WM_OT_properties_edit,
2810 WM_OT_properties_remove,
2812 WM_OT_theme_install,
2813 WM_OT_owner_disable,
2816 WM_OT_studiolight_install,
2817 WM_OT_studiolight_new,
2818 WM_OT_studiolight_uninstall,
2819 WM_OT_studiolight_copy_settings,
2820 WM_OT_studiolight_userpref_show,
2821 WM_OT_tool_set_by_name,