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