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