Cleanup: use keyword only args to rna_idprop_ui_create
[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 )
26 from bpy.props import (
27     BoolProperty,
28     EnumProperty,
29     FloatProperty,
30     IntProperty,
31     StringProperty,
32 )
33
34 # FIXME, we need a way to detect key repeat events.
35 # unfortunately checking event previous values isn't reliable.
36 use_toolbar_release_hack = True
37
38
39 rna_path_prop = StringProperty(
40     name="Context Attributes",
41     description="RNA context string",
42     maxlen=1024,
43 )
44
45 rna_reverse_prop = BoolProperty(
46     name="Reverse",
47     description="Cycle backwards",
48     default=False,
49 )
50
51 rna_wrap_prop = BoolProperty(
52     name="Wrap",
53     description="Wrap back to the first/last values",
54     default=False,
55 )
56
57 rna_relative_prop = BoolProperty(
58     name="Relative",
59     description="Apply relative to the current value (delta)",
60     default=False,
61 )
62
63 rna_space_type_prop = EnumProperty(
64     name="Type",
65     items=tuple(
66         (e.identifier, e.name, "", e. value)
67         for e in bpy.types.Space.bl_rna.properties["type"].enum_items
68     ),
69     default='EMPTY',
70 )
71
72
73 def context_path_validate(context, data_path):
74     try:
75         value = eval("context.%s" % data_path) if data_path else Ellipsis
76     except AttributeError as ex:
77         if str(ex).startswith("'NoneType'"):
78             # One of the items in the rna path is None, just ignore this
79             value = Ellipsis
80         else:
81             # We have a real error in the rna path, don't ignore that
82             raise
83
84     return value
85
86
87 def operator_value_is_undo(value):
88     if value in {None, Ellipsis}:
89         return False
90
91     # typical properties or objects
92     id_data = getattr(value, "id_data", Ellipsis)
93
94     if id_data is None:
95         return False
96     elif id_data is Ellipsis:
97         # handle mathutils types
98         id_data = getattr(getattr(value, "owner", None), "id_data", None)
99
100         if id_data is None:
101             return False
102
103     # return True if its a non window ID type
104     return (isinstance(id_data, bpy.types.ID) and
105             (not isinstance(id_data, (bpy.types.WindowManager,
106                                       bpy.types.Screen,
107                                       bpy.types.Brush,
108                                       ))))
109
110
111 def operator_path_is_undo(context, data_path):
112     # note that if we have data paths that use strings this could fail
113     # luckily we don't do this!
114     #
115     # When we can't find the data owner assume no undo is needed.
116     data_path_head = data_path.rpartition(".")[0]
117
118     if not data_path_head:
119         return False
120
121     value = context_path_validate(context, data_path_head)
122
123     return operator_value_is_undo(value)
124
125
126 def operator_path_undo_return(context, data_path):
127     return {'FINISHED'} if operator_path_is_undo(context, data_path) else {'CANCELLED'}
128
129
130 def operator_value_undo_return(value):
131     return {'FINISHED'} if operator_value_is_undo(value) else {'CANCELLED'}
132
133
134 def execute_context_assign(self, context):
135     data_path = self.data_path
136     if context_path_validate(context, data_path) is Ellipsis:
137         return {'PASS_THROUGH'}
138
139     if getattr(self, "relative", False):
140         exec("context.%s += self.value" % data_path)
141     else:
142         exec("context.%s = self.value" % data_path)
143
144     return operator_path_undo_return(context, data_path)
145
146
147 class WM_OT_context_set_boolean(Operator):
148     """Set a context value"""
149     bl_idname = "wm.context_set_boolean"
150     bl_label = "Context Set Boolean"
151     bl_options = {'UNDO', 'INTERNAL'}
152
153     data_path: rna_path_prop
154     value: BoolProperty(
155         name="Value",
156         description="Assignment value",
157         default=True,
158     )
159
160     execute = execute_context_assign
161
162
163 class WM_OT_context_set_int(Operator):  # same as enum
164     """Set a context value"""
165     bl_idname = "wm.context_set_int"
166     bl_label = "Context Set"
167     bl_options = {'UNDO', 'INTERNAL'}
168
169     data_path: rna_path_prop
170     value: IntProperty(
171         name="Value",
172         description="Assign value",
173         default=0,
174     )
175     relative: rna_relative_prop
176
177     execute = execute_context_assign
178
179
180 class WM_OT_context_scale_float(Operator):
181     """Scale a float context value"""
182     bl_idname = "wm.context_scale_float"
183     bl_label = "Context Scale Float"
184     bl_options = {'UNDO', 'INTERNAL'}
185
186     data_path: rna_path_prop
187     value: FloatProperty(
188         name="Value",
189         description="Assign value",
190         default=1.0,
191     )
192
193     def execute(self, context):
194         data_path = self.data_path
195         if context_path_validate(context, data_path) is Ellipsis:
196             return {'PASS_THROUGH'}
197
198         value = self.value
199
200         if value == 1.0:  # nothing to do
201             return {'CANCELLED'}
202
203         exec("context.%s *= value" % data_path)
204
205         return operator_path_undo_return(context, data_path)
206
207
208 class WM_OT_context_scale_int(Operator):
209     """Scale an int context value"""
210     bl_idname = "wm.context_scale_int"
211     bl_label = "Context Scale Int"
212     bl_options = {'UNDO', 'INTERNAL'}
213
214     data_path: rna_path_prop
215     value: FloatProperty(
216         name="Value",
217         description="Assign value",
218         default=1.0,
219     )
220     always_step: BoolProperty(
221         name="Always Step",
222         description="Always adjust the value by a minimum of 1 when 'value' is not 1.0",
223         default=True,
224     )
225
226     def execute(self, context):
227         data_path = self.data_path
228         if context_path_validate(context, data_path) is Ellipsis:
229             return {'PASS_THROUGH'}
230
231         value = self.value
232
233         if value == 1.0:  # nothing to do
234             return {'CANCELLED'}
235
236         if getattr(self, "always_step", False):
237             if value > 1.0:
238                 add = "1"
239                 func = "max"
240             else:
241                 add = "-1"
242                 func = "min"
243             exec("context.%s = %s(round(context.%s * value), context.%s + %s)" %
244                  (data_path, func, data_path, data_path, add))
245         else:
246             exec("context.%s *= value" % data_path)
247
248         return operator_path_undo_return(context, data_path)
249
250
251 class WM_OT_context_set_float(Operator):  # same as enum
252     """Set a context value"""
253     bl_idname = "wm.context_set_float"
254     bl_label = "Context Set Float"
255     bl_options = {'UNDO', 'INTERNAL'}
256
257     data_path: rna_path_prop
258     value: FloatProperty(
259         name="Value",
260         description="Assignment value",
261         default=0.0,
262     )
263     relative: rna_relative_prop
264
265     execute = execute_context_assign
266
267
268 class WM_OT_context_set_string(Operator):  # same as enum
269     """Set a context value"""
270     bl_idname = "wm.context_set_string"
271     bl_label = "Context Set String"
272     bl_options = {'UNDO', 'INTERNAL'}
273
274     data_path: rna_path_prop
275     value: StringProperty(
276         name="Value",
277         description="Assign value",
278         maxlen=1024,
279     )
280
281     execute = execute_context_assign
282
283
284 class WM_OT_context_set_enum(Operator):
285     """Set a context value"""
286     bl_idname = "wm.context_set_enum"
287     bl_label = "Context Set Enum"
288     bl_options = {'UNDO', 'INTERNAL'}
289
290     data_path: rna_path_prop
291     value: StringProperty(
292         name="Value",
293         description="Assignment value (as a string)",
294         maxlen=1024,
295     )
296
297     execute = execute_context_assign
298
299
300 class WM_OT_context_set_value(Operator):
301     """Set a context value"""
302     bl_idname = "wm.context_set_value"
303     bl_label = "Context Set Value"
304     bl_options = {'UNDO', 'INTERNAL'}
305
306     data_path: rna_path_prop
307     value: StringProperty(
308         name="Value",
309         description="Assignment value (as a string)",
310         maxlen=1024,
311     )
312
313     def execute(self, context):
314         data_path = self.data_path
315         if context_path_validate(context, data_path) is Ellipsis:
316             return {'PASS_THROUGH'}
317         exec("context.%s = %s" % (data_path, self.value))
318         return operator_path_undo_return(context, data_path)
319
320
321 class WM_OT_context_toggle(Operator):
322     """Toggle a context value"""
323     bl_idname = "wm.context_toggle"
324     bl_label = "Context Toggle"
325     bl_options = {'UNDO', 'INTERNAL'}
326
327     data_path: rna_path_prop
328
329     def execute(self, context):
330         data_path = self.data_path
331
332         if context_path_validate(context, data_path) is Ellipsis:
333             return {'PASS_THROUGH'}
334
335         exec("context.%s = not (context.%s)" % (data_path, data_path))
336
337         return operator_path_undo_return(context, data_path)
338
339
340 class WM_OT_context_toggle_enum(Operator):
341     """Toggle a context value"""
342     bl_idname = "wm.context_toggle_enum"
343     bl_label = "Context Toggle Values"
344     bl_options = {'UNDO', 'INTERNAL'}
345
346     data_path: rna_path_prop
347     value_1: StringProperty(
348         name="Value",
349         description="Toggle enum",
350         maxlen=1024,
351     )
352     value_2: StringProperty(
353         name="Value",
354         description="Toggle enum",
355         maxlen=1024,
356     )
357
358     def execute(self, context):
359         data_path = self.data_path
360
361         if context_path_validate(context, data_path) is Ellipsis:
362             return {'PASS_THROUGH'}
363
364         # failing silently is not ideal, but we don't want errors for shortcut
365         # keys that some values that are only available in a particular context
366         try:
367             exec("context.%s = ('%s', '%s')[context.%s != '%s']" %
368                  (data_path, self.value_1,
369                   self.value_2, data_path,
370                   self.value_2,
371                   ))
372         except:
373             return {'PASS_THROUGH'}
374
375         return operator_path_undo_return(context, data_path)
376
377
378 class WM_OT_context_cycle_int(Operator):
379     """Set a context value (useful for cycling active material, """ \
380         """vertex keys, groups, etc.)"""
381     bl_idname = "wm.context_cycle_int"
382     bl_label = "Context Int Cycle"
383     bl_options = {'UNDO', 'INTERNAL'}
384
385     data_path: rna_path_prop
386     reverse: rna_reverse_prop
387     wrap: rna_wrap_prop
388
389     def execute(self, context):
390         data_path = self.data_path
391         value = context_path_validate(context, data_path)
392         if value is Ellipsis:
393             return {'PASS_THROUGH'}
394
395         if self.reverse:
396             value -= 1
397         else:
398             value += 1
399
400         exec("context.%s = value" % data_path)
401
402         if self.wrap:
403             if value != eval("context.%s" % data_path):
404                 # relies on rna clamping integers out of the range
405                 if self.reverse:
406                     value = (1 << 31) - 1
407                 else:
408                     value = -1 << 31
409
410                 exec("context.%s = value" % data_path)
411
412         return operator_path_undo_return(context, data_path)
413
414
415 class WM_OT_context_cycle_enum(Operator):
416     """Toggle a context value"""
417     bl_idname = "wm.context_cycle_enum"
418     bl_label = "Context Enum Cycle"
419     bl_options = {'UNDO', 'INTERNAL'}
420
421     data_path: rna_path_prop
422     reverse: rna_reverse_prop
423     wrap: rna_wrap_prop
424
425     def execute(self, context):
426         data_path = self.data_path
427         value = context_path_validate(context, data_path)
428         if value is Ellipsis:
429             return {'PASS_THROUGH'}
430
431         orig_value = value
432
433         # Have to get rna enum values
434         rna_struct_str, rna_prop_str = data_path.rsplit('.', 1)
435         i = rna_prop_str.find('[')
436
437         # just in case we get "context.foo.bar[0]"
438         if i != -1:
439             rna_prop_str = rna_prop_str[0:i]
440
441         rna_struct = eval("context.%s.rna_type" % rna_struct_str)
442
443         rna_prop = rna_struct.properties[rna_prop_str]
444
445         if type(rna_prop) != bpy.types.EnumProperty:
446             raise Exception("expected an enum property")
447
448         enums = rna_struct.properties[rna_prop_str].enum_items.keys()
449         orig_index = enums.index(orig_value)
450
451         # Have the info we need, advance to the next item.
452         #
453         # When wrap's disabled we may set the value to its self,
454         # this is done to ensure update callbacks run.
455         if self.reverse:
456             if orig_index == 0:
457                 advance_enum = enums[-1] if self.wrap else enums[0]
458             else:
459                 advance_enum = enums[orig_index - 1]
460         else:
461             if orig_index == len(enums) - 1:
462                 advance_enum = enums[0] if self.wrap else enums[-1]
463             else:
464                 advance_enum = enums[orig_index + 1]
465
466         # set the new value
467         exec("context.%s = advance_enum" % data_path)
468         return operator_path_undo_return(context, data_path)
469
470
471 class WM_OT_context_cycle_array(Operator):
472     """Set a context array value """ \
473         """(useful for cycling the active mesh edit mode)"""
474     bl_idname = "wm.context_cycle_array"
475     bl_label = "Context Array Cycle"
476     bl_options = {'UNDO', 'INTERNAL'}
477
478     data_path: rna_path_prop
479     reverse: rna_reverse_prop
480
481     def execute(self, context):
482         data_path = self.data_path
483         value = context_path_validate(context, data_path)
484         if value is Ellipsis:
485             return {'PASS_THROUGH'}
486
487         def cycle(array):
488             if self.reverse:
489                 array.insert(0, array.pop())
490             else:
491                 array.append(array.pop(0))
492             return array
493
494         exec("context.%s = cycle(context.%s[:])" % (data_path, data_path))
495
496         return operator_path_undo_return(context, data_path)
497
498
499 class WM_OT_context_menu_enum(Operator):
500     bl_idname = "wm.context_menu_enum"
501     bl_label = "Context Enum Menu"
502     bl_options = {'UNDO', 'INTERNAL'}
503
504     data_path: rna_path_prop
505
506     def execute(self, context):
507         data_path = self.data_path
508         value = context_path_validate(context, data_path)
509
510         if value is Ellipsis:
511             return {'PASS_THROUGH'}
512
513         base_path, prop_string = data_path.rsplit(".", 1)
514         value_base = context_path_validate(context, base_path)
515         prop = value_base.bl_rna.properties[prop_string]
516
517         def draw_cb(self, context):
518             layout = self.layout
519             layout.prop(value_base, prop_string, expand=True)
520
521         context.window_manager.popup_menu(draw_func=draw_cb, title=prop.name, icon=prop.icon)
522
523         return {'FINISHED'}
524
525
526 class WM_OT_context_pie_enum(Operator):
527     bl_idname = "wm.context_pie_enum"
528     bl_label = "Context Enum Pie"
529     bl_options = {'UNDO', 'INTERNAL'}
530
531     data_path: rna_path_prop
532
533     def invoke(self, context, event):
534         wm = context.window_manager
535         data_path = self.data_path
536         value = context_path_validate(context, data_path)
537
538         if value is Ellipsis:
539             return {'PASS_THROUGH'}
540
541         base_path, prop_string = data_path.rsplit(".", 1)
542         value_base = context_path_validate(context, base_path)
543         prop = value_base.bl_rna.properties[prop_string]
544
545         def draw_cb(self, context):
546             layout = self.layout
547             layout.prop(value_base, prop_string, expand=True)
548
549         wm.popup_menu_pie(draw_func=draw_cb, title=prop.name, icon=prop.icon, event=event)
550
551         return {'FINISHED'}
552
553
554 class WM_OT_operator_pie_enum(Operator):
555     bl_idname = "wm.operator_pie_enum"
556     bl_label = "Operator Enum Pie"
557     bl_options = {'UNDO', 'INTERNAL'}
558
559     data_path: StringProperty(
560         name="Operator",
561         description="Operator name (in python as string)",
562         maxlen=1024,
563     )
564     prop_string: StringProperty(
565         name="Property",
566         description="Property name (as a string)",
567         maxlen=1024,
568     )
569
570     def invoke(self, context, event):
571         wm = context.window_manager
572
573         data_path = self.data_path
574         prop_string = self.prop_string
575
576         # same as eval("bpy.ops." + data_path)
577         op_mod_str, ob_id_str = data_path.split(".", 1)
578         op = getattr(getattr(bpy.ops, op_mod_str), ob_id_str)
579         del op_mod_str, ob_id_str
580
581         try:
582             op_rna = op.get_rna_type()
583         except KeyError:
584             self.report({'ERROR'}, "Operator not found: bpy.ops.%s" % data_path)
585             return {'CANCELLED'}
586
587         def draw_cb(self, context):
588             layout = self.layout
589             pie = layout.menu_pie()
590             pie.operator_enum(data_path, prop_string)
591
592         wm.popup_menu_pie(draw_func=draw_cb, title=op_rna.name, event=event)
593
594         return {'FINISHED'}
595
596
597 class WM_OT_context_set_id(Operator):
598     """Set a context value to an ID data-block"""
599     bl_idname = "wm.context_set_id"
600     bl_label = "Set Library ID"
601     bl_options = {'UNDO', 'INTERNAL'}
602
603     data_path: rna_path_prop
604     value: StringProperty(
605         name="Value",
606         description="Assign value",
607         maxlen=1024,
608     )
609
610     def execute(self, context):
611         value = self.value
612         data_path = self.data_path
613
614         # match the pointer type from the target property to bpy.data.*
615         # so we lookup the correct list.
616         data_path_base, data_path_prop = data_path.rsplit(".", 1)
617         data_prop_rna = eval("context.%s" % data_path_base).rna_type.properties[data_path_prop]
618         data_prop_rna_type = data_prop_rna.fixed_type
619
620         id_iter = None
621
622         for prop in bpy.data.rna_type.properties:
623             if prop.rna_type.identifier == "CollectionProperty":
624                 if prop.fixed_type == data_prop_rna_type:
625                     id_iter = prop.identifier
626                     break
627
628         if id_iter:
629             value_id = getattr(bpy.data, id_iter).get(value)
630             exec("context.%s = value_id" % data_path)
631
632         return operator_path_undo_return(context, data_path)
633
634
635 doc_id = StringProperty(
636     name="Doc ID",
637     maxlen=1024,
638     options={'HIDDEN'},
639 )
640
641 data_path_iter = StringProperty(
642     description="The data path relative to the context, must point to an iterable")
643
644 data_path_item = StringProperty(
645     description="The data path from each iterable to the value (int or float)")
646
647
648 class WM_OT_context_collection_boolean_set(Operator):
649     """Set boolean values for a collection of items"""
650     bl_idname = "wm.context_collection_boolean_set"
651     bl_label = "Context Collection Boolean Set"
652     bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
653
654     data_path_iter: data_path_iter
655     data_path_item: data_path_item
656
657     type: EnumProperty(
658         name="Type",
659         items=(
660             ('TOGGLE', "Toggle", ""),
661             ('ENABLE', "Enable", ""),
662             ('DISABLE', "Disable", ""),
663         ),
664     )
665
666     def execute(self, context):
667         data_path_iter = self.data_path_iter
668         data_path_item = self.data_path_item
669
670         items = list(getattr(context, data_path_iter))
671         items_ok = []
672         is_set = False
673         for item in items:
674             try:
675                 value_orig = eval("item." + data_path_item)
676             except:
677                 continue
678
679             if value_orig is True:
680                 is_set = True
681             elif value_orig is False:
682                 pass
683             else:
684                 self.report({'WARNING'}, "Non boolean value found: %s[ ].%s" %
685                             (data_path_iter, data_path_item))
686                 return {'CANCELLED'}
687
688             items_ok.append(item)
689
690         # avoid undo push when nothing to do
691         if not items_ok:
692             return {'CANCELLED'}
693
694         if self.type == 'ENABLE':
695             is_set = True
696         elif self.type == 'DISABLE':
697             is_set = False
698         else:
699             is_set = not is_set
700
701         exec_str = "item.%s = %s" % (data_path_item, is_set)
702         for item in items_ok:
703             exec(exec_str)
704
705         return operator_value_undo_return(item)
706
707
708 class WM_OT_context_modal_mouse(Operator):
709     """Adjust arbitrary values with mouse input"""
710     bl_idname = "wm.context_modal_mouse"
711     bl_label = "Context Modal Mouse"
712     bl_options = {'GRAB_CURSOR', 'BLOCKING', 'UNDO', 'INTERNAL'}
713
714     data_path_iter: data_path_iter
715     data_path_item: data_path_item
716     header_text: StringProperty(
717         name="Header Text",
718         description="Text to display in header during scale",
719     )
720
721     input_scale: FloatProperty(
722         description="Scale the mouse movement by this value before applying the delta",
723         default=0.01,
724     )
725     invert: BoolProperty(
726         description="Invert the mouse input",
727         default=False,
728     )
729     initial_x: IntProperty(options={'HIDDEN'})
730
731     def _values_store(self, context):
732         data_path_iter = self.data_path_iter
733         data_path_item = self.data_path_item
734
735         self._values = values = {}
736
737         for item in getattr(context, data_path_iter):
738             try:
739                 value_orig = eval("item." + data_path_item)
740             except:
741                 continue
742
743             # check this can be set, maybe this is library data.
744             try:
745                 exec("item.%s = %s" % (data_path_item, value_orig))
746             except:
747                 continue
748
749             values[item] = value_orig
750
751     def _values_delta(self, delta):
752         delta *= self.input_scale
753         if self.invert:
754             delta = - delta
755
756         data_path_item = self.data_path_item
757         for item, value_orig in self._values.items():
758             if type(value_orig) == int:
759                 exec("item.%s = int(%d)" % (data_path_item, round(value_orig + delta)))
760             else:
761                 exec("item.%s = %f" % (data_path_item, value_orig + delta))
762
763     def _values_restore(self):
764         data_path_item = self.data_path_item
765         for item, value_orig in self._values.items():
766             exec("item.%s = %s" % (data_path_item, value_orig))
767
768         self._values.clear()
769
770     def _values_clear(self):
771         self._values.clear()
772
773     def modal(self, context, event):
774         event_type = event.type
775
776         if event_type == 'MOUSEMOVE':
777             delta = event.mouse_x - self.initial_x
778             self._values_delta(delta)
779             header_text = self.header_text
780             if header_text:
781                 if len(self._values) == 1:
782                     (item, ) = self._values.keys()
783                     header_text = header_text % eval("item.%s" % self.data_path_item)
784                 else:
785                     header_text = (self.header_text % delta) + " (delta)"
786                 context.area.header_text_set(header_text)
787
788         elif 'LEFTMOUSE' == event_type:
789             item = next(iter(self._values.keys()))
790             self._values_clear()
791             context.area.header_text_set(None)
792             return operator_value_undo_return(item)
793
794         elif event_type in {'RIGHTMOUSE', 'ESC'}:
795             self._values_restore()
796             context.area.header_text_set(None)
797             return {'CANCELLED'}
798
799         return {'RUNNING_MODAL'}
800
801     def invoke(self, context, event):
802         self._values_store(context)
803
804         if not self._values:
805             self.report({'WARNING'}, "Nothing to operate on: %s[ ].%s" %
806                         (self.data_path_iter, self.data_path_item))
807
808             return {'CANCELLED'}
809         else:
810             self.initial_x = event.mouse_x
811
812             context.window_manager.modal_handler_add(self)
813             return {'RUNNING_MODAL'}
814
815
816 class WM_OT_url_open(Operator):
817     """Open a website in the web-browser"""
818     bl_idname = "wm.url_open"
819     bl_label = ""
820     bl_options = {'INTERNAL'}
821
822     url: StringProperty(
823         name="URL",
824         description="URL to open",
825     )
826
827     def execute(self, context):
828         import webbrowser
829         webbrowser.open(self.url)
830         return {'FINISHED'}
831
832
833 class WM_OT_path_open(Operator):
834     """Open a path in a file browser"""
835     bl_idname = "wm.path_open"
836     bl_label = ""
837     bl_options = {'INTERNAL'}
838
839     filepath: StringProperty(
840         subtype='FILE_PATH',
841         options={'SKIP_SAVE'},
842     )
843
844     def execute(self, context):
845         import sys
846         import os
847         import subprocess
848
849         filepath = self.filepath
850
851         if not filepath:
852             self.report({'ERROR'}, "File path was not set")
853             return {'CANCELLED'}
854
855         filepath = bpy.path.abspath(filepath)
856         filepath = os.path.normpath(filepath)
857
858         if not os.path.exists(filepath):
859             self.report({'ERROR'}, "File '%s' not found" % filepath)
860             return {'CANCELLED'}
861
862         if sys.platform[:3] == "win":
863             os.startfile(filepath)
864         elif sys.platform == "darwin":
865             subprocess.check_call(["open", filepath])
866         else:
867             try:
868                 subprocess.check_call(["xdg-open", filepath])
869             except:
870                 # xdg-open *should* be supported by recent Gnome, KDE, Xfce
871                 import traceback
872                 traceback.print_exc()
873
874         return {'FINISHED'}
875
876
877 def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""):
878
879     def operator_exists_pair(a, b):
880         # Not fast, this is only for docs.
881         return b in dir(getattr(bpy.ops, a))
882
883     def operator_exists_single(a):
884         a, b = a.partition("_OT_")[::2]
885         return operator_exists_pair(a.lower(), b)
886
887     id_split = doc_id.split(".")
888     url = rna = None
889
890     if len(id_split) == 1:  # rna, class
891         if do_url:
892             url = "%s/bpy.types.%s.html" % (url_prefix, id_split[0])
893         else:
894             rna = "bpy.types.%s" % id_split[0]
895
896     elif len(id_split) == 2:  # rna, class.prop
897         class_name, class_prop = id_split
898
899         # an operator (common case - just button referencing an op)
900         if operator_exists_pair(class_name, class_prop):
901             if do_url:
902                 url = (
903                     "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
904                     (url_prefix, class_name, class_name, class_prop)
905                 )
906             else:
907                 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
908         elif operator_exists_single(class_name):
909             # note: ignore the prop name since we don't have a way to link into it
910             class_name, class_prop = class_name.split("_OT_", 1)
911             class_name = class_name.lower()
912             if do_url:
913                 url = (
914                     "%s/bpy.ops.%s.html#bpy.ops.%s.%s" %
915                     (url_prefix, class_name, class_name, class_prop)
916                 )
917             else:
918                 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
919         else:
920             # an RNA setting, common case
921             rna_class = getattr(bpy.types, class_name)
922
923             # detect if this is a inherited member and use that name instead
924             rna_parent = rna_class.bl_rna
925             rna_prop = rna_parent.properties.get(class_prop)
926             if rna_prop:
927                 rna_parent = rna_parent.base
928                 while rna_parent and rna_prop == rna_parent.properties.get(class_prop):
929                     class_name = rna_parent.identifier
930                     rna_parent = rna_parent.base
931
932                 if do_url:
933                     url = (
934                         "%s/bpy.types.%s.html#bpy.types.%s.%s" %
935                         (url_prefix, class_name, class_name, class_prop)
936                     )
937                 else:
938                     rna = "bpy.types.%s.%s" % (class_name, class_prop)
939             else:
940                 # We assume this is custom property, only try to generate generic url/rna_id...
941                 if do_url:
942                     url = ("%s/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items" % (url_prefix,))
943                 else:
944                     rna = "bpy.types.bpy_struct"
945
946     return url if do_url else rna
947
948
949 class WM_OT_doc_view_manual(Operator):
950     """Load online manual"""
951     bl_idname = "wm.doc_view_manual"
952     bl_label = "View Manual"
953
954     doc_id: doc_id
955
956     @staticmethod
957     def _find_reference(rna_id, url_mapping, verbose=True):
958         if verbose:
959             print("online manual check for: '%s'... " % rna_id)
960         from fnmatch import fnmatchcase
961         # XXX, for some reason all RNA ID's are stored lowercase
962         # Adding case into all ID's isn't worth the hassle so force lowercase.
963         rna_id = rna_id.lower()
964         for pattern, url_suffix in url_mapping:
965             if fnmatchcase(rna_id, pattern):
966                 if verbose:
967                     print("            match found: '%s' --> '%s'" % (pattern, url_suffix))
968                 return url_suffix
969         if verbose:
970             print("match not found")
971         return None
972
973     @staticmethod
974     def _lookup_rna_url(rna_id, verbose=True):
975         for prefix, url_manual_mapping in bpy.utils.manual_map():
976             rna_ref = WM_OT_doc_view_manual._find_reference(rna_id, url_manual_mapping, verbose=verbose)
977             if rna_ref is not None:
978                 url = prefix + rna_ref
979                 return url
980
981     def execute(self, context):
982         rna_id = _wm_doc_get_id(self.doc_id, do_url=False)
983         if rna_id is None:
984             return {'PASS_THROUGH'}
985
986         url = self._lookup_rna_url(rna_id)
987
988         if url is None:
989             self.report(
990                 {'WARNING'},
991                 "No reference available %r, "
992                 "Update info in 'rna_manual_reference.py' "
993                 "or callback to bpy.utils.manual_map()" %
994                 self.doc_id
995             )
996             return {'CANCELLED'}
997         else:
998             import webbrowser
999             webbrowser.open(url)
1000             return {'FINISHED'}
1001
1002
1003 class WM_OT_doc_view(Operator):
1004     """Load online reference docs"""
1005     bl_idname = "wm.doc_view"
1006     bl_label = "View Documentation"
1007
1008     doc_id: doc_id
1009     if bpy.app.version_cycle == "release":
1010         _prefix = ("https://docs.blender.org/api/current")
1011     else:
1012         _prefix = ("https://docs.blender.org/api/master")
1013
1014     def execute(self, context):
1015         url = _wm_doc_get_id(self.doc_id, do_url=True, url_prefix=self._prefix)
1016         if url is None:
1017             return {'PASS_THROUGH'}
1018
1019         import webbrowser
1020         webbrowser.open(url)
1021
1022         return {'FINISHED'}
1023
1024
1025 rna_path = StringProperty(
1026     name="Property Edit",
1027     description="Property data_path edit",
1028     maxlen=1024,
1029     options={'HIDDEN'},
1030 )
1031
1032 rna_value = StringProperty(
1033     name="Property Value",
1034     description="Property value edit",
1035     maxlen=1024,
1036 )
1037
1038 rna_default = StringProperty(
1039     name="Default Value",
1040     description="Default value of the property. Important for NLA mixing",
1041     maxlen=1024,
1042 )
1043
1044 rna_property = StringProperty(
1045     name="Property Name",
1046     description="Property name edit",
1047     maxlen=1024,
1048 )
1049
1050 rna_min = FloatProperty(
1051     name="Min",
1052     default=-10000.0,
1053     precision=3,
1054 )
1055
1056 rna_max = FloatProperty(
1057     name="Max",
1058     default=10000.0,
1059     precision=3,
1060 )
1061
1062 rna_use_soft_limits = BoolProperty(
1063     name="Use Soft Limits",
1064 )
1065
1066 rna_is_overridable_static = BoolProperty(
1067     name="Is Statically Overridable",
1068     default=False,
1069 )
1070
1071
1072 class WM_OT_properties_edit(Operator):
1073     bl_idname = "wm.properties_edit"
1074     bl_label = "Edit Property"
1075     # register only because invoke_props_popup requires.
1076     bl_options = {'REGISTER', 'INTERNAL'}
1077
1078     data_path: rna_path
1079     property: rna_property
1080     value: rna_value
1081     default: rna_default
1082     min: rna_min
1083     max: rna_max
1084     use_soft_limits: rna_use_soft_limits
1085     is_overridable_static: rna_is_overridable_static
1086     soft_min: rna_min
1087     soft_max: rna_max
1088     description: StringProperty(
1089         name="Tooltip",
1090     )
1091
1092     def _cmp_props_get(self):
1093         # Changing these properties will refresh the UI
1094         return {
1095             "use_soft_limits": self.use_soft_limits,
1096             "soft_range": (self.soft_min, self.soft_max),
1097             "hard_range": (self.min, self.max),
1098         }
1099
1100     def get_value_eval(self):
1101         try:
1102             value_eval = eval(self.value)
1103             # assert else None -> None, not "None", see [#33431]
1104             assert(type(value_eval) in {str, float, int, bool, tuple, list})
1105         except:
1106             value_eval = self.value
1107
1108         return value_eval
1109
1110     def get_default_eval(self):
1111         try:
1112             default_eval = eval(self.default)
1113             # assert else None -> None, not "None", see [#33431]
1114             assert(type(default_eval) in {str, float, int, bool, tuple, list})
1115         except:
1116             default_eval = self.default
1117
1118         return default_eval
1119
1120     def execute(self, context):
1121         from rna_prop_ui import (
1122             rna_idprop_ui_prop_get,
1123             rna_idprop_ui_prop_clear,
1124             rna_idprop_ui_prop_update,
1125         )
1126
1127         data_path = self.data_path
1128         prop = self.property
1129
1130         prop_old = getattr(self, "_last_prop", [None])[0]
1131
1132         if prop_old is None:
1133             self.report({'ERROR'}, "Direct execution not supported")
1134             return {'CANCELLED'}
1135
1136         value_eval = self.get_value_eval()
1137         default_eval = self.get_default_eval()
1138
1139         # First remove
1140         item = eval("context.%s" % data_path)
1141         prop_type_old = type(item[prop_old])
1142
1143         rna_idprop_ui_prop_clear(item, prop_old)
1144         exec_str = "del item[%r]" % prop_old
1145         # print(exec_str)
1146         exec(exec_str)
1147
1148         # Reassign
1149         exec_str = "item[%r] = %s" % (prop, repr(value_eval))
1150         # print(exec_str)
1151         exec(exec_str)
1152
1153         exec_str = "item.property_overridable_static_set('[\"%s\"]', %s)" % (prop, self.is_overridable_static)
1154         exec(exec_str)
1155
1156         rna_idprop_ui_prop_update(item, prop)
1157
1158         self._last_prop[:] = [prop]
1159
1160         prop_type = type(item[prop])
1161
1162         prop_ui = rna_idprop_ui_prop_get(item, prop)
1163
1164         if prop_type in {float, int}:
1165             prop_ui["min"] = prop_type(self.min)
1166             prop_ui["max"] = prop_type(self.max)
1167             if type(default_eval) in {float, int} and default_eval != 0:
1168                 prop_ui["default"] = prop_type(default_eval)
1169
1170             if self.use_soft_limits:
1171                 prop_ui["soft_min"] = prop_type(self.soft_min)
1172                 prop_ui["soft_max"] = prop_type(self.soft_max)
1173             else:
1174                 prop_ui["soft_min"] = prop_type(self.min)
1175                 prop_ui["soft_max"] = prop_type(self.max)
1176
1177         prop_ui["description"] = self.description
1178
1179         # If we have changed the type of the property, update its potential anim curves!
1180         if prop_type_old != prop_type:
1181             data_path = '["%s"]' % bpy.utils.escape_identifier(prop)
1182             done = set()
1183
1184             def _update(fcurves):
1185                 for fcu in fcurves:
1186                     if fcu not in done and fcu.data_path == data_path:
1187                         fcu.update_autoflags(item)
1188                         done.add(fcu)
1189
1190             def _update_strips(strips):
1191                 for st in strips:
1192                     if st.type == 'CLIP' and st.action:
1193                         _update(st.action.fcurves)
1194                     elif st.type == 'META':
1195                         _update_strips(st.strips)
1196
1197             adt = getattr(item, "animation_data", None)
1198             if adt is not None:
1199                 if adt.action:
1200                     _update(adt.action.fcurves)
1201                 if adt.drivers:
1202                     _update(adt.drivers)
1203                 if adt.nla_tracks:
1204                     for nt in adt.nla_tracks:
1205                         _update_strips(nt.strips)
1206
1207         # otherwise existing buttons which reference freed
1208         # memory may crash blender [#26510]
1209         # context.area.tag_redraw()
1210         for win in context.window_manager.windows:
1211             for area in win.screen.areas:
1212                 area.tag_redraw()
1213
1214         return {'FINISHED'}
1215
1216     def invoke(self, context, event):
1217         from rna_prop_ui import rna_idprop_ui_prop_get
1218
1219         data_path = self.data_path
1220
1221         if not data_path:
1222             self.report({'ERROR'}, "Data path not set")
1223             return {'CANCELLED'}
1224
1225         self._last_prop = [self.property]
1226
1227         item = eval("context.%s" % data_path)
1228
1229         # retrieve overridable static
1230         exec_str = "item.is_property_overridable_static('[\"%s\"]')" % (self.property)
1231         self.is_overridable_static = bool(eval(exec_str))
1232
1233         # default default value
1234         prop_type = type(self.get_value_eval())
1235         if prop_type in {int, float}:
1236             self.default = str(prop_type(0))
1237         else:
1238             self.default = ""
1239
1240         # setup defaults
1241         prop_ui = rna_idprop_ui_prop_get(item, self.property, False)  # don't create
1242         if prop_ui:
1243             self.min = prop_ui.get("min", -1000000000)
1244             self.max = prop_ui.get("max", 1000000000)
1245             self.description = prop_ui.get("description", "")
1246
1247             defval = prop_ui.get("default", None)
1248             if defval is not None:
1249                 self.default = str(defval)
1250
1251             self.soft_min = prop_ui.get("soft_min", self.min)
1252             self.soft_max = prop_ui.get("soft_max", self.max)
1253             self.use_soft_limits = (
1254                 self.min != self.soft_min or
1255                 self.max != self.soft_max
1256             )
1257
1258         # store for comparison
1259         self._cmp_props = self._cmp_props_get()
1260
1261         wm = context.window_manager
1262         return wm.invoke_props_dialog(self)
1263
1264     def check(self, context):
1265         cmp_props = self._cmp_props_get()
1266         changed = False
1267         if self._cmp_props != cmp_props:
1268             if cmp_props["use_soft_limits"]:
1269                 if cmp_props["soft_range"] != self._cmp_props["soft_range"]:
1270                     self.min = min(self.min, self.soft_min)
1271                     self.max = max(self.max, self.soft_max)
1272                     changed = True
1273                 if cmp_props["hard_range"] != self._cmp_props["hard_range"]:
1274                     self.soft_min = max(self.min, self.soft_min)
1275                     self.soft_max = min(self.max, self.soft_max)
1276                     changed = True
1277             else:
1278                 if cmp_props["soft_range"] != cmp_props["hard_range"]:
1279                     self.soft_min = self.min
1280                     self.soft_max = self.max
1281                     changed = True
1282
1283             changed |= (cmp_props["use_soft_limits"] != self._cmp_props["use_soft_limits"])
1284
1285             if changed:
1286                 cmp_props = self._cmp_props_get()
1287
1288             self._cmp_props = cmp_props
1289
1290         return changed
1291
1292     def draw(self, context):
1293         layout = self.layout
1294         layout.prop(self, "property")
1295         layout.prop(self, "value")
1296
1297         row = layout.row()
1298         row.enabled = type(self.get_value_eval()) in {int, float}
1299         row.prop(self, "default")
1300
1301         row = layout.row(align=True)
1302         row.prop(self, "min")
1303         row.prop(self, "max")
1304
1305         row = layout.row()
1306         row.prop(self, "use_soft_limits")
1307         row.prop(self, "is_overridable_static")
1308
1309         row = layout.row(align=True)
1310         row.enabled = self.use_soft_limits
1311         row.prop(self, "soft_min", text="Soft Min")
1312         row.prop(self, "soft_max", text="Soft Max")
1313         layout.prop(self, "description")
1314
1315
1316 class WM_OT_properties_add(Operator):
1317     bl_idname = "wm.properties_add"
1318     bl_label = "Add Property"
1319     bl_options = {'UNDO', 'INTERNAL'}
1320
1321     data_path: rna_path
1322
1323     def execute(self, context):
1324         from rna_prop_ui import (
1325             rna_idprop_ui_create,
1326         )
1327
1328         data_path = self.data_path
1329         item = eval("context.%s" % data_path)
1330
1331         def unique_name(names):
1332             prop = "prop"
1333             prop_new = prop
1334             i = 1
1335             while prop_new in names:
1336                 prop_new = prop + str(i)
1337                 i += 1
1338
1339             return prop_new
1340
1341         prop = unique_name({
1342             *item.keys(),
1343             *type(item).bl_rna.properties.keys(),
1344         })
1345
1346         rna_idprop_ui_create(item, prop, default=1.0)
1347
1348         return {'FINISHED'}
1349
1350
1351 class WM_OT_properties_context_change(Operator):
1352     """Jump to a different tab inside the properties editor"""
1353     bl_idname = "wm.properties_context_change"
1354     bl_label = ""
1355     bl_options = {'INTERNAL'}
1356
1357     context: StringProperty(
1358         name="Context",
1359         maxlen=64,
1360     )
1361
1362     def execute(self, context):
1363         context.space_data.context = self.context
1364         return {'FINISHED'}
1365
1366
1367 class WM_OT_properties_remove(Operator):
1368     """Internal use (edit a property data_path)"""
1369     bl_idname = "wm.properties_remove"
1370     bl_label = "Remove Property"
1371     bl_options = {'UNDO', 'INTERNAL'}
1372
1373     data_path: rna_path
1374     property: rna_property
1375
1376     def execute(self, context):
1377         from rna_prop_ui import (
1378             rna_idprop_ui_prop_clear,
1379             rna_idprop_ui_prop_update,
1380         )
1381         data_path = self.data_path
1382         item = eval("context.%s" % data_path)
1383         prop = self.property
1384         rna_idprop_ui_prop_update(item, prop)
1385         del item[prop]
1386         rna_idprop_ui_prop_clear(item, prop)
1387
1388         return {'FINISHED'}
1389
1390
1391 class WM_OT_sysinfo(Operator):
1392     """Generate system information, saved into a text file"""
1393
1394     bl_idname = "wm.sysinfo"
1395     bl_label = "Save System Info"
1396
1397     filepath: StringProperty(
1398         subtype='FILE_PATH',
1399         options={'SKIP_SAVE'},
1400     )
1401
1402     def execute(self, context):
1403         import sys_info
1404         sys_info.write_sysinfo(self.filepath)
1405         return {'FINISHED'}
1406
1407     def invoke(self, context, event):
1408         import os
1409
1410         if not self.filepath:
1411             self.filepath = os.path.join(
1412                 os.path.expanduser("~"), "system-info.txt")
1413
1414         wm = context.window_manager
1415         wm.fileselect_add(self)
1416         return {'RUNNING_MODAL'}
1417
1418
1419 class WM_OT_operator_cheat_sheet(Operator):
1420     """List all the Operators in a text-block, useful for scripting"""
1421     bl_idname = "wm.operator_cheat_sheet"
1422     bl_label = "Operator Cheat Sheet"
1423
1424     def execute(self, context):
1425         op_strings = []
1426         tot = 0
1427         for op_module_name in dir(bpy.ops):
1428             op_module = getattr(bpy.ops, op_module_name)
1429             for op_submodule_name in dir(op_module):
1430                 op = getattr(op_module, op_submodule_name)
1431                 text = repr(op)
1432                 if text.split("\n")[-1].startswith("bpy.ops."):
1433                     op_strings.append(text)
1434                     tot += 1
1435
1436             op_strings.append('')
1437
1438         textblock = bpy.data.texts.new("OperatorList.txt")
1439         textblock.write('# %d Operators\n\n' % tot)
1440         textblock.write('\n'.join(op_strings))
1441         self.report({'INFO'}, "See OperatorList.txt textblock")
1442         return {'FINISHED'}
1443
1444
1445 # -----------------------------------------------------------------------------
1446 # Add-on Operators
1447
1448 class WM_OT_owner_enable(Operator):
1449     """Enable workspace owner ID"""
1450     bl_idname = "wm.owner_enable"
1451     bl_label = "Enable Add-on"
1452
1453     owner_id: StringProperty(
1454         name="UI Tag",
1455     )
1456
1457     def execute(self, context):
1458         workspace = context.workspace
1459         workspace.owner_ids.new(self.owner_id)
1460         return {'FINISHED'}
1461
1462
1463 class WM_OT_owner_disable(Operator):
1464     """Enable workspace owner ID"""
1465     bl_idname = "wm.owner_disable"
1466     bl_label = "Disable UI Tag"
1467
1468     owner_id: StringProperty(
1469         name="UI Tag",
1470     )
1471
1472     def execute(self, context):
1473         workspace = context.workspace
1474         owner_id = workspace.owner_ids[self.owner_id]
1475         workspace.owner_ids.remove(owner_id)
1476         return {'FINISHED'}
1477
1478
1479 class WM_OT_tool_set_by_id(Operator):
1480     """Set the tool by name (for keymaps)"""
1481     bl_idname = "wm.tool_set_by_id"
1482     bl_label = "Set Tool By Name"
1483
1484     name: StringProperty(
1485         name="Identifier",
1486         description="Identifier of the tool",
1487     )
1488     cycle: BoolProperty(
1489         name="Cycle",
1490         description="Cycle through tools in this group",
1491         default=False,
1492         options={'SKIP_SAVE'},
1493     )
1494
1495     space_type: rna_space_type_prop
1496
1497     if use_toolbar_release_hack:
1498         def invoke(self, context, event):
1499             # Hack :S
1500             if not self.properties.is_property_set("name"):
1501                 WM_OT_toolbar._key_held = False
1502                 return {'PASS_THROUGH'}
1503             elif (WM_OT_toolbar._key_held == event.type) and (event.value != 'RELEASE'):
1504                 return {'PASS_THROUGH'}
1505             WM_OT_toolbar._key_held = None
1506
1507             return self.execute(context)
1508
1509     def execute(self, context):
1510         from bl_ui.space_toolsystem_common import (
1511             activate_by_id,
1512             activate_by_id_or_cycle,
1513         )
1514
1515         if self.properties.is_property_set("space_type"):
1516             space_type = self.space_type
1517         else:
1518             space_type = context.space_data.type
1519
1520         fn = activate_by_id_or_cycle if self.cycle else activate_by_id
1521         if fn(context, space_type, self.name):
1522             return {'FINISHED'}
1523         else:
1524             self.report({'WARNING'}, f"Tool {self.name!r:s} not found for space {space_type!r:s}.")
1525             return {'CANCELLED'}
1526
1527
1528 class WM_OT_toolbar(Operator):
1529     bl_idname = "wm.toolbar"
1530     bl_label = "Toolbar"
1531
1532     @classmethod
1533     def poll(cls, context):
1534         return context.space_data is not None
1535
1536     if use_toolbar_release_hack:
1537         _key_held = None
1538
1539         def invoke(self, context, event):
1540             WM_OT_toolbar._key_held = event.type
1541             return self.execute(context)
1542
1543     def execute(self, context):
1544         from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
1545         from bl_keymap_utils import keymap_from_toolbar
1546
1547         space_type = context.space_data.type
1548         cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
1549         if cls is None:
1550             return {'CANCELLED'}
1551
1552         wm = context.window_manager
1553         keymap = keymap_from_toolbar.generate(context, space_type)
1554
1555         def draw_menu(popover, context):
1556             layout = popover.layout
1557             layout.operator_context = 'INVOKE_REGION_WIN'
1558             cls.draw_cls(layout, context, detect_layout=False, scale_y=1.0)
1559
1560         wm.popover(draw_menu, ui_units_x=8, keymap=keymap)
1561         return {'FINISHED'}
1562
1563
1564 class WM_MT_splash(Menu):
1565     bl_label = "Splash"
1566
1567     def draw_setup(self, context):
1568         wm = context.window_manager
1569         # prefs = context.preferences
1570
1571         layout = self.layout
1572
1573         layout.operator_context = 'EXEC_DEFAULT'
1574
1575         layout.label(text="Quick Setup")
1576
1577         split = layout.split(factor=0.25)
1578         split.label()
1579         split = split.split(factor=2.0 / 3.0)
1580
1581         col = split.column()
1582
1583         col.label()
1584
1585         sub = col.split(factor=0.35)
1586         row = sub.row()
1587         row.alignment = 'RIGHT'
1588         row.label(text="Shortcuts")
1589         text = bpy.path.display_name(wm.keyconfigs.active.name)
1590         if not text:
1591             text = "Blender"
1592         sub.menu("USERPREF_MT_keyconfigs", text=text)
1593
1594         kc = wm.keyconfigs.active
1595         kc_prefs = kc.preferences
1596         has_select_mouse = hasattr(kc_prefs, "select_mouse")
1597         if has_select_mouse:
1598             sub = col.split(factor=0.35)
1599             row = sub.row()
1600             row.alignment = 'RIGHT'
1601             row.label(text="Select With")
1602             sub.row().prop(kc_prefs, "select_mouse", expand=True)
1603             has_select_mouse = True
1604
1605         has_spacebar_action = hasattr(kc_prefs, "spacebar_action")
1606         if has_spacebar_action:
1607             sub = col.split(factor=0.35)
1608             row = sub.row()
1609             row.alignment = 'RIGHT'
1610             row.label(text="Spacebar")
1611             sub.row().prop(kc_prefs, "spacebar_action", expand=True)
1612             has_select_mouse = True
1613
1614         col.separator()
1615
1616         sub = col.split(factor=0.35)
1617         row = sub.row()
1618         row.alignment = 'RIGHT'
1619         row.label(text="Theme")
1620         label = bpy.types.USERPREF_MT_interface_theme_presets.bl_label
1621         if label == "Presets":
1622             label = "Blender Dark"
1623         sub.menu("USERPREF_MT_interface_theme_presets", text=label)
1624
1625         # We need to make switching to a language easier first
1626         #sub = col.split(factor=0.35)
1627         #row = sub.row()
1628         #row.alignment = 'RIGHT'
1629         # row.label(text="Language:")
1630         #prefs = context.preferences
1631         #sub.prop(prefs.system, "language", text="")
1632
1633         # Keep height constant
1634         if not has_select_mouse:
1635             col.label()
1636         if not has_spacebar_action:
1637             col.label()
1638
1639         layout.label()
1640
1641         row = layout.row()
1642
1643         sub = row.row()
1644         if bpy.types.PREFERENCES_OT_copy_prev.poll(context):
1645             old_version = bpy.types.PREFERENCES_OT_copy_prev.previous_version()
1646             sub.operator("preferences.copy_prev", text="Load %d.%d Settings" % old_version)
1647             sub.operator("wm.save_userpref", text="Save New Settings")
1648         else:
1649             sub.label()
1650             sub.label()
1651             sub.operator("wm.save_userpref", text="Next")
1652
1653         layout.separator()
1654         layout.separator()
1655
1656     def draw(self, context):
1657         # Draw setup screen if no preferences have been saved yet.
1658         import os
1659
1660         userconfig_path = bpy.utils.user_resource('CONFIG')
1661         userdef_path = os.path.join(userconfig_path, "userpref.blend")
1662
1663         if not os.path.isfile(userdef_path):
1664             self.draw_setup(context)
1665             return
1666
1667         # Pass
1668         layout = self.layout
1669         layout.operator_context = 'EXEC_DEFAULT'
1670         layout.emboss = 'PULLDOWN_MENU'
1671
1672         split = layout.split()
1673
1674         # Templates
1675         col1 = split.column()
1676         col1.label(text="New File")
1677
1678         bpy.types.TOPBAR_MT_file_new.draw_ex(col1, context, use_splash=True)
1679
1680         # Recent
1681         col2 = split.column()
1682         col2_title = col2.row()
1683
1684         found_recent = col2.template_recent_files()
1685
1686         if found_recent:
1687             col2_title.label(text="Recent Files")
1688         else:
1689             # Links if no recent files
1690             col2_title.label(text="Getting Started")
1691
1692             col2.operator(
1693                 "wm.url_open", text="Manual", icon='URL'
1694             ).url = "https://docs.blender.org/manual/en/dev/"
1695             col2.operator(
1696                 "wm.url_open", text="Release Notes", icon='URL',
1697             ).url = "https://www.blender.org/download/releases/%d-%d/" % bpy.app.version[:2]
1698             col2.operator(
1699                 "wm.url_open", text="Blender Website", icon='URL',
1700             ).url = "https://www.blender.org"
1701             col2.operator(
1702                 "wm.url_open", text="Credits", icon='URL',
1703             ).url = "https://www.blender.org/about/credits/"
1704
1705         layout.separator()
1706
1707         split = layout.split()
1708
1709         col1 = split.column()
1710         sub = col1.row()
1711         sub.operator_context = 'INVOKE_DEFAULT'
1712         sub.operator("wm.open_mainfile", text="Open...", icon='FILE_FOLDER')
1713         col1.operator("wm.recover_last_session", icon='RECOVER_LAST')
1714
1715         col2 = split.column()
1716         if found_recent:
1717             col2.operator(
1718                 "wm.url_open", text="Release Notes", icon='URL',
1719             ).url = "https://www.blender.org/download/releases/%d-%d/" % bpy.app.version[:2]
1720             col2.operator(
1721                 "wm.url_open", text="Development Fund", icon='URL'
1722             ).url = "https://fund.blender.org"
1723         else:
1724             col2.operator(
1725                 "wm.url_open", text="Development Fund", icon='URL'
1726             ).url = "https://fund.blender.org"
1727             col2.operator(
1728                 "wm.url_open", text="Donate", icon='URL'
1729             ).url = "https://www.blender.org/foundation/donation-payment/"
1730
1731         layout.separator()
1732         layout.separator()
1733
1734
1735 class WM_OT_drop_blend_file(Operator):
1736     bl_idname = "wm.drop_blend_file"
1737     bl_label = "Handle dropped .blend file"
1738     bl_options = {'INTERNAL'}
1739
1740     filepath: StringProperty()
1741
1742     def invoke(self, context, event):
1743         context.window_manager.popup_menu(self.draw_menu, title=bpy.path.basename(self.filepath), icon='QUESTION')
1744         return {'FINISHED'}
1745
1746     def draw_menu(self, menu, context):
1747         layout = menu.layout
1748
1749         col = layout.column()
1750         col.operator_context = 'EXEC_DEFAULT'
1751         col.operator("wm.open_mainfile", text="Open", icon='FILE_FOLDER').filepath = self.filepath
1752
1753         layout.separator()
1754         col = layout.column()
1755         col.operator_context = 'INVOKE_DEFAULT'
1756         col.operator("wm.link", text="Link...", icon='LINK_BLEND').filepath = self.filepath
1757         col.operator("wm.append", text="Append...", icon='APPEND_BLEND').filepath = self.filepath
1758
1759
1760 classes = (
1761     WM_OT_context_collection_boolean_set,
1762     WM_OT_context_cycle_array,
1763     WM_OT_context_cycle_enum,
1764     WM_OT_context_cycle_int,
1765     WM_OT_context_menu_enum,
1766     WM_OT_context_modal_mouse,
1767     WM_OT_context_pie_enum,
1768     WM_OT_context_scale_float,
1769     WM_OT_context_scale_int,
1770     WM_OT_context_set_boolean,
1771     WM_OT_context_set_enum,
1772     WM_OT_context_set_float,
1773     WM_OT_context_set_id,
1774     WM_OT_context_set_int,
1775     WM_OT_context_set_string,
1776     WM_OT_context_set_value,
1777     WM_OT_context_toggle,
1778     WM_OT_context_toggle_enum,
1779     WM_OT_doc_view,
1780     WM_OT_doc_view_manual,
1781     WM_OT_drop_blend_file,
1782     WM_OT_operator_cheat_sheet,
1783     WM_OT_operator_pie_enum,
1784     WM_OT_path_open,
1785     WM_OT_properties_add,
1786     WM_OT_properties_context_change,
1787     WM_OT_properties_edit,
1788     WM_OT_properties_remove,
1789     WM_OT_sysinfo,
1790     WM_OT_owner_disable,
1791     WM_OT_owner_enable,
1792     WM_OT_url_open,
1793     WM_OT_tool_set_by_id,
1794     WM_OT_toolbar,
1795     WM_MT_splash,
1796 )