Merge branch 'master' into blender2.8
[blender.git] / release / scripts / startup / bl_operators / wm.py
1 # ##### BEGIN GPL LICENSE BLOCK #####
2 #
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.
7 #
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.
12 #
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.
16 #
17 # ##### END GPL LICENSE BLOCK #####
18
19 # <pep8 compliant>
20
21 import bpy
22 from bpy.types import (
23     Menu,
24     Operator,
25     OperatorFileListElement
26 )
27 from bpy.props import (
28     BoolProperty,
29     EnumProperty,
30     FloatProperty,
31     IntProperty,
32     StringProperty,
33     CollectionProperty,
34 )
35
36 from bpy.app.translations import pgettext_tip as tip_
37
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
41
42
43 rna_path_prop = StringProperty(
44     name="Context Attributes",
45     description="RNA context string",
46     maxlen=1024,
47 )
48
49 rna_reverse_prop = BoolProperty(
50     name="Reverse",
51     description="Cycle backwards",
52     default=False,
53 )
54
55 rna_wrap_prop = BoolProperty(
56     name="Wrap",
57     description="Wrap back to the first/last values",
58     default=False,
59 )
60
61 rna_relative_prop = BoolProperty(
62     name="Relative",
63     description="Apply relative to the current value (delta)",
64     default=False,
65 )
66
67 rna_space_type_prop = EnumProperty(
68     name="Type",
69     items=tuple(
70         (e.identifier, e.name, "", e. value)
71         for e in bpy.types.Space.bl_rna.properties["type"].enum_items
72     ),
73     default='EMPTY',
74 )
75
76
77 def context_path_validate(context, data_path):
78     try:
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
83             value = Ellipsis
84         else:
85             # We have a real error in the rna path, don't ignore that
86             raise
87
88     return value
89
90
91 def operator_value_is_undo(value):
92     if value in {None, Ellipsis}:
93         return False
94
95     # typical properties or objects
96     id_data = getattr(value, "id_data", Ellipsis)
97
98     if id_data is None:
99         return False
100     elif id_data is Ellipsis:
101         # handle mathutils types
102         id_data = getattr(getattr(value, "owner", None), "id_data", None)
103
104         if id_data is None:
105             return False
106
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,
110                                       bpy.types.Screen,
111                                       bpy.types.Brush,
112                                       ))))
113
114
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!
118     #
119     # When we can't find the data owner assume no undo is needed.
120     data_path_head = data_path.rpartition(".")[0]
121
122     if not data_path_head:
123         return False
124
125     value = context_path_validate(context, data_path_head)
126
127     return operator_value_is_undo(value)
128
129
130 def operator_path_undo_return(context, data_path):
131     return {'FINISHED'} if operator_path_is_undo(context, data_path) else {'CANCELLED'}
132
133
134 def operator_value_undo_return(value):
135     return {'FINISHED'} if operator_value_is_undo(value) else {'CANCELLED'}
136
137
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'}
142
143     if getattr(self, "relative", False):
144         exec("context.%s += self.value" % data_path)
145     else:
146         exec("context.%s = self.value" % data_path)
147
148     return operator_path_undo_return(context, data_path)
149
150
151 def module_filesystem_remove(path_base, module_name):
152     import os
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)
158
159             if os.path.isdir(f_full):
160                 os.rmdir(f_full)
161             else:
162                 os.remove(f_full)
163
164
165 class BRUSH_OT_active_index_set(Operator):
166     """Set active sculpt/paint brush from it's number"""
167     bl_idname = "brush.active_index_set"
168     bl_label = "Set Brush Number"
169
170     mode: StringProperty(
171         name="Mode",
172         description="Paint mode to set brush for",
173         maxlen=1024,
174     )
175     index: IntProperty(
176         name="Number",
177         description="Brush number",
178     )
179
180     _attr_dict = {
181         "sculpt": "use_paint_sculpt",
182         "vertex_paint": "use_paint_vertex",
183         "weight_paint": "use_paint_weight",
184         "image_paint": "use_paint_image",
185     }
186
187     def execute(self, context):
188         attr = self._attr_dict.get(self.mode)
189         if attr is None:
190             return {'CANCELLED'}
191
192         toolsettings = context.tool_settings
193         for i, brush in enumerate((cur for cur in bpy.data.brushes if getattr(cur, attr))):
194             if i == self.index:
195                 getattr(toolsettings, self.mode).brush = brush
196                 return {'FINISHED'}
197
198         return {'CANCELLED'}
199
200
201 class WM_OT_context_set_boolean(Operator):
202     """Set a context value"""
203     bl_idname = "wm.context_set_boolean"
204     bl_label = "Context Set Boolean"
205     bl_options = {'UNDO', 'INTERNAL'}
206
207     data_path: rna_path_prop
208     value: BoolProperty(
209         name="Value",
210         description="Assignment value",
211         default=True,
212     )
213
214     execute = execute_context_assign
215
216
217 class WM_OT_context_set_int(Operator):  # same as enum
218     """Set a context value"""
219     bl_idname = "wm.context_set_int"
220     bl_label = "Context Set"
221     bl_options = {'UNDO', 'INTERNAL'}
222
223     data_path: rna_path_prop
224     value: IntProperty(
225         name="Value",
226         description="Assign value",
227         default=0,
228     )
229     relative: rna_relative_prop
230
231     execute = execute_context_assign
232
233
234 class WM_OT_context_scale_float(Operator):
235     """Scale a float context value"""
236     bl_idname = "wm.context_scale_float"
237     bl_label = "Context Scale Float"
238     bl_options = {'UNDO', 'INTERNAL'}
239
240     data_path: rna_path_prop
241     value: FloatProperty(
242         name="Value",
243         description="Assign value",
244         default=1.0,
245     )
246
247     def execute(self, context):
248         data_path = self.data_path
249         if context_path_validate(context, data_path) is Ellipsis:
250             return {'PASS_THROUGH'}
251
252         value = self.value
253
254         if value == 1.0:  # nothing to do
255             return {'CANCELLED'}
256
257         exec("context.%s *= value" % data_path)
258
259         return operator_path_undo_return(context, data_path)
260
261
262 class WM_OT_context_scale_int(Operator):
263     """Scale an int context value"""
264     bl_idname = "wm.context_scale_int"
265     bl_label = "Context Scale Int"
266     bl_options = {'UNDO', 'INTERNAL'}
267
268     data_path: rna_path_prop
269     value: FloatProperty(
270         name="Value",
271         description="Assign value",
272         default=1.0,
273     )
274     always_step: BoolProperty(
275         name="Always Step",
276         description="Always adjust the value by a minimum of 1 when 'value' is not 1.0",
277         default=True,
278     )
279
280     def execute(self, context):
281         data_path = self.data_path
282         if context_path_validate(context, data_path) is Ellipsis:
283             return {'PASS_THROUGH'}
284
285         value = self.value
286
287         if value == 1.0:  # nothing to do
288             return {'CANCELLED'}
289
290         if getattr(self, "always_step", False):
291             if value > 1.0:
292                 add = "1"
293                 func = "max"
294             else:
295                 add = "-1"
296                 func = "min"
297             exec("context.%s = %s(round(context.%s * value), context.%s + %s)" %
298                  (data_path, func, data_path, data_path, add))
299         else:
300             exec("context.%s *= value" % data_path)
301
302         return operator_path_undo_return(context, data_path)
303
304
305 class WM_OT_context_set_float(Operator):  # same as enum
306     """Set a context value"""
307     bl_idname = "wm.context_set_float"
308     bl_label = "Context Set Float"
309     bl_options = {'UNDO', 'INTERNAL'}
310
311     data_path: rna_path_prop
312     value: FloatProperty(
313         name="Value",
314         description="Assignment value",
315         default=0.0,
316     )
317     relative: rna_relative_prop
318
319     execute = execute_context_assign
320
321
322 class WM_OT_context_set_string(Operator):  # same as enum
323     """Set a context value"""
324     bl_idname = "wm.context_set_string"
325     bl_label = "Context Set String"
326     bl_options = {'UNDO', 'INTERNAL'}
327
328     data_path: rna_path_prop
329     value: StringProperty(
330         name="Value",
331         description="Assign value",
332         maxlen=1024,
333     )
334
335     execute = execute_context_assign
336
337
338 class WM_OT_context_set_enum(Operator):
339     """Set a context value"""
340     bl_idname = "wm.context_set_enum"
341     bl_label = "Context Set Enum"
342     bl_options = {'UNDO', 'INTERNAL'}
343
344     data_path: rna_path_prop
345     value: StringProperty(
346         name="Value",
347         description="Assignment value (as a string)",
348         maxlen=1024,
349     )
350
351     execute = execute_context_assign
352
353
354 class WM_OT_context_set_value(Operator):
355     """Set a context value"""
356     bl_idname = "wm.context_set_value"
357     bl_label = "Context Set Value"
358     bl_options = {'UNDO', 'INTERNAL'}
359
360     data_path: rna_path_prop
361     value: StringProperty(
362         name="Value",
363         description="Assignment value (as a string)",
364         maxlen=1024,
365     )
366
367     def execute(self, context):
368         data_path = self.data_path
369         if context_path_validate(context, data_path) is Ellipsis:
370             return {'PASS_THROUGH'}
371         exec("context.%s = %s" % (data_path, self.value))
372         return operator_path_undo_return(context, data_path)
373
374
375 class WM_OT_context_toggle(Operator):
376     """Toggle a context value"""
377     bl_idname = "wm.context_toggle"
378     bl_label = "Context Toggle"
379     bl_options = {'UNDO', 'INTERNAL'}
380
381     data_path: rna_path_prop
382
383     def execute(self, context):
384         data_path = self.data_path
385
386         if context_path_validate(context, data_path) is Ellipsis:
387             return {'PASS_THROUGH'}
388
389         exec("context.%s = not (context.%s)" % (data_path, data_path))
390
391         return operator_path_undo_return(context, data_path)
392
393
394 class WM_OT_context_toggle_enum(Operator):
395     """Toggle a context value"""
396     bl_idname = "wm.context_toggle_enum"
397     bl_label = "Context Toggle Values"
398     bl_options = {'UNDO', 'INTERNAL'}
399
400     data_path: rna_path_prop
401     value_1: StringProperty(
402         name="Value",
403         description="Toggle enum",
404         maxlen=1024,
405     )
406     value_2: StringProperty(
407         name="Value",
408         description="Toggle enum",
409         maxlen=1024,
410     )
411
412     def execute(self, context):
413         data_path = self.data_path
414
415         if context_path_validate(context, data_path) is Ellipsis:
416             return {'PASS_THROUGH'}
417
418         # failing silently is not ideal, but we don't want errors for shortcut
419         # keys that some values that are only available in a particular context
420         try:
421             exec("context.%s = ('%s', '%s')[context.%s != '%s']" %
422                  (data_path, self.value_1,
423                   self.value_2, data_path,
424                   self.value_2,
425                   ))
426         except:
427             return {'PASS_THROUGH'}
428
429         return operator_path_undo_return(context, data_path)
430
431
432 class WM_OT_context_cycle_int(Operator):
433     """Set a context value (useful for cycling active material, """ \
434         """vertex keys, groups, etc.)"""
435     bl_idname = "wm.context_cycle_int"
436     bl_label = "Context Int Cycle"
437     bl_options = {'UNDO', 'INTERNAL'}
438
439     data_path: rna_path_prop
440     reverse: rna_reverse_prop
441     wrap: rna_wrap_prop
442
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'}
448
449         if self.reverse:
450             value -= 1
451         else:
452             value += 1
453
454         exec("context.%s = value" % data_path)
455
456         if self.wrap:
457             if value != eval("context.%s" % data_path):
458                 # relies on rna clamping integers out of the range
459                 if self.reverse:
460                     value = (1 << 31) - 1
461                 else:
462                     value = -1 << 31
463
464                 exec("context.%s = value" % data_path)
465
466         return operator_path_undo_return(context, data_path)
467
468
469 class WM_OT_context_cycle_enum(Operator):
470     """Toggle a context value"""
471     bl_idname = "wm.context_cycle_enum"
472     bl_label = "Context Enum Cycle"
473     bl_options = {'UNDO', 'INTERNAL'}
474
475     data_path: rna_path_prop
476     reverse: rna_reverse_prop
477     wrap: rna_wrap_prop
478
479     def execute(self, context):
480         data_path = self.data_path
481         value = context_path_validate(context, data_path)
482         if value is Ellipsis:
483             return {'PASS_THROUGH'}
484
485         orig_value = value
486
487         # Have to get rna enum values
488         rna_struct_str, rna_prop_str = data_path.rsplit('.', 1)
489         i = rna_prop_str.find('[')
490
491         # just in case we get "context.foo.bar[0]"
492         if i != -1:
493             rna_prop_str = rna_prop_str[0:i]
494
495         rna_struct = eval("context.%s.rna_type" % rna_struct_str)
496
497         rna_prop = rna_struct.properties[rna_prop_str]
498
499         if type(rna_prop) != bpy.types.EnumProperty:
500             raise Exception("expected an enum property")
501
502         enums = rna_struct.properties[rna_prop_str].enum_items.keys()
503         orig_index = enums.index(orig_value)
504
505         # Have the info we need, advance to the next item.
506         #
507         # When wrap's disabled we may set the value to its self,
508         # this is done to ensure update callbacks run.
509         if self.reverse:
510             if orig_index == 0:
511                 advance_enum = enums[-1] if self.wrap else enums[0]
512             else:
513                 advance_enum = enums[orig_index - 1]
514         else:
515             if orig_index == len(enums) - 1:
516                 advance_enum = enums[0] if self.wrap else enums[-1]
517             else:
518                 advance_enum = enums[orig_index + 1]
519
520         # set the new value
521         exec("context.%s = advance_enum" % data_path)
522         return operator_path_undo_return(context, data_path)
523
524
525 class WM_OT_context_cycle_array(Operator):
526     """Set a context array value """ \
527         """(useful for cycling the active mesh edit mode)"""
528     bl_idname = "wm.context_cycle_array"
529     bl_label = "Context Array Cycle"
530     bl_options = {'UNDO', 'INTERNAL'}
531
532     data_path: rna_path_prop
533     reverse: rna_reverse_prop
534
535     def execute(self, context):
536         data_path = self.data_path
537         value = context_path_validate(context, data_path)
538         if value is Ellipsis:
539             return {'PASS_THROUGH'}
540
541         def cycle(array):
542             if self.reverse:
543                 array.insert(0, array.pop())
544             else:
545                 array.append(array.pop(0))
546             return array
547
548         exec("context.%s = cycle(context.%s[:])" % (data_path, data_path))
549
550         return operator_path_undo_return(context, data_path)
551
552
553 class WM_OT_context_menu_enum(Operator):
554     bl_idname = "wm.context_menu_enum"
555     bl_label = "Context Enum Menu"
556     bl_options = {'UNDO', 'INTERNAL'}
557
558     data_path: rna_path_prop
559
560     def execute(self, context):
561         data_path = self.data_path
562         value = context_path_validate(context, data_path)
563
564         if value is Ellipsis:
565             return {'PASS_THROUGH'}
566
567         base_path, prop_string = data_path.rsplit(".", 1)
568         value_base = context_path_validate(context, base_path)
569         prop = value_base.bl_rna.properties[prop_string]
570
571         def draw_cb(self, context):
572             layout = self.layout
573             layout.prop(value_base, prop_string, expand=True)
574
575         context.window_manager.popup_menu(draw_func=draw_cb, title=prop.name, icon=prop.icon)
576
577         return {'FINISHED'}
578
579
580 class WM_OT_context_pie_enum(Operator):
581     bl_idname = "wm.context_pie_enum"
582     bl_label = "Context Enum Pie"
583     bl_options = {'UNDO', 'INTERNAL'}
584
585     data_path: rna_path_prop
586
587     def invoke(self, context, event):
588         wm = context.window_manager
589         data_path = self.data_path
590         value = context_path_validate(context, data_path)
591
592         if value is Ellipsis:
593             return {'PASS_THROUGH'}
594
595         base_path, prop_string = data_path.rsplit(".", 1)
596         value_base = context_path_validate(context, base_path)
597         prop = value_base.bl_rna.properties[prop_string]
598
599         def draw_cb(self, context):
600             layout = self.layout
601             layout.prop(value_base, prop_string, expand=True)
602
603         wm.popup_menu_pie(draw_func=draw_cb, title=prop.name, icon=prop.icon, event=event)
604
605         return {'FINISHED'}
606
607
608 class WM_OT_operator_pie_enum(Operator):
609     bl_idname = "wm.operator_pie_enum"
610     bl_label = "Operator Enum Pie"
611     bl_options = {'UNDO', 'INTERNAL'}
612
613     data_path: StringProperty(
614         name="Operator",
615         description="Operator name (in python as string)",
616         maxlen=1024,
617     )
618     prop_string: StringProperty(
619         name="Property",
620         description="Property name (as a string)",
621         maxlen=1024,
622     )
623
624     def invoke(self, context, event):
625         wm = context.window_manager
626
627         data_path = self.data_path
628         prop_string = self.prop_string
629
630         # same as eval("bpy.ops." + data_path)
631         op_mod_str, ob_id_str = data_path.split(".", 1)
632         op = getattr(getattr(bpy.ops, op_mod_str), ob_id_str)
633         del op_mod_str, ob_id_str
634
635         try:
636             op_rna = op.get_rna_type()
637         except KeyError:
638             self.report({'ERROR'}, "Operator not found: bpy.ops.%s" % data_path)
639             return {'CANCELLED'}
640
641         def draw_cb(self, context):
642             layout = self.layout
643             pie = layout.menu_pie()
644             pie.operator_enum(data_path, prop_string)
645
646         wm.popup_menu_pie(draw_func=draw_cb, title=op_rna.name, event=event)
647
648         return {'FINISHED'}
649
650
651 class WM_OT_context_set_id(Operator):
652     """Set a context value to an ID data-block"""
653     bl_idname = "wm.context_set_id"
654     bl_label = "Set Library ID"
655     bl_options = {'UNDO', 'INTERNAL'}
656
657     data_path: rna_path_prop
658     value: StringProperty(
659         name="Value",
660         description="Assign value",
661         maxlen=1024,
662     )
663
664     def execute(self, context):
665         value = self.value
666         data_path = self.data_path
667
668         # match the pointer type from the target property to bpy.data.*
669         # so we lookup the correct list.
670         data_path_base, data_path_prop = data_path.rsplit(".", 1)
671         data_prop_rna = eval("context.%s" % data_path_base).rna_type.properties[data_path_prop]
672         data_prop_rna_type = data_prop_rna.fixed_type
673
674         id_iter = None
675
676         for prop in bpy.data.rna_type.properties:
677             if prop.rna_type.identifier == "CollectionProperty":
678                 if prop.fixed_type == data_prop_rna_type:
679                     id_iter = prop.identifier
680                     break
681
682         if id_iter:
683             value_id = getattr(bpy.data, id_iter).get(value)
684             exec("context.%s = value_id" % data_path)
685
686         return operator_path_undo_return(context, data_path)
687
688
689 doc_id = StringProperty(
690     name="Doc ID",
691     maxlen=1024,
692     options={'HIDDEN'},
693 )
694
695 data_path_iter = StringProperty(
696     description="The data path relative to the context, must point to an iterable")
697
698 data_path_item = StringProperty(
699     description="The data path from each iterable to the value (int or float)")
700
701
702 class WM_OT_context_collection_boolean_set(Operator):
703     """Set boolean values for a collection of items"""
704     bl_idname = "wm.context_collection_boolean_set"
705     bl_label = "Context Collection Boolean Set"
706     bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
707
708     data_path_iter: data_path_iter
709     data_path_item: data_path_item
710
711     type: EnumProperty(
712         name="Type",
713         items=(('TOGGLE', "Toggle", ""),
714                ('ENABLE', "Enable", ""),
715                ('DISABLE', "Disable", ""),
716                ),
717     )
718
719     def execute(self, context):
720         data_path_iter = self.data_path_iter
721         data_path_item = self.data_path_item
722
723         items = list(getattr(context, data_path_iter))
724         items_ok = []
725         is_set = False
726         for item in items:
727             try:
728                 value_orig = eval("item." + data_path_item)
729             except:
730                 continue
731
732             if value_orig is True:
733                 is_set = True
734             elif value_orig is False:
735                 pass
736             else:
737                 self.report({'WARNING'}, "Non boolean value found: %s[ ].%s" %
738                             (data_path_iter, data_path_item))
739                 return {'CANCELLED'}
740
741             items_ok.append(item)
742
743         # avoid undo push when nothing to do
744         if not items_ok:
745             return {'CANCELLED'}
746
747         if self.type == 'ENABLE':
748             is_set = True
749         elif self.type == 'DISABLE':
750             is_set = False
751         else:
752             is_set = not is_set
753
754         exec_str = "item.%s = %s" % (data_path_item, is_set)
755         for item in items_ok:
756             exec(exec_str)
757
758         return operator_value_undo_return(item)
759
760
761 class WM_OT_context_modal_mouse(Operator):
762     """Adjust arbitrary values with mouse input"""
763     bl_idname = "wm.context_modal_mouse"
764     bl_label = "Context Modal Mouse"
765     bl_options = {'GRAB_CURSOR', 'BLOCKING', 'UNDO', 'INTERNAL'}
766
767     data_path_iter: data_path_iter
768     data_path_item: data_path_item
769     header_text: StringProperty(
770         name="Header Text",
771         description="Text to display in header during scale",
772     )
773
774     input_scale: FloatProperty(
775         description="Scale the mouse movement by this value before applying the delta",
776         default=0.01,
777     )
778     invert: BoolProperty(
779         description="Invert the mouse input",
780         default=False,
781     )
782     initial_x: IntProperty(options={'HIDDEN'})
783
784     def _values_store(self, context):
785         data_path_iter = self.data_path_iter
786         data_path_item = self.data_path_item
787
788         self._values = values = {}
789
790         for item in getattr(context, data_path_iter):
791             try:
792                 value_orig = eval("item." + data_path_item)
793             except:
794                 continue
795
796             # check this can be set, maybe this is library data.
797             try:
798                 exec("item.%s = %s" % (data_path_item, value_orig))
799             except:
800                 continue
801
802             values[item] = value_orig
803
804     def _values_delta(self, delta):
805         delta *= self.input_scale
806         if self.invert:
807             delta = - delta
808
809         data_path_item = self.data_path_item
810         for item, value_orig in self._values.items():
811             if type(value_orig) == int:
812                 exec("item.%s = int(%d)" % (data_path_item, round(value_orig + delta)))
813             else:
814                 exec("item.%s = %f" % (data_path_item, value_orig + delta))
815
816     def _values_restore(self):
817         data_path_item = self.data_path_item
818         for item, value_orig in self._values.items():
819             exec("item.%s = %s" % (data_path_item, value_orig))
820
821         self._values.clear()
822
823     def _values_clear(self):
824         self._values.clear()
825
826     def modal(self, context, event):
827         event_type = event.type
828
829         if event_type == 'MOUSEMOVE':
830             delta = event.mouse_x - self.initial_x
831             self._values_delta(delta)
832             header_text = self.header_text
833             if header_text:
834                 if len(self._values) == 1:
835                     (item, ) = self._values.keys()
836                     header_text = header_text % eval("item.%s" % self.data_path_item)
837                 else:
838                     header_text = (self.header_text % delta) + " (delta)"
839                 context.area.header_text_set(header_text)
840
841         elif 'LEFTMOUSE' == event_type:
842             item = next(iter(self._values.keys()))
843             self._values_clear()
844             context.area.header_text_set(None)
845             return operator_value_undo_return(item)
846
847         elif event_type in {'RIGHTMOUSE', 'ESC'}:
848             self._values_restore()
849             context.area.header_text_set(None)
850             return {'CANCELLED'}
851
852         return {'RUNNING_MODAL'}
853
854     def invoke(self, context, event):
855         self._values_store(context)
856
857         if not self._values:
858             self.report({'WARNING'}, "Nothing to operate on: %s[ ].%s" %
859                         (self.data_path_iter, self.data_path_item))
860
861             return {'CANCELLED'}
862         else:
863             self.initial_x = event.mouse_x
864
865             context.window_manager.modal_handler_add(self)
866             return {'RUNNING_MODAL'}
867
868
869 class WM_OT_url_open(Operator):
870     """Open a website in the web-browser"""
871     bl_idname = "wm.url_open"
872     bl_label = ""
873     bl_options = {'INTERNAL'}
874
875     url: StringProperty(
876         name="URL",
877         description="URL to open",
878     )
879
880     def execute(self, context):
881         import webbrowser
882         webbrowser.open(self.url)
883         return {'FINISHED'}
884
885
886 class WM_OT_path_open(Operator):
887     """Open a path in a file browser"""
888     bl_idname = "wm.path_open"
889     bl_label = ""
890     bl_options = {'INTERNAL'}
891
892     filepath: StringProperty(
893         subtype='FILE_PATH',
894         options={'SKIP_SAVE'},
895     )
896
897     def execute(self, context):
898         import sys
899         import os
900         import subprocess
901
902         filepath = self.filepath
903
904         if not filepath:
905             self.report({'ERROR'}, "File path was not set")
906             return {'CANCELLED'}
907
908         filepath = bpy.path.abspath(filepath)
909         filepath = os.path.normpath(filepath)
910
911         if not os.path.exists(filepath):
912             self.report({'ERROR'}, "File '%s' not found" % filepath)
913             return {'CANCELLED'}
914
915         if sys.platform[:3] == "win":
916             os.startfile(filepath)
917         elif sys.platform == "darwin":
918             subprocess.check_call(["open", filepath])
919         else:
920             try:
921                 subprocess.check_call(["xdg-open", filepath])
922             except:
923                 # xdg-open *should* be supported by recent Gnome, KDE, Xfce
924                 import traceback
925                 traceback.print_exc()
926
927         return {'FINISHED'}
928
929
930 def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""):
931
932     def operator_exists_pair(a, b):
933         # Not fast, this is only for docs.
934         return b in dir(getattr(bpy.ops, a))
935
936     def operator_exists_single(a):
937         a, b = a.partition("_OT_")[::2]
938         return operator_exists_pair(a.lower(), b)
939
940     id_split = doc_id.split(".")
941     url = rna = None
942
943     if len(id_split) == 1:  # rna, class
944         if do_url:
945             url = "%s/bpy.types.%s.html" % (url_prefix, id_split[0])
946         else:
947             rna = "bpy.types.%s" % id_split[0]
948
949     elif len(id_split) == 2:  # rna, class.prop
950         class_name, class_prop = id_split
951
952         # an operator (common case - just button referencing an op)
953         if operator_exists_pair(class_name, class_prop):
954             if do_url:
955                 url = (
956                     "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
957                     (url_prefix, class_name, class_name, class_prop)
958                 )
959             else:
960                 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
961         elif operator_exists_single(class_name):
962             # note: ignore the prop name since we don't have a way to link into it
963             class_name, class_prop = class_name.split("_OT_", 1)
964             class_name = class_name.lower()
965             if do_url:
966                 url = (
967                     "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
968                     (url_prefix, class_name, class_name, class_prop)
969                 )
970             else:
971                 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
972         else:
973             # an RNA setting, common case
974             rna_class = getattr(bpy.types, class_name)
975
976             # detect if this is a inherited member and use that name instead
977             rna_parent = rna_class.bl_rna
978             rna_prop = rna_parent.properties.get(class_prop)
979             if rna_prop:
980                 rna_parent = rna_parent.base
981                 while rna_parent and rna_prop == rna_parent.properties.get(class_prop):
982                     class_name = rna_parent.identifier
983                     rna_parent = rna_parent.base
984
985                 if do_url:
986                     url = (
987                         "%s/bpy.types.%s.html#bpy.types.%s.%s" %
988                         (url_prefix, class_name, class_name, class_prop)
989                     )
990                 else:
991                     rna = "bpy.types.%s.%s" % (class_name, class_prop)
992             else:
993                 # We assume this is custom property, only try to generate generic url/rna_id...
994                 if do_url:
995                     url = ("%s/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items" % (url_prefix,))
996                 else:
997                     rna = "bpy.types.bpy_struct"
998
999     return url if do_url else rna
1000
1001
1002 class WM_OT_doc_view_manual(Operator):
1003     """Load online manual"""
1004     bl_idname = "wm.doc_view_manual"
1005     bl_label = "View Manual"
1006
1007     doc_id: doc_id
1008
1009     @staticmethod
1010     def _find_reference(rna_id, url_mapping, verbose=True):
1011         if verbose:
1012             print("online manual check for: '%s'... " % rna_id)
1013         from fnmatch import fnmatchcase
1014         # XXX, for some reason all RNA ID's are stored lowercase
1015         # Adding case into all ID's isn't worth the hassle so force lowercase.
1016         rna_id = rna_id.lower()
1017         for pattern, url_suffix in url_mapping:
1018             if fnmatchcase(rna_id, pattern):
1019                 if verbose:
1020                     print("            match found: '%s' --> '%s'" % (pattern, url_suffix))
1021                 return url_suffix
1022         if verbose:
1023             print("match not found")
1024         return None
1025
1026     @staticmethod
1027     def _lookup_rna_url(rna_id, verbose=True):
1028         for prefix, url_manual_mapping in bpy.utils.manual_map():
1029             rna_ref = WM_OT_doc_view_manual._find_reference(rna_id, url_manual_mapping, verbose=verbose)
1030             if rna_ref is not None:
1031                 url = prefix + rna_ref
1032                 return url
1033
1034     def execute(self, context):
1035         rna_id = _wm_doc_get_id(self.doc_id, do_url=False)
1036         if rna_id is None:
1037             return {'PASS_THROUGH'}
1038
1039         url = self._lookup_rna_url(rna_id)
1040
1041         if url is None:
1042             self.report(
1043                 {'WARNING'},
1044                 "No reference available %r, "
1045                 "Update info in 'rna_manual_reference.py' "
1046                 "or callback to bpy.utils.manual_map()" %
1047                 self.doc_id
1048             )
1049             return {'CANCELLED'}
1050         else:
1051             import webbrowser
1052             webbrowser.open(url)
1053             return {'FINISHED'}
1054
1055
1056 class WM_OT_doc_view(Operator):
1057     """Load online reference docs"""
1058     bl_idname = "wm.doc_view"
1059     bl_label = "View Documentation"
1060
1061     doc_id: doc_id
1062     if bpy.app.version_cycle == "release":
1063         _prefix = ("https://docs.blender.org/api/blender_python_api_current")
1064     else:
1065         _prefix = ("https://docs.blender.org/api/blender_python_api_master")
1066
1067     def execute(self, context):
1068         url = _wm_doc_get_id(self.doc_id, do_url=True, url_prefix=self._prefix)
1069         if url is None:
1070             return {'PASS_THROUGH'}
1071
1072         import webbrowser
1073         webbrowser.open(url)
1074
1075         return {'FINISHED'}
1076
1077
1078 rna_path = StringProperty(
1079     name="Property Edit",
1080     description="Property data_path edit",
1081     maxlen=1024,
1082     options={'HIDDEN'},
1083 )
1084
1085 rna_value = StringProperty(
1086     name="Property Value",
1087     description="Property value edit",
1088     maxlen=1024,
1089 )
1090
1091 rna_property = StringProperty(
1092     name="Property Name",
1093     description="Property name edit",
1094     maxlen=1024,
1095 )
1096
1097 rna_min = FloatProperty(
1098     name="Min",
1099     default=-10000.0,
1100     precision=3,
1101 )
1102
1103 rna_max = FloatProperty(
1104     name="Max",
1105     default=10000.0,
1106     precision=3,
1107 )
1108
1109 rna_use_soft_limits = BoolProperty(
1110     name="Use Soft Limits",
1111 )
1112
1113 rna_is_overridable_static = BoolProperty(
1114     name="Is Statically Overridable",
1115     default=False,
1116 )
1117
1118
1119 class WM_OT_properties_edit(Operator):
1120     bl_idname = "wm.properties_edit"
1121     bl_label = "Edit Property"
1122     # register only because invoke_props_popup requires.
1123     bl_options = {'REGISTER', 'INTERNAL'}
1124
1125     data_path: rna_path
1126     property: rna_property
1127     value: rna_value
1128     min: rna_min
1129     max: rna_max
1130     use_soft_limits: rna_use_soft_limits
1131     is_overridable_static: rna_is_overridable_static
1132     soft_min: rna_min
1133     soft_max: rna_max
1134     description: StringProperty(
1135         name="Tooltip",
1136     )
1137
1138     def _cmp_props_get(self):
1139         # Changing these properties will refresh the UI
1140         return {
1141             "use_soft_limits": self.use_soft_limits,
1142             "soft_range": (self.soft_min, self.soft_max),
1143             "hard_range": (self.min, self.max),
1144         }
1145
1146     def execute(self, context):
1147         from rna_prop_ui import (
1148             rna_idprop_ui_prop_get,
1149             rna_idprop_ui_prop_clear,
1150             rna_idprop_ui_prop_update,
1151         )
1152
1153         data_path = self.data_path
1154         value = self.value
1155         prop = self.property
1156
1157         prop_old = getattr(self, "_last_prop", [None])[0]
1158
1159         if prop_old is None:
1160             self.report({'ERROR'}, "Direct execution not supported")
1161             return {'CANCELLED'}
1162
1163         try:
1164             value_eval = eval(value)
1165             # assert else None -> None, not "None", see [#33431]
1166             assert(type(value_eval) in {str, float, int, bool, tuple, list})
1167         except:
1168             value_eval = value
1169
1170         # First remove
1171         item = eval("context.%s" % data_path)
1172         prop_type_old = type(item[prop_old])
1173
1174         rna_idprop_ui_prop_clear(item, prop_old)
1175         exec_str = "del item[%r]" % prop_old
1176         # print(exec_str)
1177         exec(exec_str)
1178
1179         # Reassign
1180         exec_str = "item[%r] = %s" % (prop, repr(value_eval))
1181         # print(exec_str)
1182         exec(exec_str)
1183
1184         exec_str = "item.property_overridable_static_set('[\"%s\"]', %s)" % (prop, self.is_overridable_static)
1185         exec(exec_str)
1186
1187         rna_idprop_ui_prop_update(item, prop)
1188
1189         self._last_prop[:] = [prop]
1190
1191         prop_type = type(item[prop])
1192
1193         prop_ui = rna_idprop_ui_prop_get(item, prop)
1194
1195         if prop_type in {float, int}:
1196             prop_ui["min"] = prop_type(self.min)
1197             prop_ui["max"] = prop_type(self.max)
1198
1199             if self.use_soft_limits:
1200                 prop_ui["soft_min"] = prop_type(self.soft_min)
1201                 prop_ui["soft_max"] = prop_type(self.soft_max)
1202             else:
1203                 prop_ui["soft_min"] = prop_type(self.min)
1204                 prop_ui["soft_max"] = prop_type(self.max)
1205
1206         prop_ui["description"] = self.description
1207
1208         # If we have changed the type of the property, update its potential anim curves!
1209         if prop_type_old != prop_type:
1210             data_path = '["%s"]' % bpy.utils.escape_identifier(prop)
1211             done = set()
1212
1213             def _update(fcurves):
1214                 for fcu in fcurves:
1215                     if fcu not in done and fcu.data_path == data_path:
1216                         fcu.update_autoflags(item)
1217                         done.add(fcu)
1218
1219             def _update_strips(strips):
1220                 for st in strips:
1221                     if st.type == 'CLIP' and st.action:
1222                         _update(st.action.fcurves)
1223                     elif st.type == 'META':
1224                         _update_strips(st.strips)
1225
1226             adt = getattr(item, "animation_data", None)
1227             if adt is not None:
1228                 if adt.action:
1229                     _update(adt.action.fcurves)
1230                 if adt.drivers:
1231                     _update(adt.drivers)
1232                 if adt.nla_tracks:
1233                     for nt in adt.nla_tracks:
1234                         _update_strips(nt.strips)
1235
1236         # otherwise existing buttons which reference freed
1237         # memory may crash blender [#26510]
1238         # context.area.tag_redraw()
1239         for win in context.window_manager.windows:
1240             for area in win.screen.areas:
1241                 area.tag_redraw()
1242
1243         return {'FINISHED'}
1244
1245     def invoke(self, context, event):
1246         from rna_prop_ui import rna_idprop_ui_prop_get
1247
1248         data_path = self.data_path
1249
1250         if not data_path:
1251             self.report({'ERROR'}, "Data path not set")
1252             return {'CANCELLED'}
1253
1254         self._last_prop = [self.property]
1255
1256         item = eval("context.%s" % data_path)
1257
1258         # setup defaults
1259         prop_ui = rna_idprop_ui_prop_get(item, self.property, False)  # don't create
1260         if prop_ui:
1261             self.min = prop_ui.get("min", -1000000000)
1262             self.max = prop_ui.get("max", 1000000000)
1263             self.description = prop_ui.get("description", "")
1264
1265             self.soft_min = prop_ui.get("soft_min", self.min)
1266             self.soft_max = prop_ui.get("soft_max", self.max)
1267             self.use_soft_limits = (
1268                 self.min != self.soft_min or
1269                 self.max != self.soft_max
1270             )
1271
1272         # store for comparison
1273         self._cmp_props = self._cmp_props_get()
1274
1275         wm = context.window_manager
1276         return wm.invoke_props_dialog(self)
1277
1278     def check(self, context):
1279         cmp_props = self._cmp_props_get()
1280         changed = False
1281         if self._cmp_props != cmp_props:
1282             if cmp_props["use_soft_limits"]:
1283                 if cmp_props["soft_range"] != self._cmp_props["soft_range"]:
1284                     self.min = min(self.min, self.soft_min)
1285                     self.max = max(self.max, self.soft_max)
1286                     changed = True
1287                 if cmp_props["hard_range"] != self._cmp_props["hard_range"]:
1288                     self.soft_min = max(self.min, self.soft_min)
1289                     self.soft_max = min(self.max, self.soft_max)
1290                     changed = True
1291             else:
1292                 if cmp_props["soft_range"] != cmp_props["hard_range"]:
1293                     self.soft_min = self.min
1294                     self.soft_max = self.max
1295                     changed = True
1296
1297             changed |= (cmp_props["use_soft_limits"] != self._cmp_props["use_soft_limits"])
1298
1299             if changed:
1300                 cmp_props = self._cmp_props_get()
1301
1302             self._cmp_props = cmp_props
1303
1304         return changed
1305
1306     def draw(self, context):
1307         layout = self.layout
1308         layout.prop(self, "property")
1309         layout.prop(self, "value")
1310         row = layout.row(align=True)
1311         row.prop(self, "min")
1312         row.prop(self, "max")
1313
1314         row = layout.row()
1315         row.prop(self, "use_soft_limits")
1316         row.prop(self, "is_overridable_static")
1317
1318         row = layout.row(align=True)
1319         row.enabled = self.use_soft_limits
1320         row.prop(self, "soft_min", text="Soft Min")
1321         row.prop(self, "soft_max", text="Soft Max")
1322         layout.prop(self, "description")
1323
1324
1325 class WM_OT_properties_add(Operator):
1326     bl_idname = "wm.properties_add"
1327     bl_label = "Add Property"
1328     bl_options = {'UNDO', 'INTERNAL'}
1329
1330     data_path: rna_path
1331
1332     def execute(self, context):
1333         from rna_prop_ui import (
1334             rna_idprop_ui_prop_get,
1335             rna_idprop_ui_prop_update,
1336         )
1337
1338         data_path = self.data_path
1339         item = eval("context.%s" % data_path)
1340
1341         def unique_name(names):
1342             prop = "prop"
1343             prop_new = prop
1344             i = 1
1345             while prop_new in names:
1346                 prop_new = prop + str(i)
1347                 i += 1
1348
1349             return prop_new
1350
1351         prop = unique_name({
1352             *item.keys(),
1353             *type(item).bl_rna.properties.keys(),
1354         })
1355
1356         item[prop] = 1.0
1357         rna_idprop_ui_prop_update(item, prop)
1358
1359         # not essential, but without this we get [#31661]
1360         prop_ui = rna_idprop_ui_prop_get(item, prop)
1361         prop_ui["soft_min"] = prop_ui["min"] = 0.0
1362         prop_ui["soft_max"] = prop_ui["max"] = 1.0
1363
1364         return {'FINISHED'}
1365
1366
1367 class WM_OT_properties_context_change(Operator):
1368     """Jump to a different tab inside the properties editor"""
1369     bl_idname = "wm.properties_context_change"
1370     bl_label = ""
1371     bl_options = {'INTERNAL'}
1372
1373     context: StringProperty(
1374         name="Context",
1375         maxlen=64,
1376     )
1377
1378     def execute(self, context):
1379         context.space_data.context = self.context
1380         return {'FINISHED'}
1381
1382
1383 class WM_OT_properties_remove(Operator):
1384     """Internal use (edit a property data_path)"""
1385     bl_idname = "wm.properties_remove"
1386     bl_label = "Remove Property"
1387     bl_options = {'UNDO', 'INTERNAL'}
1388
1389     data_path: rna_path
1390     property: rna_property
1391
1392     def execute(self, context):
1393         from rna_prop_ui import (
1394             rna_idprop_ui_prop_clear,
1395             rna_idprop_ui_prop_update,
1396         )
1397         data_path = self.data_path
1398         item = eval("context.%s" % data_path)
1399         prop = self.property
1400         rna_idprop_ui_prop_update(item, prop)
1401         del item[prop]
1402         rna_idprop_ui_prop_clear(item, prop)
1403
1404         return {'FINISHED'}
1405
1406
1407 class WM_OT_keyconfig_activate(Operator):
1408     bl_idname = "wm.keyconfig_activate"
1409     bl_label = "Activate Keyconfig"
1410
1411     filepath: StringProperty(
1412         subtype='FILE_PATH',
1413     )
1414
1415     def execute(self, context):
1416         if bpy.utils.keyconfig_set(self.filepath, report=self.report):
1417             return {'FINISHED'}
1418         else:
1419             return {'CANCELLED'}
1420
1421
1422 class WM_OT_sysinfo(Operator):
1423     """Generate system information, saved into a text file"""
1424
1425     bl_idname = "wm.sysinfo"
1426     bl_label = "Save System Info"
1427
1428     filepath: StringProperty(
1429         subtype='FILE_PATH',
1430         options={'SKIP_SAVE'},
1431     )
1432
1433     def execute(self, context):
1434         import sys_info
1435         sys_info.write_sysinfo(self.filepath)
1436         return {'FINISHED'}
1437
1438     def invoke(self, context, event):
1439         import os
1440
1441         if not self.filepath:
1442             self.filepath = os.path.join(
1443                 os.path.expanduser("~"), "system-info.txt")
1444
1445         wm = context.window_manager
1446         wm.fileselect_add(self)
1447         return {'RUNNING_MODAL'}
1448
1449
1450 class WM_OT_copy_prev_settings(Operator):
1451     """Copy settings from previous version"""
1452     bl_idname = "wm.copy_prev_settings"
1453     bl_label = "Copy Previous Settings"
1454
1455     @staticmethod
1456     def previous_version():
1457         ver = bpy.app.version
1458         ver_old = ((ver[0] * 100) + ver[1]) - 1
1459         return ver_old // 100, ver_old % 100
1460
1461     @staticmethod
1462     def _old_path():
1463         ver = bpy.app.version
1464         ver_old = ((ver[0] * 100) + ver[1]) - 1
1465         return bpy.utils.resource_path('USER', ver_old // 100, ver_old % 100)
1466
1467     @staticmethod
1468     def _new_path():
1469         return bpy.utils.resource_path('USER')
1470
1471     @classmethod
1472     def poll(cls, context):
1473         import os
1474
1475         old = cls._old_path()
1476         new = cls._new_path()
1477         if os.path.isdir(old) and not os.path.isdir(new):
1478             return True
1479
1480         old_userpref = os.path.join(old, "config", "userpref.blend")
1481         new_userpref = os.path.join(new, "config", "userpref.blend")
1482         return os.path.isfile(old_userpref) and not os.path.isfile(new_userpref)
1483
1484     def execute(self, context):
1485         import shutil
1486
1487         shutil.copytree(self._old_path(), self._new_path(), symlinks=True)
1488
1489         # reload recent-files.txt
1490         bpy.ops.wm.read_history()
1491
1492         # don't loose users work if they open the splash later.
1493         if bpy.data.is_saved is bpy.data.is_dirty is False:
1494             bpy.ops.wm.read_homefile()
1495         else:
1496             self.report({'INFO'}, "Reload Start-Up file to restore settings")
1497
1498         return {'FINISHED'}
1499
1500
1501 class WM_OT_keyconfig_test(Operator):
1502     """Test key-config for conflicts"""
1503     bl_idname = "wm.keyconfig_test"
1504     bl_label = "Test Key Configuration for Conflicts"
1505
1506     def execute(self, context):
1507         from bpy_extras import keyconfig_utils
1508
1509         wm = context.window_manager
1510         kc = wm.keyconfigs.default
1511
1512         if keyconfig_utils.keyconfig_test(kc):
1513             print("CONFLICT")
1514
1515         return {'FINISHED'}
1516
1517
1518 class WM_OT_keyconfig_import(Operator):
1519     """Import key configuration from a python script"""
1520     bl_idname = "wm.keyconfig_import"
1521     bl_label = "Import Key Configuration..."
1522
1523     filepath: StringProperty(
1524         subtype='FILE_PATH',
1525         default="keymap.py",
1526     )
1527     filter_folder: BoolProperty(
1528         name="Filter folders",
1529         default=True,
1530         options={'HIDDEN'},
1531     )
1532     filter_text: BoolProperty(
1533         name="Filter text",
1534         default=True,
1535         options={'HIDDEN'},
1536     )
1537     filter_python: BoolProperty(
1538         name="Filter python",
1539         default=True,
1540         options={'HIDDEN'},
1541     )
1542     keep_original: BoolProperty(
1543         name="Keep original",
1544         description="Keep original file after copying to configuration folder",
1545         default=True,
1546     )
1547
1548     def execute(self, context):
1549         import os
1550         from os.path import basename
1551         import shutil
1552
1553         if not self.filepath:
1554             self.report({'ERROR'}, "Filepath not set")
1555             return {'CANCELLED'}
1556
1557         config_name = basename(self.filepath)
1558
1559         path = bpy.utils.user_resource('SCRIPTS', os.path.join("presets", "keyconfig"), create=True)
1560         path = os.path.join(path, config_name)
1561
1562         try:
1563             if self.keep_original:
1564                 shutil.copy(self.filepath, path)
1565             else:
1566                 shutil.move(self.filepath, path)
1567         except Exception as ex:
1568             self.report({'ERROR'}, "Installing keymap failed: %s" % ex)
1569             return {'CANCELLED'}
1570
1571         # sneaky way to check we're actually running the code.
1572         if bpy.utils.keyconfig_set(path, report=self.report):
1573             return {'FINISHED'}
1574         else:
1575             return {'CANCELLED'}
1576
1577     def invoke(self, context, event):
1578         wm = context.window_manager
1579         wm.fileselect_add(self)
1580         return {'RUNNING_MODAL'}
1581
1582 # This operator is also used by interaction presets saving - AddPresetBase
1583
1584
1585 class WM_OT_keyconfig_export(Operator):
1586     """Export key configuration to a python script"""
1587     bl_idname = "wm.keyconfig_export"
1588     bl_label = "Export Key Configuration..."
1589
1590     all: BoolProperty(
1591         name="All Keymaps",
1592         default=False,
1593         description="Write all keymaps (not just user modified)",
1594     )
1595     filepath: StringProperty(
1596         subtype='FILE_PATH',
1597         default="keymap.py",
1598     )
1599     filter_folder: BoolProperty(
1600         name="Filter folders",
1601         default=True,
1602         options={'HIDDEN'},
1603     )
1604     filter_text: BoolProperty(
1605         name="Filter text",
1606         default=True,
1607         options={'HIDDEN'},
1608     )
1609     filter_python: BoolProperty(
1610         name="Filter python",
1611         default=True,
1612         options={'HIDDEN'},
1613     )
1614
1615     def execute(self, context):
1616         from bl_keymap_utils.io import keyconfig_export_as_data
1617
1618         if not self.filepath:
1619             raise Exception("Filepath not set")
1620
1621         if not self.filepath.endswith(".py"):
1622             self.filepath += ".py"
1623
1624         wm = context.window_manager
1625
1626         keyconfig_export_as_data(
1627             wm,
1628             wm.keyconfigs.active,
1629             self.filepath,
1630             all_keymaps=self.all,
1631         )
1632
1633         return {'FINISHED'}
1634
1635     def invoke(self, context, event):
1636         wm = context.window_manager
1637         wm.fileselect_add(self)
1638         return {'RUNNING_MODAL'}
1639
1640
1641 class WM_OT_keymap_restore(Operator):
1642     """Restore key map(s)"""
1643     bl_idname = "wm.keymap_restore"
1644     bl_label = "Restore Key Map(s)"
1645
1646     all: BoolProperty(
1647         name="All Keymaps",
1648         description="Restore all keymaps to default",
1649     )
1650
1651     def execute(self, context):
1652         wm = context.window_manager
1653
1654         if self.all:
1655             for km in wm.keyconfigs.user.keymaps:
1656                 km.restore_to_default()
1657         else:
1658             km = context.keymap
1659             km.restore_to_default()
1660
1661         return {'FINISHED'}
1662
1663
1664 class WM_OT_keyitem_restore(Operator):
1665     """Restore key map item"""
1666     bl_idname = "wm.keyitem_restore"
1667     bl_label = "Restore Key Map Item"
1668
1669     item_id: IntProperty(
1670         name="Item Identifier",
1671         description="Identifier of the item to remove",
1672     )
1673
1674     @classmethod
1675     def poll(cls, context):
1676         keymap = getattr(context, "keymap", None)
1677         return keymap
1678
1679     def execute(self, context):
1680         km = context.keymap
1681         kmi = km.keymap_items.from_id(self.item_id)
1682
1683         if (not kmi.is_user_defined) and kmi.is_user_modified:
1684             km.restore_item_to_default(kmi)
1685
1686         return {'FINISHED'}
1687
1688
1689 class WM_OT_keyitem_add(Operator):
1690     """Add key map item"""
1691     bl_idname = "wm.keyitem_add"
1692     bl_label = "Add Key Map Item"
1693
1694     def execute(self, context):
1695         km = context.keymap
1696
1697         if km.is_modal:
1698             km.keymap_items.new_modal("", 'A', 'PRESS')
1699         else:
1700             km.keymap_items.new("none", 'A', 'PRESS')
1701
1702         # clear filter and expand keymap so we can see the newly added item
1703         if context.space_data.filter_text != "":
1704             context.space_data.filter_text = ""
1705             km.show_expanded_items = True
1706             km.show_expanded_children = True
1707
1708         return {'FINISHED'}
1709
1710
1711 class WM_OT_keyitem_remove(Operator):
1712     """Remove key map item"""
1713     bl_idname = "wm.keyitem_remove"
1714     bl_label = "Remove Key Map Item"
1715
1716     item_id: IntProperty(
1717         name="Item Identifier",
1718         description="Identifier of the item to remove",
1719     )
1720
1721     @classmethod
1722     def poll(cls, context):
1723         return hasattr(context, "keymap")
1724
1725     def execute(self, context):
1726         km = context.keymap
1727         kmi = km.keymap_items.from_id(self.item_id)
1728         km.keymap_items.remove(kmi)
1729         return {'FINISHED'}
1730
1731
1732 class WM_OT_keyconfig_remove(Operator):
1733     """Remove key config"""
1734     bl_idname = "wm.keyconfig_remove"
1735     bl_label = "Remove Key Config"
1736
1737     @classmethod
1738     def poll(cls, context):
1739         wm = context.window_manager
1740         keyconf = wm.keyconfigs.active
1741         return keyconf and keyconf.is_user_defined
1742
1743     def execute(self, context):
1744         wm = context.window_manager
1745         keyconfig = wm.keyconfigs.active
1746         wm.keyconfigs.remove(keyconfig)
1747         return {'FINISHED'}
1748
1749
1750 class WM_OT_operator_cheat_sheet(Operator):
1751     """List all the Operators in a text-block, useful for scripting"""
1752     bl_idname = "wm.operator_cheat_sheet"
1753     bl_label = "Operator Cheat Sheet"
1754
1755     def execute(self, context):
1756         op_strings = []
1757         tot = 0
1758         for op_module_name in dir(bpy.ops):
1759             op_module = getattr(bpy.ops, op_module_name)
1760             for op_submodule_name in dir(op_module):
1761                 op = getattr(op_module, op_submodule_name)
1762                 text = repr(op)
1763                 if text.split("\n")[-1].startswith("bpy.ops."):
1764                     op_strings.append(text)
1765                     tot += 1
1766
1767             op_strings.append('')
1768
1769         textblock = bpy.data.texts.new("OperatorList.txt")
1770         textblock.write('# %d Operators\n\n' % tot)
1771         textblock.write('\n'.join(op_strings))
1772         self.report({'INFO'}, "See OperatorList.txt textblock")
1773         return {'FINISHED'}
1774
1775
1776 # -----------------------------------------------------------------------------
1777 # Add-on Operators
1778
1779 class WM_OT_addon_enable(Operator):
1780     """Enable an add-on"""
1781     bl_idname = "wm.addon_enable"
1782     bl_label = "Enable Add-on"
1783
1784     module: StringProperty(
1785         name="Module",
1786         description="Module name of the add-on to enable",
1787     )
1788
1789     def execute(self, context):
1790         import addon_utils
1791
1792         err_str = ""
1793
1794         def err_cb(ex):
1795             import traceback
1796             nonlocal err_str
1797             err_str = traceback.format_exc()
1798             print(err_str)
1799
1800         mod = addon_utils.enable(self.module, default_set=True, handle_error=err_cb)
1801
1802         if mod:
1803             info = addon_utils.module_bl_info(mod)
1804
1805             info_ver = info.get("blender", (0, 0, 0))
1806
1807             if info_ver > bpy.app.version:
1808                 self.report(
1809                     {'WARNING'},
1810                     "This script was written Blender "
1811                     "version %d.%d.%d and might not "
1812                     "function (correctly), "
1813                     "though it is enabled" %
1814                     info_ver
1815                 )
1816             return {'FINISHED'}
1817         else:
1818
1819             if err_str:
1820                 self.report({'ERROR'}, err_str)
1821
1822             return {'CANCELLED'}
1823
1824
1825 class WM_OT_addon_disable(Operator):
1826     """Disable an add-on"""
1827     bl_idname = "wm.addon_disable"
1828     bl_label = "Disable Add-on"
1829
1830     module: StringProperty(
1831         name="Module",
1832         description="Module name of the add-on to disable",
1833     )
1834
1835     def execute(self, context):
1836         import addon_utils
1837
1838         err_str = ""
1839
1840         def err_cb(ex):
1841             import traceback
1842             nonlocal err_str
1843             err_str = traceback.format_exc()
1844             print(err_str)
1845
1846         addon_utils.disable(self.module, default_set=True, handle_error=err_cb)
1847
1848         if err_str:
1849             self.report({'ERROR'}, err_str)
1850
1851         return {'FINISHED'}
1852
1853
1854 class WM_OT_owner_enable(Operator):
1855     """Enable workspace owner ID"""
1856     bl_idname = "wm.owner_enable"
1857     bl_label = "Enable Add-on"
1858
1859     owner_id: StringProperty(
1860         name="UI Tag",
1861     )
1862
1863     def execute(self, context):
1864         workspace = context.workspace
1865         workspace.owner_ids.new(self.owner_id)
1866         return {'FINISHED'}
1867
1868
1869 class WM_OT_owner_disable(Operator):
1870     """Enable workspace owner ID"""
1871     bl_idname = "wm.owner_disable"
1872     bl_label = "Disable UI Tag"
1873
1874     owner_id: StringProperty(
1875         name="UI Tag",
1876     )
1877
1878     def execute(self, context):
1879         workspace = context.workspace
1880         owner_id = workspace.owner_ids[self.owner_id]
1881         workspace.owner_ids.remove(owner_id)
1882         return {'FINISHED'}
1883
1884
1885 class WM_OT_theme_install(Operator):
1886     """Load and apply a Blender XML theme file"""
1887     bl_idname = "wm.theme_install"
1888     bl_label = "Install Theme..."
1889
1890     overwrite: BoolProperty(
1891         name="Overwrite",
1892         description="Remove existing theme file if exists",
1893         default=True,
1894     )
1895     filepath: StringProperty(
1896         subtype='FILE_PATH',
1897     )
1898     filter_folder: BoolProperty(
1899         name="Filter folders",
1900         default=True,
1901         options={'HIDDEN'},
1902     )
1903     filter_glob: StringProperty(
1904         default="*.xml",
1905         options={'HIDDEN'},
1906     )
1907
1908     def execute(self, context):
1909         import os
1910         import shutil
1911         import traceback
1912
1913         xmlfile = self.filepath
1914
1915         path_themes = bpy.utils.user_resource('SCRIPTS', "presets/interface_theme", create=True)
1916
1917         if not path_themes:
1918             self.report({'ERROR'}, "Failed to get themes path")
1919             return {'CANCELLED'}
1920
1921         path_dest = os.path.join(path_themes, os.path.basename(xmlfile))
1922
1923         if not self.overwrite:
1924             if os.path.exists(path_dest):
1925                 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
1926                 return {'CANCELLED'}
1927
1928         try:
1929             shutil.copyfile(xmlfile, path_dest)
1930             bpy.ops.script.execute_preset(
1931                 filepath=path_dest,
1932                 menu_idname="USERPREF_MT_interface_theme_presets",
1933             )
1934
1935         except:
1936             traceback.print_exc()
1937             return {'CANCELLED'}
1938
1939         return {'FINISHED'}
1940
1941     def invoke(self, context, event):
1942         wm = context.window_manager
1943         wm.fileselect_add(self)
1944         return {'RUNNING_MODAL'}
1945
1946
1947 class WM_OT_addon_refresh(Operator):
1948     """Scan add-on directories for new modules"""
1949     bl_idname = "wm.addon_refresh"
1950     bl_label = "Refresh"
1951
1952     def execute(self, context):
1953         import addon_utils
1954
1955         addon_utils.modules_refresh()
1956
1957         return {'FINISHED'}
1958
1959
1960 # Note: shares some logic with WM_OT_app_template_install
1961 # but not enough to de-duplicate. Fixed here may apply to both.
1962 class WM_OT_addon_install(Operator):
1963     """Install an add-on"""
1964     bl_idname = "wm.addon_install"
1965     bl_label = "Install Add-on from File..."
1966
1967     overwrite: BoolProperty(
1968         name="Overwrite",
1969         description="Remove existing add-ons with the same ID",
1970         default=True,
1971     )
1972     target: EnumProperty(
1973         name="Target Path",
1974         items=(('DEFAULT', "Default", ""),
1975                ('PREFS', "User Prefs", "")),
1976     )
1977
1978     filepath: StringProperty(
1979         subtype='FILE_PATH',
1980     )
1981     filter_folder: BoolProperty(
1982         name="Filter folders",
1983         default=True,
1984         options={'HIDDEN'},
1985     )
1986     filter_python: BoolProperty(
1987         name="Filter python",
1988         default=True,
1989         options={'HIDDEN'},
1990     )
1991     filter_glob: StringProperty(
1992         default="*.py;*.zip",
1993         options={'HIDDEN'},
1994     )
1995
1996     def execute(self, context):
1997         import addon_utils
1998         import traceback
1999         import zipfile
2000         import shutil
2001         import os
2002
2003         pyfile = self.filepath
2004
2005         if self.target == 'DEFAULT':
2006             # don't use bpy.utils.script_paths("addons") because we may not be able to write to it.
2007             path_addons = bpy.utils.user_resource('SCRIPTS', "addons", create=True)
2008         else:
2009             path_addons = context.user_preferences.filepaths.script_directory
2010             if path_addons:
2011                 path_addons = os.path.join(path_addons, "addons")
2012
2013         if not path_addons:
2014             self.report({'ERROR'}, "Failed to get add-ons path")
2015             return {'CANCELLED'}
2016
2017         if not os.path.isdir(path_addons):
2018             try:
2019                 os.makedirs(path_addons, exist_ok=True)
2020             except:
2021                 traceback.print_exc()
2022
2023         # Check if we are installing from a target path,
2024         # doing so causes 2+ addons of same name or when the same from/to
2025         # location is used, removal of the file!
2026         addon_path = ""
2027         pyfile_dir = os.path.dirname(pyfile)
2028         for addon_path in addon_utils.paths():
2029             if os.path.samefile(pyfile_dir, addon_path):
2030                 self.report({'ERROR'}, "Source file is in the add-on search path: %r" % addon_path)
2031                 return {'CANCELLED'}
2032         del addon_path
2033         del pyfile_dir
2034         # done checking for exceptional case
2035
2036         addons_old = {mod.__name__ for mod in addon_utils.modules()}
2037
2038         # check to see if the file is in compressed format (.zip)
2039         if zipfile.is_zipfile(pyfile):
2040             try:
2041                 file_to_extract = zipfile.ZipFile(pyfile, 'r')
2042             except:
2043                 traceback.print_exc()
2044                 return {'CANCELLED'}
2045
2046             if self.overwrite:
2047                 for f in file_to_extract.namelist():
2048                     module_filesystem_remove(path_addons, f)
2049             else:
2050                 for f in file_to_extract.namelist():
2051                     path_dest = os.path.join(path_addons, os.path.basename(f))
2052                     if os.path.exists(path_dest):
2053                         self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
2054                         return {'CANCELLED'}
2055
2056             try:  # extract the file to "addons"
2057                 file_to_extract.extractall(path_addons)
2058             except:
2059                 traceback.print_exc()
2060                 return {'CANCELLED'}
2061
2062         else:
2063             path_dest = os.path.join(path_addons, os.path.basename(pyfile))
2064
2065             if self.overwrite:
2066                 module_filesystem_remove(path_addons, os.path.basename(pyfile))
2067             elif os.path.exists(path_dest):
2068                 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
2069                 return {'CANCELLED'}
2070
2071             # if not compressed file just copy into the addon path
2072             try:
2073                 shutil.copyfile(pyfile, path_dest)
2074             except:
2075                 traceback.print_exc()
2076                 return {'CANCELLED'}
2077
2078         addons_new = {mod.__name__ for mod in addon_utils.modules()} - addons_old
2079         addons_new.discard("modules")
2080
2081         # disable any addons we may have enabled previously and removed.
2082         # this is unlikely but do just in case. bug [#23978]
2083         for new_addon in addons_new:
2084             addon_utils.disable(new_addon, default_set=True)
2085
2086         # possible the zip contains multiple addons, we could disallow this
2087         # but for now just use the first
2088         for mod in addon_utils.modules(refresh=False):
2089             if mod.__name__ in addons_new:
2090                 info = addon_utils.module_bl_info(mod)
2091
2092                 # show the newly installed addon.
2093                 context.window_manager.addon_filter = 'All'
2094                 context.window_manager.addon_search = info["name"]
2095                 break
2096
2097         # in case a new module path was created to install this addon.
2098         bpy.utils.refresh_script_paths()
2099
2100         # print message
2101         msg = (
2102             tip_("Modules Installed (%s) from %r into %r") %
2103             (", ".join(sorted(addons_new)), pyfile, path_addons)
2104         )
2105         print(msg)
2106         self.report({'INFO'}, msg)
2107
2108         return {'FINISHED'}
2109
2110     def invoke(self, context, event):
2111         wm = context.window_manager
2112         wm.fileselect_add(self)
2113         return {'RUNNING_MODAL'}
2114
2115
2116 class WM_OT_addon_remove(Operator):
2117     """Delete the add-on from the file system"""
2118     bl_idname = "wm.addon_remove"
2119     bl_label = "Remove Add-on"
2120
2121     module: StringProperty(
2122         name="Module",
2123         description="Module name of the add-on to remove",
2124     )
2125
2126     @staticmethod
2127     def path_from_addon(module):
2128         import os
2129         import addon_utils
2130
2131         for mod in addon_utils.modules():
2132             if mod.__name__ == module:
2133                 filepath = mod.__file__
2134                 if os.path.exists(filepath):
2135                     if os.path.splitext(os.path.basename(filepath))[0] == "__init__":
2136                         return os.path.dirname(filepath), True
2137                     else:
2138                         return filepath, False
2139         return None, False
2140
2141     def execute(self, context):
2142         import addon_utils
2143         import os
2144
2145         path, isdir = WM_OT_addon_remove.path_from_addon(self.module)
2146         if path is None:
2147             self.report({'WARNING'}, "Add-on path %r could not be found" % path)
2148             return {'CANCELLED'}
2149
2150         # in case its enabled
2151         addon_utils.disable(self.module, default_set=True)
2152
2153         import shutil
2154         if isdir:
2155             shutil.rmtree(path)
2156         else:
2157             os.remove(path)
2158
2159         addon_utils.modules_refresh()
2160
2161         context.area.tag_redraw()
2162         return {'FINISHED'}
2163
2164     # lame confirmation check
2165     def draw(self, context):
2166         self.layout.label(text="Remove Add-on: %r?" % self.module)
2167         path, _isdir = WM_OT_addon_remove.path_from_addon(self.module)
2168         self.layout.label(text="Path: %r" % path)
2169
2170     def invoke(self, context, event):
2171         wm = context.window_manager
2172         return wm.invoke_props_dialog(self, width=600)
2173
2174
2175 class WM_OT_addon_expand(Operator):
2176     """Display information and preferences for this add-on"""
2177     bl_idname = "wm.addon_expand"
2178     bl_label = ""
2179     bl_options = {'INTERNAL'}
2180
2181     module: StringProperty(
2182         name="Module",
2183         description="Module name of the add-on to expand",
2184     )
2185
2186     def execute(self, context):
2187         import addon_utils
2188
2189         module_name = self.module
2190
2191         mod = addon_utils.addons_fake_modules.get(module_name)
2192         if mod is not None:
2193             info = addon_utils.module_bl_info(mod)
2194             info["show_expanded"] = not info["show_expanded"]
2195
2196         return {'FINISHED'}
2197
2198
2199 class WM_OT_addon_userpref_show(Operator):
2200     """Show add-on user preferences"""
2201     bl_idname = "wm.addon_userpref_show"
2202     bl_label = ""
2203     bl_options = {'INTERNAL'}
2204
2205     module: StringProperty(
2206         name="Module",
2207         description="Module name of the add-on to expand",
2208     )
2209
2210     def execute(self, context):
2211         import addon_utils
2212
2213         module_name = self.module
2214
2215         _modules = addon_utils.modules(refresh=False)
2216         mod = addon_utils.addons_fake_modules.get(module_name)
2217         if mod is not None:
2218             info = addon_utils.module_bl_info(mod)
2219             info["show_expanded"] = True
2220
2221             context.user_preferences.active_section = 'ADDONS'
2222             context.window_manager.addon_filter = 'All'
2223             context.window_manager.addon_search = info["name"]
2224             bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
2225
2226         return {'FINISHED'}
2227
2228
2229 # Note: shares some logic with WM_OT_addon_install
2230 # but not enough to de-duplicate. Fixes here may apply to both.
2231 class WM_OT_app_template_install(Operator):
2232     """Install an application-template"""
2233     bl_idname = "wm.app_template_install"
2234     bl_label = "Install Template from File..."
2235
2236     overwrite: BoolProperty(
2237         name="Overwrite",
2238         description="Remove existing template with the same ID",
2239         default=True,
2240     )
2241
2242     filepath: StringProperty(
2243         subtype='FILE_PATH',
2244     )
2245     filter_folder: BoolProperty(
2246         name="Filter folders",
2247         default=True,
2248         options={'HIDDEN'},
2249     )
2250     filter_glob: StringProperty(
2251         default="*.zip",
2252         options={'HIDDEN'},
2253     )
2254
2255     def execute(self, context):
2256         import traceback
2257         import zipfile
2258         import os
2259
2260         filepath = self.filepath
2261
2262         path_app_templates = bpy.utils.user_resource(
2263             'SCRIPTS', os.path.join("startup", "bl_app_templates_user"),
2264             create=True,
2265         )
2266
2267         if not path_app_templates:
2268             self.report({'ERROR'}, "Failed to get add-ons path")
2269             return {'CANCELLED'}
2270
2271         if not os.path.isdir(path_app_templates):
2272             try:
2273                 os.makedirs(path_app_templates, exist_ok=True)
2274             except:
2275                 traceback.print_exc()
2276
2277         app_templates_old = set(os.listdir(path_app_templates))
2278
2279         # check to see if the file is in compressed format (.zip)
2280         if zipfile.is_zipfile(filepath):
2281             try:
2282                 file_to_extract = zipfile.ZipFile(filepath, 'r')
2283             except:
2284                 traceback.print_exc()
2285                 return {'CANCELLED'}
2286
2287             if self.overwrite:
2288                 for f in file_to_extract.namelist():
2289                     module_filesystem_remove(path_app_templates, f)
2290             else:
2291                 for f in file_to_extract.namelist():
2292                     path_dest = os.path.join(path_app_templates, os.path.basename(f))
2293                     if os.path.exists(path_dest):
2294                         self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
2295                         return {'CANCELLED'}
2296
2297             try:  # extract the file to "bl_app_templates_user"
2298                 file_to_extract.extractall(path_app_templates)
2299             except:
2300                 traceback.print_exc()
2301                 return {'CANCELLED'}
2302
2303         else:
2304             # Only support installing zipfiles
2305             self.report({'WARNING'}, "Expected a zip-file %r\n" % filepath)
2306             return {'CANCELLED'}
2307
2308         app_templates_new = set(os.listdir(path_app_templates)) - app_templates_old
2309
2310         # in case a new module path was created to install this addon.
2311         bpy.utils.refresh_script_paths()
2312
2313         # print message
2314         msg = (
2315             tip_("Template Installed (%s) from %r into %r") %
2316             (", ".join(sorted(app_templates_new)), filepath, path_app_templates)
2317         )
2318         print(msg)
2319         self.report({'INFO'}, msg)
2320
2321         return {'FINISHED'}
2322
2323     def invoke(self, context, event):
2324         wm = context.window_manager
2325         wm.fileselect_add(self)
2326         return {'RUNNING_MODAL'}
2327
2328
2329 class WM_OT_tool_set_by_name(Operator):
2330     """Set the tool by name (for keymaps)"""
2331     bl_idname = "wm.tool_set_by_name"
2332     bl_label = "Set Tool By Name"
2333
2334     name: StringProperty(
2335         name="Text",
2336         description="Display name of the tool",
2337     )
2338     cycle: BoolProperty(
2339         name="Cycle",
2340         description="Cycle through tools in this group",
2341         default=False,
2342         options={'SKIP_SAVE'},
2343     )
2344
2345     space_type: rna_space_type_prop
2346
2347     if use_toolbar_release_hack:
2348         def invoke(self, context, event):
2349             # Hack :S
2350             if not self.properties.is_property_set("name"):
2351                 WM_OT_toolbar._key_held = False
2352                 return {'PASS_THROUGH'}
2353             elif (WM_OT_toolbar._key_held == event.type) and (event.value != 'RELEASE'):
2354                 return {'PASS_THROUGH'}
2355             WM_OT_toolbar._key_held = None
2356
2357             return self.execute(context)
2358
2359     def execute(self, context):
2360         from bl_ui.space_toolsystem_common import (
2361             activate_by_name,
2362             activate_by_name_or_cycle,
2363         )
2364
2365         if self.properties.is_property_set("space_type"):
2366             space_type = self.space_type
2367         else:
2368             space_type = context.space_data.type
2369
2370         fn = activate_by_name_or_cycle if self.cycle else activate_by_name
2371         if fn(context, space_type, self.name):
2372             return {'FINISHED'}
2373         else:
2374             self.report({'WARNING'}, f"Tool {self.name!r:s} not found for space {space_type!r:s}.")
2375             return {'CANCELLED'}
2376
2377
2378 class WM_OT_toolbar(Operator):
2379     bl_idname = "wm.toolbar"
2380     bl_label = "Toolbar"
2381
2382     @classmethod
2383     def poll(cls, context):
2384         return context.space_data is not None
2385
2386     if use_toolbar_release_hack:
2387         _key_held = None
2388         def invoke(self, context, event):
2389             WM_OT_toolbar._key_held = event.type
2390             return self.execute(context)
2391
2392     def execute(self, context):
2393         from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
2394         from bl_keymap_utils import keymap_from_toolbar
2395
2396         space_type = context.space_data.type
2397         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
2398         if cls is None:
2399             return {'CANCELLED'}
2400
2401         wm = context.window_manager
2402         keymap = keymap_from_toolbar.generate(context, space_type)
2403
2404         def draw_menu(popover, context):
2405             layout = popover.layout
2406             layout.operator_context = 'INVOKE_REGION_WIN'
2407             cls.draw_cls(layout, context, detect_layout=False, scale_y=1.0)
2408
2409         wm.popover(draw_menu, ui_units_x=8, keymap=keymap)
2410         return {'FINISHED'}
2411
2412
2413 # Studio Light operations
2414 class WM_OT_studiolight_install(Operator):
2415     """Install a user defined studio light"""
2416     bl_idname = "wm.studiolight_install"
2417     bl_label = "Install Custom Studio Light"
2418
2419     files: CollectionProperty(
2420         name="File Path",
2421         type=OperatorFileListElement,
2422     )
2423     directory: StringProperty(
2424         subtype='DIR_PATH',
2425     )
2426     filter_folder: BoolProperty(
2427         name="Filter folders",
2428         default=True,
2429         options={'HIDDEN'},
2430     )
2431     filter_glob: StringProperty(
2432         default="*.png;*.jpg;*.hdr;*.exr",
2433         options={'HIDDEN'},
2434     )
2435     type: EnumProperty(
2436         items=(
2437             ('MATCAP', "MatCap", ""),
2438             ('WORLD', "World", ""),
2439             ('STUDIO', "Studio", ""),
2440         )
2441     )
2442
2443     def execute(self, context):
2444         import traceback
2445         import shutil
2446         import pathlib
2447         userpref = context.user_preferences
2448
2449         filepaths = [pathlib.Path(self.directory, e.name) for e in self.files]
2450         path_studiolights = bpy.utils.user_resource('DATAFILES')
2451
2452         if not path_studiolights:
2453             self.report({'ERROR'}, "Failed to get Studio Light path")
2454             return {'CANCELLED'}
2455
2456         path_studiolights = pathlib.Path(path_studiolights, "studiolights", self.type.lower())
2457         if not path_studiolights.exists():
2458             try:
2459                 path_studiolights.mkdir(parents=True, exist_ok=True)
2460             except:
2461                 traceback.print_exc()
2462
2463         for filepath in filepaths:
2464             shutil.copy(str(filepath), str(path_studiolights))
2465             userpref.studio_lights.load(str(path_studiolights.joinpath(filepath.name)), self.type)
2466
2467         # print message
2468         msg = (
2469             tip_("StudioLight Installed %r into %r") %
2470             (", ".join(str(x.name) for x in self.files), str(path_studiolights))
2471         )
2472         print(msg)
2473         self.report({'INFO'}, msg)
2474         return {'FINISHED'}
2475
2476     def invoke(self, context, event):
2477         wm = context.window_manager
2478         wm.fileselect_add(self)
2479         return {'RUNNING_MODAL'}
2480
2481
2482 class WM_OT_studiolight_new(Operator):
2483     """Create custom studio light from the studio light editor settings"""
2484     bl_idname = 'wm.studiolight_new'
2485     bl_label = "Create custom Studio light"
2486
2487     filename: StringProperty(
2488         name="Name",
2489         default="StudioLight",
2490     )
2491
2492     def execute(self, context):
2493         import pathlib
2494         userpref = context.user_preferences
2495
2496         path_studiolights = bpy.utils.user_resource('DATAFILES')
2497
2498         if not path_studiolights:
2499             self.report({'ERROR'}, "Failed to get Studio Light path")
2500             return {'CANCELLED'}
2501
2502         path_studiolights = pathlib.Path(path_studiolights, "studiolights", "studio")
2503         if not path_studiolights.exists():
2504             try:
2505                 path_studiolights.mkdir(parents=True, exist_ok=True)
2506             except:
2507                 traceback.print_exc()
2508
2509         finalpath = str(path_studiolights.joinpath(self.filename));
2510         if pathlib.Path(finalpath + ".sl").is_file():
2511             self.report({'ERROR'}, "File already exists")
2512             return {'CANCELLED'}
2513
2514         userpref.studio_lights.new(path=finalpath)
2515
2516         # print message
2517         msg = (
2518             tip_("StudioLight Installed %r into %r") %
2519             (self.filename, str(path_studiolights))
2520         )
2521         print(msg)
2522         self.report({'INFO'}, msg)
2523         return {'FINISHED'}
2524
2525     def draw(self, context):
2526         layout = self.layout
2527         layout.prop(self, "filename")
2528
2529     def invoke(self, context, event):
2530         wm = context.window_manager
2531         return wm.invoke_props_dialog(self, width=600)
2532
2533
2534 class WM_OT_studiolight_uninstall(Operator):
2535     bl_idname = 'wm.studiolight_uninstall'
2536     bl_label = "Uninstall Studio Light"
2537     index: bpy.props.IntProperty()
2538
2539     def _remove_path(self, path):
2540         if path.exists():
2541             path.unlink()
2542
2543     def execute(self, context):
2544         import pathlib
2545         userpref = context.user_preferences
2546         for studio_light in userpref.studio_lights:
2547             if studio_light.index == self.index:
2548                 if studio_light.path:
2549                     self._remove_path(pathlib.Path(studio_light.path))
2550                 if studio_light.path_irr_cache:
2551                     self._remove_path(pathlib.Path(studio_light.path_irr_cache))
2552                 if studio_light.path_sh_cache:
2553                     self._remove_path(pathlib.Path(studio_light.path_sh_cache))
2554                 userpref.studio_lights.remove(studio_light)
2555                 return {'FINISHED'}
2556         return {'CANCELLED'}
2557
2558
2559 class WM_OT_studiolight_userpref_show(Operator):
2560     """Show light user preferences"""
2561     bl_idname = "wm.studiolight_userpref_show"
2562     bl_label = ""
2563     bl_options = {'INTERNAL'}
2564
2565     def execute(self, context):
2566         context.user_preferences.active_section = 'LIGHTS'
2567         bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
2568         return {'FINISHED'}
2569
2570
2571 class WM_MT_splash(Menu):
2572     bl_label = "Splash"
2573
2574     def draw_setup(self, context):
2575         wm = context.window_manager
2576         # userpref = context.user_preferences
2577
2578         layout = self.layout
2579
2580         layout.operator_context = 'EXEC_DEFAULT'
2581
2582         layout.label(text="Quick Setup")
2583
2584         split = layout.split(factor=0.25)
2585         split.label()
2586         split = split.split(factor=2.0 / 3.0)
2587
2588         col = split.column()
2589
2590         col.label()
2591
2592         sub = col.split(factor=0.35)
2593         row = sub.row()
2594         row.alignment = 'RIGHT'
2595         row.label(text="Shortcuts")
2596         text = bpy.path.display_name(wm.keyconfigs.active.name)
2597         if not text:
2598             text = "Blender"
2599         sub.menu("USERPREF_MT_keyconfigs", text=text)
2600
2601         kc = wm.keyconfigs.active
2602         kc_prefs = kc.preferences
2603         has_select_mouse = hasattr(kc_prefs, "select_mouse")
2604         if has_select_mouse:
2605             sub = col.split(factor=0.35)
2606             row = sub.row()
2607             row.alignment = 'RIGHT'
2608             row.label(text="Select With")
2609             sub.row().prop(kc_prefs, "select_mouse", expand=True)
2610             has_select_mouse = True
2611
2612         has_spacebar_action = hasattr(kc_prefs, "spacebar_action")
2613         if has_spacebar_action:
2614             sub = col.split(factor=0.35)
2615             row = sub.row()
2616             row.alignment = 'RIGHT'
2617             row.label(text="Spacebar")
2618             sub.row().prop(kc_prefs, "spacebar_action", expand=True)
2619             has_select_mouse = True
2620
2621         col.separator()
2622
2623         sub = col.split(factor=0.35)
2624         row = sub.row()
2625         row.alignment = 'RIGHT'
2626         row.label(text="Theme")
2627         label = bpy.types.USERPREF_MT_interface_theme_presets.bl_label
2628         if label == "Presets":
2629             label = "Blender Dark"
2630         sub.menu("USERPREF_MT_interface_theme_presets", text=label)
2631
2632         # We need to make switching to a language easier first
2633         #sub = col.split(factor=0.35)
2634         #row = sub.row()
2635         #row.alignment = 'RIGHT'
2636         #row.label(text="Language:")
2637         #userpref = context.user_preferences
2638         #sub.prop(userpref.system, "language", text="")
2639
2640         # Keep height constant
2641         if not has_select_mouse:
2642             col.label()
2643         if not has_spacebar_action:
2644             col.label()
2645
2646         layout.label()
2647
2648         row = layout.row()
2649
2650         sub = row.row()
2651         if bpy.types.WM_OT_copy_prev_settings.poll(context):
2652             old_version = bpy.types.WM_OT_copy_prev_settings.previous_version()
2653             sub.operator("wm.copy_prev_settings", text="Load %d.%d Settings" % old_version)
2654             sub.operator("wm.save_userpref", text="Save New Settings")
2655         else:
2656             sub.label()
2657             sub.label()
2658             sub.operator("wm.save_userpref", text="Next")
2659
2660         layout.separator()
2661         layout.separator()
2662
2663     def draw(self, context):
2664         # Draw setup screen if no user preferences have been saved yet.
2665         import os
2666
2667         user_path = bpy.utils.resource_path('USER')
2668         userdef_path = os.path.join(user_path, "config", "userpref.blend")
2669
2670         if not os.path.isfile(userdef_path):
2671             self.draw_setup(context)
2672             return
2673
2674         # Pass
2675         layout = self.layout
2676         layout.operator_context = 'EXEC_DEFAULT'
2677         layout.emboss = 'PULLDOWN_MENU'
2678
2679         split = layout.split()
2680
2681         # Templates
2682         col1 = split.column()
2683         col1.label(text="New File")
2684
2685         bpy.types.TOPBAR_MT_file_new.draw_ex(col1, context, use_splash=True)
2686
2687         # Recent
2688         col2 = split.column()
2689         col2_title = col2.row()
2690
2691         found_recent = col2.template_recent_files()
2692
2693         if found_recent:
2694             col2_title.label(text="Recent Files")
2695         else:
2696             # Links if no recent files
2697             col2_title.label(text="Getting Started")
2698
2699             col2.operator(
2700                 "wm.url_open", text="Manual", icon='URL'
2701             ).url = "https://docs.blender.org/manual/en/dev/"
2702             col2.operator(
2703                 "wm.url_open", text="Release Notes", icon='URL',
2704             ).url = "https://www.blender.org/download/releases/%d-%d/" % bpy.app.version[:2]
2705             col2.operator(
2706                 "wm.url_open", text="Blender Website", icon='URL',
2707             ).url = "https://www.blender.org"
2708             col2.operator(
2709                 "wm.url_open", text="Credits", icon='URL',
2710             ).url = "https://www.blender.org/about/credits/"
2711
2712         layout.separator()
2713
2714         split = layout.split()
2715
2716         col1 = split.column()
2717         sub = col1.row()
2718         sub.operator_context = 'INVOKE_DEFAULT'
2719         sub.operator("wm.open_mainfile", text="Open...", icon='FILE_FOLDER')
2720         col1.operator("wm.recover_last_session", icon='RECOVER_LAST')
2721
2722         col2 = split.column()
2723         if found_recent:
2724             col2.operator(
2725                 "wm.url_open", text="Release Notes", icon='URL',
2726             ).url = "https://www.blender.org/download/releases/%d-%d/" % bpy.app.version[:2]
2727             col2.operator(
2728                 "wm.url_open", text="Development Fund", icon='URL'
2729             ).url = "https://fund.blender.org"
2730         else:
2731             col2.operator(
2732                 "wm.url_open", text="Development Fund", icon='URL'
2733             ).url = "https://fund.blender.org"
2734             col2.operator(
2735                 "wm.url_open", text="Donate", icon='URL'
2736             ).url = "https://www.blender.org/foundation/donation-payment/"
2737
2738         layout.separator()
2739         layout.separator()
2740
2741
2742 class WM_OT_drop_blend_file(Operator):
2743     bl_idname = "wm.drop_blend_file"
2744     bl_label = "Handle dropped .blend file"
2745     bl_options = {'INTERNAL'}
2746
2747     filepath: StringProperty()
2748
2749     def invoke(self, context, event):
2750         context.window_manager.popup_menu(self.draw_menu, title=bpy.path.basename(self.filepath), icon='QUESTION')
2751         return {"FINISHED"}
2752
2753     def draw_menu(self, menu, context):
2754         layout = menu.layout
2755
2756         col = layout.column()
2757         col.operator_context = 'EXEC_DEFAULT'
2758         col.operator("wm.open_mainfile", text="Open", icon='FILE_FOLDER').filepath = self.filepath
2759
2760         layout.separator()
2761         col = layout.column()
2762         col.operator_context = 'INVOKE_DEFAULT'
2763         col.operator("wm.link", text="Link...", icon='LINK_BLEND').filepath = self.filepath
2764         col.operator("wm.append", text="Append...", icon='APPEND_BLEND').filepath = self.filepath
2765
2766 classes = (
2767     BRUSH_OT_active_index_set,
2768     WM_OT_addon_disable,
2769     WM_OT_addon_enable,
2770     WM_OT_addon_expand,
2771     WM_OT_addon_install,
2772     WM_OT_addon_refresh,
2773     WM_OT_addon_remove,
2774     WM_OT_addon_userpref_show,
2775     WM_OT_app_template_install,
2776     WM_OT_context_collection_boolean_set,
2777     WM_OT_context_cycle_array,
2778     WM_OT_context_cycle_enum,
2779     WM_OT_context_cycle_int,
2780     WM_OT_context_menu_enum,
2781     WM_OT_context_modal_mouse,
2782     WM_OT_context_pie_enum,
2783     WM_OT_context_scale_float,
2784     WM_OT_context_scale_int,
2785     WM_OT_context_set_boolean,
2786     WM_OT_context_set_enum,
2787     WM_OT_context_set_float,
2788     WM_OT_context_set_id,
2789     WM_OT_context_set_int,
2790     WM_OT_context_set_string,
2791     WM_OT_context_set_value,
2792     WM_OT_context_toggle,
2793     WM_OT_context_toggle_enum,
2794     WM_OT_copy_prev_settings,
2795     WM_OT_doc_view,
2796     WM_OT_doc_view_manual,
2797     WM_OT_drop_blend_file,
2798     WM_OT_keyconfig_activate,
2799     WM_OT_keyconfig_export,
2800     WM_OT_keyconfig_import,
2801     WM_OT_keyconfig_remove,
2802     WM_OT_keyconfig_test,
2803     WM_OT_keyitem_add,
2804     WM_OT_keyitem_remove,
2805     WM_OT_keyitem_restore,
2806     WM_OT_keymap_restore,
2807     WM_OT_operator_cheat_sheet,
2808     WM_OT_operator_pie_enum,
2809     WM_OT_path_open,
2810     WM_OT_properties_add,
2811     WM_OT_properties_context_change,
2812     WM_OT_properties_edit,
2813     WM_OT_properties_remove,
2814     WM_OT_sysinfo,
2815     WM_OT_theme_install,
2816     WM_OT_owner_disable,
2817     WM_OT_owner_enable,
2818     WM_OT_url_open,
2819     WM_OT_studiolight_install,
2820     WM_OT_studiolight_new,
2821     WM_OT_studiolight_uninstall,
2822     WM_OT_studiolight_userpref_show,
2823     WM_OT_tool_set_by_name,
2824     WM_OT_toolbar,
2825     WM_MT_splash,
2826 )