Python: Remove deprecated uses of os.popen
[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 Operator
23 from bpy.props import (StringProperty,
24                        BoolProperty,
25                        IntProperty,
26                        FloatProperty,
27                        EnumProperty,
28                        )
29
30 from bpy.app.translations import pgettext_tip as tip_
31
32
33 rna_path_prop = StringProperty(
34         name="Context Attributes",
35         description="RNA context string",
36         maxlen=1024,
37         )
38
39 rna_reverse_prop = BoolProperty(
40         name="Reverse",
41         description="Cycle backwards",
42         default=False,
43         )
44
45 rna_relative_prop = BoolProperty(
46         name="Relative",
47         description="Apply relative to the current value (delta)",
48         default=False,
49         )
50
51
52 def context_path_validate(context, data_path):
53     try:
54         value = eval("context.%s" % data_path) if data_path else Ellipsis
55     except AttributeError as e:
56         if str(e).startswith("'NoneType'"):
57             # One of the items in the rna path is None, just ignore this
58             value = Ellipsis
59         else:
60             # We have a real error in the rna path, don't ignore that
61             raise
62
63     return value
64
65
66 def operator_value_is_undo(value):
67     if value in {None, Ellipsis}:
68         return False
69
70     # typical properties or objects
71     id_data = getattr(value, "id_data", Ellipsis)
72
73     if id_data is None:
74         return False
75     elif id_data is Ellipsis:
76         # handle mathutils types
77         id_data = getattr(getattr(value, "owner", None), "id_data", None)
78
79         if id_data is None:
80             return False
81
82     # return True if its a non window ID type
83     return (isinstance(id_data, bpy.types.ID) and
84             (not isinstance(id_data, (bpy.types.WindowManager,
85                                       bpy.types.Screen,
86                                       bpy.types.Brush,
87                                       ))))
88
89
90 def operator_path_is_undo(context, data_path):
91     # note that if we have data paths that use strings this could fail
92     # luckily we don't do this!
93     #
94     # When we cant find the data owner assume no undo is needed.
95     data_path_head = data_path.rpartition(".")[0]
96
97     if not data_path_head:
98         return False
99
100     value = context_path_validate(context, data_path_head)
101
102     return operator_value_is_undo(value)
103
104
105 def operator_path_undo_return(context, data_path):
106     return {'FINISHED'} if operator_path_is_undo(context, data_path) else {'CANCELLED'}
107
108
109 def operator_value_undo_return(value):
110     return {'FINISHED'} if operator_value_is_undo(value) else {'CANCELLED'}
111
112
113 def execute_context_assign(self, context):
114     data_path = self.data_path
115     if context_path_validate(context, data_path) is Ellipsis:
116         return {'PASS_THROUGH'}
117
118     if getattr(self, "relative", False):
119         exec("context.%s += self.value" % data_path)
120     else:
121         exec("context.%s = self.value" % data_path)
122
123     return operator_path_undo_return(context, data_path)
124
125
126 class BRUSH_OT_active_index_set(Operator):
127     """Set active sculpt/paint brush from it's number"""
128     bl_idname = "brush.active_index_set"
129     bl_label = "Set Brush Number"
130
131     mode = StringProperty(
132             name="Mode",
133             description="Paint mode to set brush for",
134             maxlen=1024,
135             )
136     index = IntProperty(
137             name="Number",
138             description="Brush number",
139             )
140
141     _attr_dict = {"sculpt": "use_paint_sculpt",
142                   "vertex_paint": "use_paint_vertex",
143                   "weight_paint": "use_paint_weight",
144                   "image_paint": "use_paint_image",
145                   }
146
147     def execute(self, context):
148         attr = self._attr_dict.get(self.mode)
149         if attr is None:
150             return {'CANCELLED'}
151
152         toolsettings = context.tool_settings
153         for i, brush in enumerate((cur for cur in bpy.data.brushes if getattr(cur, attr))):
154             if i == self.index:
155                 getattr(toolsettings, self.mode).brush = brush
156                 return {'FINISHED'}
157
158         return {'CANCELLED'}
159
160
161 class WM_OT_context_set_boolean(Operator):
162     """Set a context value"""
163     bl_idname = "wm.context_set_boolean"
164     bl_label = "Context Set Boolean"
165     bl_options = {'UNDO', 'INTERNAL'}
166
167     data_path = rna_path_prop
168     value = BoolProperty(
169             name="Value",
170             description="Assignment value",
171             default=True,
172             )
173
174     execute = execute_context_assign
175
176
177 class WM_OT_context_set_int(Operator):  # same as enum
178     """Set a context value"""
179     bl_idname = "wm.context_set_int"
180     bl_label = "Context Set"
181     bl_options = {'UNDO', 'INTERNAL'}
182
183     data_path = rna_path_prop
184     value = IntProperty(
185             name="Value",
186             description="Assign value",
187             default=0,
188             )
189     relative = rna_relative_prop
190
191     execute = execute_context_assign
192
193
194 class WM_OT_context_scale_float(Operator):
195     """Scale a float context value"""
196     bl_idname = "wm.context_scale_float"
197     bl_label = "Context Scale Float"
198     bl_options = {'UNDO', 'INTERNAL'}
199
200     data_path = rna_path_prop
201     value = FloatProperty(
202             name="Value",
203             description="Assign value",
204             default=1.0,
205             )
206
207     def execute(self, context):
208         data_path = self.data_path
209         if context_path_validate(context, data_path) is Ellipsis:
210             return {'PASS_THROUGH'}
211
212         value = self.value
213
214         if value == 1.0:  # nothing to do
215             return {'CANCELLED'}
216
217         exec("context.%s *= value" % data_path)
218
219         return operator_path_undo_return(context, data_path)
220
221
222 class WM_OT_context_scale_int(Operator):
223     """Scale an int context value"""
224     bl_idname = "wm.context_scale_int"
225     bl_label = "Context Scale Int"
226     bl_options = {'UNDO', 'INTERNAL'}
227
228     data_path = rna_path_prop
229     value = FloatProperty(
230             name="Value",
231             description="Assign value",
232             default=1.0,
233             )
234     always_step = BoolProperty(
235             name="Always Step",
236             description="Always adjust the value by a minimum of 1 when 'value' is not 1.0",
237             default=True,
238             )
239
240     def execute(self, context):
241         data_path = self.data_path
242         if context_path_validate(context, data_path) is Ellipsis:
243             return {'PASS_THROUGH'}
244
245         value = self.value
246
247         if value == 1.0:  # nothing to do
248             return {'CANCELLED'}
249
250         if getattr(self, "always_step", False):
251             if value > 1.0:
252                 add = "1"
253                 func = "max"
254             else:
255                 add = "-1"
256                 func = "min"
257             exec("context.%s = %s(round(context.%s * value), context.%s + %s)" %
258                  (data_path, func, data_path, data_path, add))
259         else:
260             exec("context.%s *= value" % data_path)
261
262         return operator_path_undo_return(context, data_path)
263
264
265 class WM_OT_context_set_float(Operator):  # same as enum
266     """Set a context value"""
267     bl_idname = "wm.context_set_float"
268     bl_label = "Context Set Float"
269     bl_options = {'UNDO', 'INTERNAL'}
270
271     data_path = rna_path_prop
272     value = FloatProperty(
273             name="Value",
274             description="Assignment value",
275             default=0.0,
276             )
277     relative = rna_relative_prop
278
279     execute = execute_context_assign
280
281
282 class WM_OT_context_set_string(Operator):  # same as enum
283     """Set a context value"""
284     bl_idname = "wm.context_set_string"
285     bl_label = "Context Set String"
286     bl_options = {'UNDO', 'INTERNAL'}
287
288     data_path = rna_path_prop
289     value = StringProperty(
290             name="Value",
291             description="Assign value",
292             maxlen=1024,
293             )
294
295     execute = execute_context_assign
296
297
298 class WM_OT_context_set_enum(Operator):
299     """Set a context value"""
300     bl_idname = "wm.context_set_enum"
301     bl_label = "Context Set Enum"
302     bl_options = {'UNDO', 'INTERNAL'}
303
304     data_path = rna_path_prop
305     value = StringProperty(
306             name="Value",
307             description="Assignment value (as a string)",
308             maxlen=1024,
309             )
310
311     execute = execute_context_assign
312
313
314 class WM_OT_context_set_value(Operator):
315     """Set a context value"""
316     bl_idname = "wm.context_set_value"
317     bl_label = "Context Set Value"
318     bl_options = {'UNDO', 'INTERNAL'}
319
320     data_path = rna_path_prop
321     value = StringProperty(
322             name="Value",
323             description="Assignment value (as a string)",
324             maxlen=1024,
325             )
326
327     def execute(self, context):
328         data_path = self.data_path
329         if context_path_validate(context, data_path) is Ellipsis:
330             return {'PASS_THROUGH'}
331         exec("context.%s = %s" % (data_path, self.value))
332         return operator_path_undo_return(context, data_path)
333
334
335 class WM_OT_context_toggle(Operator):
336     """Toggle a context value"""
337     bl_idname = "wm.context_toggle"
338     bl_label = "Context Toggle"
339     bl_options = {'UNDO', 'INTERNAL'}
340
341     data_path = rna_path_prop
342
343     def execute(self, context):
344         data_path = self.data_path
345
346         if context_path_validate(context, data_path) is Ellipsis:
347             return {'PASS_THROUGH'}
348
349         exec("context.%s = not (context.%s)" % (data_path, data_path))
350
351         return operator_path_undo_return(context, data_path)
352
353
354 class WM_OT_context_toggle_enum(Operator):
355     """Toggle a context value"""
356     bl_idname = "wm.context_toggle_enum"
357     bl_label = "Context Toggle Values"
358     bl_options = {'UNDO', 'INTERNAL'}
359
360     data_path = rna_path_prop
361     value_1 = StringProperty(
362             name="Value",
363             description="Toggle enum",
364             maxlen=1024,
365             )
366     value_2 = StringProperty(
367             name="Value",
368             description="Toggle enum",
369             maxlen=1024,
370             )
371
372     def execute(self, context):
373         data_path = self.data_path
374
375         if context_path_validate(context, data_path) is Ellipsis:
376             return {'PASS_THROUGH'}
377
378         # failing silently is not ideal, but we don't want errors for shortcut
379         # keys that some values that are only available in a particular context
380         try:
381             exec("context.%s = ('%s', '%s')[context.%s != '%s']" %
382                  (data_path, self.value_1,
383                   self.value_2, data_path,
384                   self.value_2,
385                   ))
386         except:
387             return {'PASS_THROUGH'}
388
389         return operator_path_undo_return(context, data_path)
390
391
392 class WM_OT_context_cycle_int(Operator):
393     """Set a context value (useful for cycling active material, """ \
394     """vertex keys, groups, etc.)"""
395     bl_idname = "wm.context_cycle_int"
396     bl_label = "Context Int Cycle"
397     bl_options = {'UNDO', 'INTERNAL'}
398
399     data_path = rna_path_prop
400     reverse = rna_reverse_prop
401
402     def execute(self, context):
403         data_path = self.data_path
404         value = context_path_validate(context, data_path)
405         if value is Ellipsis:
406             return {'PASS_THROUGH'}
407
408         if self.reverse:
409             value -= 1
410         else:
411             value += 1
412
413         exec("context.%s = value" % data_path)
414
415         if value != eval("context.%s" % data_path):
416             # relies on rna clamping integers out of the range
417             if self.reverse:
418                 value = (1 << 31) - 1
419             else:
420                 value = -1 << 31
421
422             exec("context.%s = value" % data_path)
423
424         return operator_path_undo_return(context, data_path)
425
426
427 class WM_OT_context_cycle_enum(Operator):
428     """Toggle a context value"""
429     bl_idname = "wm.context_cycle_enum"
430     bl_label = "Context Enum Cycle"
431     bl_options = {'UNDO', 'INTERNAL'}
432
433     data_path = rna_path_prop
434     reverse = rna_reverse_prop
435
436     def execute(self, context):
437         data_path = self.data_path
438         value = context_path_validate(context, data_path)
439         if value is Ellipsis:
440             return {'PASS_THROUGH'}
441
442         orig_value = value
443
444         # Have to get rna enum values
445         rna_struct_str, rna_prop_str = data_path.rsplit('.', 1)
446         i = rna_prop_str.find('[')
447
448         # just in case we get "context.foo.bar[0]"
449         if i != -1:
450             rna_prop_str = rna_prop_str[0:i]
451
452         rna_struct = eval("context.%s.rna_type" % rna_struct_str)
453
454         rna_prop = rna_struct.properties[rna_prop_str]
455
456         if type(rna_prop) != bpy.types.EnumProperty:
457             raise Exception("expected an enum property")
458
459         enums = rna_struct.properties[rna_prop_str].enum_items.keys()
460         orig_index = enums.index(orig_value)
461
462         # Have the info we need, advance to the next item
463         if self.reverse:
464             if orig_index == 0:
465                 advance_enum = enums[-1]
466             else:
467                 advance_enum = enums[orig_index - 1]
468         else:
469             if orig_index == len(enums) - 1:
470                 advance_enum = enums[0]
471             else:
472                 advance_enum = enums[orig_index + 1]
473
474         # set the new value
475         exec("context.%s = advance_enum" % data_path)
476         return operator_path_undo_return(context, data_path)
477
478
479 class WM_OT_context_cycle_array(Operator):
480     """Set a context array value """ \
481     """(useful for cycling the active mesh edit mode)"""
482     bl_idname = "wm.context_cycle_array"
483     bl_label = "Context Array Cycle"
484     bl_options = {'UNDO', 'INTERNAL'}
485
486     data_path = rna_path_prop
487     reverse = rna_reverse_prop
488
489     def execute(self, context):
490         data_path = self.data_path
491         value = context_path_validate(context, data_path)
492         if value is Ellipsis:
493             return {'PASS_THROUGH'}
494
495         def cycle(array):
496             if self.reverse:
497                 array.insert(0, array.pop())
498             else:
499                 array.append(array.pop(0))
500             return array
501
502         exec("context.%s = cycle(context.%s[:])" % (data_path, data_path))
503
504         return operator_path_undo_return(context, data_path)
505
506
507 class WM_OT_context_menu_enum(Operator):
508     bl_idname = "wm.context_menu_enum"
509     bl_label = "Context Enum Menu"
510     bl_options = {'UNDO', 'INTERNAL'}
511     data_path = rna_path_prop
512
513     def execute(self, context):
514         data_path = self.data_path
515         value = context_path_validate(context, data_path)
516
517         if value is Ellipsis:
518             return {'PASS_THROUGH'}
519
520         base_path, prop_string = data_path.rsplit(".", 1)
521         value_base = context_path_validate(context, base_path)
522         prop = value_base.bl_rna.properties[prop_string]
523
524         def draw_cb(self, context):
525             layout = self.layout
526             layout.prop(value_base, prop_string, expand=True)
527
528         context.window_manager.popup_menu(draw_func=draw_cb, title=prop.name, icon=prop.icon)
529
530         return {'PASS_THROUGH'}
531
532
533 class WM_OT_context_set_id(Operator):
534     """Set a context value to an ID data-block"""
535     bl_idname = "wm.context_set_id"
536     bl_label = "Set Library ID"
537     bl_options = {'UNDO', 'INTERNAL'}
538
539     data_path = rna_path_prop
540     value = StringProperty(
541             name="Value",
542             description="Assign value",
543             maxlen=1024,
544             )
545
546     def execute(self, context):
547         value = self.value
548         data_path = self.data_path
549
550         # match the pointer type from the target property to bpy.data.*
551         # so we lookup the correct list.
552         data_path_base, data_path_prop = data_path.rsplit(".", 1)
553         data_prop_rna = eval("context.%s" % data_path_base).rna_type.properties[data_path_prop]
554         data_prop_rna_type = data_prop_rna.fixed_type
555
556         id_iter = None
557
558         for prop in bpy.data.rna_type.properties:
559             if prop.rna_type.identifier == "CollectionProperty":
560                 if prop.fixed_type == data_prop_rna_type:
561                     id_iter = prop.identifier
562                     break
563
564         if id_iter:
565             value_id = getattr(bpy.data, id_iter).get(value)
566             exec("context.%s = value_id" % data_path)
567
568         return operator_path_undo_return(context, data_path)
569
570
571 doc_id = StringProperty(
572         name="Doc ID",
573         maxlen=1024,
574         options={'HIDDEN'},
575         )
576
577 doc_new = StringProperty(
578         name="Edit Description",
579         maxlen=1024,
580         )
581
582 data_path_iter = StringProperty(
583         description="The data path relative to the context, must point to an iterable")
584
585 data_path_item = StringProperty(
586         description="The data path from each iterable to the value (int or float)")
587
588
589 class WM_OT_context_collection_boolean_set(Operator):
590     """Set boolean values for a collection of items"""
591     bl_idname = "wm.context_collection_boolean_set"
592     bl_label = "Context Collection Boolean Set"
593     bl_options = {'UNDO', 'REGISTER', 'INTERNAL'}
594
595     data_path_iter = data_path_iter
596     data_path_item = data_path_item
597
598     type = EnumProperty(
599             name="Type",
600             items=(('TOGGLE', "Toggle", ""),
601                    ('ENABLE', "Enable", ""),
602                    ('DISABLE', "Disable", ""),
603                    ),
604             )
605
606     def execute(self, context):
607         data_path_iter = self.data_path_iter
608         data_path_item = self.data_path_item
609
610         items = list(getattr(context, data_path_iter))
611         items_ok = []
612         is_set = False
613         for item in items:
614             try:
615                 value_orig = eval("item." + data_path_item)
616             except:
617                 continue
618
619             if value_orig is True:
620                 is_set = True
621             elif value_orig is False:
622                 pass
623             else:
624                 self.report({'WARNING'}, "Non boolean value found: %s[ ].%s" %
625                             (data_path_iter, data_path_item))
626                 return {'CANCELLED'}
627
628             items_ok.append(item)
629
630         # avoid undo push when nothing to do
631         if not items_ok:
632             return {'CANCELLED'}
633
634         if self.type == 'ENABLE':
635             is_set = True
636         elif self.type == 'DISABLE':
637             is_set = False
638         else:
639             is_set = not is_set
640
641         exec_str = "item.%s = %s" % (data_path_item, is_set)
642         for item in items_ok:
643             exec(exec_str)
644
645         return operator_value_undo_return(item)
646
647
648 class WM_OT_context_modal_mouse(Operator):
649     """Adjust arbitrary values with mouse input"""
650     bl_idname = "wm.context_modal_mouse"
651     bl_label = "Context Modal Mouse"
652     bl_options = {'GRAB_POINTER', 'BLOCKING', 'UNDO', 'INTERNAL'}
653
654     data_path_iter = data_path_iter
655     data_path_item = data_path_item
656     header_text = StringProperty(
657             name="Header Text",
658             description="Text to display in header during scale",
659             )
660
661     input_scale = FloatProperty(
662             description="Scale the mouse movement by this value before applying the delta",
663             default=0.01,
664             )
665     invert = BoolProperty(
666             description="Invert the mouse input",
667             default=False,
668             )
669     initial_x = IntProperty(options={'HIDDEN'})
670
671     def _values_store(self, context):
672         data_path_iter = self.data_path_iter
673         data_path_item = self.data_path_item
674
675         self._values = values = {}
676
677         for item in getattr(context, data_path_iter):
678             try:
679                 value_orig = eval("item." + data_path_item)
680             except:
681                 continue
682
683             # check this can be set, maybe this is library data.
684             try:
685                 exec("item.%s = %s" % (data_path_item, value_orig))
686             except:
687                 continue
688
689             values[item] = value_orig
690
691     def _values_delta(self, delta):
692         delta *= self.input_scale
693         if self.invert:
694             delta = - delta
695
696         data_path_item = self.data_path_item
697         for item, value_orig in self._values.items():
698             if type(value_orig) == int:
699                 exec("item.%s = int(%d)" % (data_path_item, round(value_orig + delta)))
700             else:
701                 exec("item.%s = %f" % (data_path_item, value_orig + delta))
702
703     def _values_restore(self):
704         data_path_item = self.data_path_item
705         for item, value_orig in self._values.items():
706             exec("item.%s = %s" % (data_path_item, value_orig))
707
708         self._values.clear()
709
710     def _values_clear(self):
711         self._values.clear()
712
713     def modal(self, context, event):
714         event_type = event.type
715
716         if event_type == 'MOUSEMOVE':
717             delta = event.mouse_x - self.initial_x
718             self._values_delta(delta)
719             header_text = self.header_text
720             if header_text:
721                 if len(self._values) == 1:
722                     (item, ) = self._values.keys()
723                     header_text = header_text % eval("item.%s" % self.data_path_item)
724                 else:
725                     header_text = (self.header_text % delta) + " (delta)"
726                 context.area.header_text_set(header_text)
727
728         elif 'LEFTMOUSE' == event_type:
729             item = next(iter(self._values.keys()))
730             self._values_clear()
731             context.area.header_text_set()
732             return operator_value_undo_return(item)
733
734         elif event_type in {'RIGHTMOUSE', 'ESC'}:
735             self._values_restore()
736             context.area.header_text_set()
737             return {'CANCELLED'}
738
739         return {'RUNNING_MODAL'}
740
741     def invoke(self, context, event):
742         self._values_store(context)
743
744         if not self._values:
745             self.report({'WARNING'}, "Nothing to operate on: %s[ ].%s" %
746                         (self.data_path_iter, self.data_path_item))
747
748             return {'CANCELLED'}
749         else:
750             self.initial_x = event.mouse_x
751
752             context.window_manager.modal_handler_add(self)
753             return {'RUNNING_MODAL'}
754
755
756 class WM_OT_url_open(Operator):
757     "Open a website in the web-browser"
758     bl_idname = "wm.url_open"
759     bl_label = ""
760     bl_options = {'INTERNAL'}
761
762     url = StringProperty(
763             name="URL",
764             description="URL to open",
765             )
766
767     def execute(self, context):
768         import webbrowser
769         webbrowser.open(self.url)
770         return {'FINISHED'}
771
772
773 class WM_OT_path_open(Operator):
774     "Open a path in a file browser"
775     bl_idname = "wm.path_open"
776     bl_label = ""
777     bl_options = {'INTERNAL'}
778
779     filepath = StringProperty(
780             subtype='FILE_PATH',
781             options={'SKIP_SAVE'},
782             )
783
784     def execute(self, context):
785         import sys
786         import os
787         import subprocess
788
789         filepath = self.filepath
790
791         if not filepath:
792             self.report({'ERROR'}, "File path was not set")
793             return {'CANCELLED'}
794
795         filepath = bpy.path.abspath(filepath)
796         filepath = os.path.normpath(filepath)
797
798         if not os.path.exists(filepath):
799             self.report({'ERROR'}, "File '%s' not found" % filepath)
800             return {'CANCELLED'}
801
802         if sys.platform[:3] == "win":
803             os.startfile(filepath)
804         elif sys.platform == "darwin":
805             subprocess.check_call(["open", filepath])
806         else:
807             try:
808                 subprocess.check_call(["xdg-open", filepath])
809             except:
810                 # xdg-open *should* be supported by recent Gnome, KDE, Xfce
811                 import traceback
812                 traceback.print_exc()
813
814         return {'FINISHED'}
815
816
817 def _wm_doc_get_id(doc_id, do_url=True, url_prefix=""):
818     id_split = doc_id.split(".")
819     url = rna = None
820
821     if len(id_split) == 1:  # rna, class
822         if do_url:
823             url = "%s/bpy.types.%s.html" % (url_prefix, id_split[0])
824         else:
825             rna = "bpy.types.%s" % id_split[0]
826
827     elif len(id_split) == 2:  # rna, class.prop
828         class_name, class_prop = id_split
829
830         # an operator (common case - just button referencing an op)
831         if hasattr(bpy.types, class_name.upper() + "_OT_" + class_prop):
832             if do_url:
833                 url = ("%s/bpy.ops.%s.html#bpy.ops.%s.%s" % (url_prefix, class_name, class_name, class_prop))
834             else:
835                 rna = "bpy.ops.%s.%s" % (class_name, class_prop)
836         else:
837             rna_class = getattr(bpy.types, class_name)
838
839             # an operator setting (selected from a running operator), rare case
840             # note: Py defined operators are subclass of Operator,
841             #       C defined operators are subclass of OperatorProperties.
842             #       we may need to check on this at some point.
843             if issubclass(rna_class, (bpy.types.Operator, bpy.types.OperatorProperties)):
844                 # note: ignore the prop name since we don't have a way to link into it
845                 class_name, class_prop = class_name.split("_OT_", 1)
846                 class_name = class_name.lower()
847                 if do_url:
848                     url = ("%s/bpy.ops.%s.html#bpy.ops.%s.%s" % (url_prefix, class_name, class_name, class_prop))
849                 else:
850                     rna = "bpy.ops.%s.%s" % (class_name, class_prop)
851             else:
852                 # an RNA setting, common case
853
854                 # detect if this is a inherited member and use that name instead
855                 rna_parent = rna_class.bl_rna
856                 rna_prop = rna_parent.properties[class_prop]
857                 rna_parent = rna_parent.base
858                 while rna_parent and rna_prop == rna_parent.properties.get(class_prop):
859                     class_name = rna_parent.identifier
860                     rna_parent = rna_parent.base
861
862                 if do_url:
863                     url = ("%s/bpy.types.%s.html#bpy.types.%s.%s" % (url_prefix, class_name, class_name, class_prop))
864                 else:
865                     rna = ("bpy.types.%s.%s" % (class_name, class_prop))
866
867     return url if do_url else rna
868
869
870 class WM_OT_doc_view_manual(Operator):
871     """Load online manual"""
872     bl_idname = "wm.doc_view_manual"
873     bl_label = "View Manual"
874
875     doc_id = doc_id
876
877     @staticmethod
878     def _find_reference(rna_id, url_mapping, verbose=True):
879         if verbose:
880             print("online manual check for: '%s'... " % rna_id)
881         from fnmatch import fnmatch
882         for pattern, url_suffix in url_mapping:
883             if fnmatch(rna_id, pattern):
884                 if verbose:
885                     print("            match found: '%s' --> '%s'" % (pattern, url_suffix))
886                 return url_suffix
887         if verbose:
888             print("match not found")
889         return None
890
891     @staticmethod
892     def _lookup_rna_url(rna_id, verbose=True):
893         for prefix, url_manual_mapping in bpy.utils.manual_map():
894             rna_ref = WM_OT_doc_view_manual._find_reference(rna_id, url_manual_mapping, verbose=verbose)
895             if rna_ref is not None:
896                 url = prefix + rna_ref
897                 return url
898
899     def execute(self, context):
900         rna_id = _wm_doc_get_id(self.doc_id, do_url=False)
901         if rna_id is None:
902             return {'PASS_THROUGH'}
903
904         url = self._lookup_rna_url(rna_id)
905
906         if url is None:
907             self.report({'WARNING'}, "No reference available %r, "
908                                      "Update info in 'rna_wiki_reference.py' "
909                                      " or callback to bpy.utils.manual_map()" %
910                                      self.doc_id)
911             return {'CANCELLED'}
912         else:
913             import webbrowser
914             webbrowser.open(url)
915             return {'FINISHED'}
916
917
918 class WM_OT_doc_view(Operator):
919     """Load online reference docs"""
920     bl_idname = "wm.doc_view"
921     bl_label = "View Documentation"
922
923     doc_id = doc_id
924     if bpy.app.version_cycle == "release":
925         _prefix = ("http://www.blender.org/documentation/blender_python_api_%s%s_release" %
926                    ("_".join(str(v) for v in bpy.app.version[:2]), bpy.app.version_char))
927     else:
928         _prefix = ("http://www.blender.org/documentation/blender_python_api_%s" %
929                    "_".join(str(v) for v in bpy.app.version))
930
931     def execute(self, context):
932         url = _wm_doc_get_id(self.doc_id, do_url=True, url_prefix=self._prefix)
933         if url is None:
934             return {'PASS_THROUGH'}
935
936         import webbrowser
937         webbrowser.open(url)
938
939         return {'FINISHED'}
940
941
942 '''
943 class WM_OT_doc_edit(Operator):
944     """Edit online reference docs"""
945     bl_idname = "wm.doc_edit"
946     bl_label = "Edit Documentation"
947
948     doc_id = doc_id
949     doc_new = doc_new
950
951     _url = "http://www.mindrones.com/blender/svn/xmlrpc.php"
952
953     def _send_xmlrpc(self, data_dict):
954         print("sending data:", data_dict)
955
956         import xmlrpc.client
957         user = "blenderuser"
958         pwd = "blender>user"
959
960         docblog = xmlrpc.client.ServerProxy(self._url)
961         docblog.metaWeblog.newPost(1, user, pwd, data_dict, 1)
962
963     def execute(self, context):
964
965         doc_id = self.doc_id
966         doc_new = self.doc_new
967
968         class_name, class_prop = doc_id.split('.')
969
970         if not doc_new:
971             self.report({'ERROR'}, "No input given for '%s'" % doc_id)
972             return {'CANCELLED'}
973
974         # check if this is an operator
975         op_name = class_name.upper() + '_OT_' + class_prop
976         op_class = getattr(bpy.types, op_name, None)
977
978         # Upload this to the web server
979         upload = {}
980
981         if op_class:
982             rna = op_class.bl_rna
983             doc_orig = rna.description
984             if doc_orig == doc_new:
985                 return {'RUNNING_MODAL'}
986
987             print("op - old:'%s' -> new:'%s'" % (doc_orig, doc_new))
988             upload["title"] = 'OPERATOR %s:%s' % (doc_id, doc_orig)
989         else:
990             rna = getattr(bpy.types, class_name).bl_rna
991             doc_orig = rna.properties[class_prop].description
992             if doc_orig == doc_new:
993                 return {'RUNNING_MODAL'}
994
995             print("rna - old:'%s' -> new:'%s'" % (doc_orig, doc_new))
996             upload["title"] = 'RNA %s:%s' % (doc_id, doc_orig)
997
998         upload["description"] = doc_new
999
1000         self._send_xmlrpc(upload)
1001
1002         return {'FINISHED'}
1003
1004     def draw(self, context):
1005         layout = self.layout
1006         layout.label(text="Descriptor ID: '%s'" % self.doc_id)
1007         layout.prop(self, "doc_new", text="")
1008
1009     def invoke(self, context, event):
1010         wm = context.window_manager
1011         return wm.invoke_props_dialog(self, width=600)
1012 '''
1013
1014
1015 rna_path = StringProperty(
1016         name="Property Edit",
1017         description="Property data_path edit",
1018         maxlen=1024,
1019         options={'HIDDEN'},
1020         )
1021
1022 rna_value = StringProperty(
1023         name="Property Value",
1024         description="Property value edit",
1025         maxlen=1024,
1026         )
1027
1028 rna_property = StringProperty(
1029         name="Property Name",
1030         description="Property name edit",
1031         maxlen=1024,
1032         )
1033
1034 rna_min = FloatProperty(
1035         name="Min",
1036         default=0.0,
1037         precision=3,
1038         )
1039
1040 rna_max = FloatProperty(
1041         name="Max",
1042         default=1.0,
1043         precision=3,
1044         )
1045
1046
1047 class WM_OT_properties_edit(Operator):
1048     bl_idname = "wm.properties_edit"
1049     bl_label = "Edit Property"
1050     # register only because invoke_props_popup requires.
1051     bl_options = {'REGISTER', 'INTERNAL'}
1052
1053     data_path = rna_path
1054     property = rna_property
1055     value = rna_value
1056     min = rna_min
1057     max = rna_max
1058     description = StringProperty(
1059             name="Tooltip",
1060             )
1061
1062     def execute(self, context):
1063         from rna_prop_ui import rna_idprop_ui_prop_get, rna_idprop_ui_prop_clear
1064
1065         data_path = self.data_path
1066         value = self.value
1067         prop = self.property
1068
1069         prop_old = getattr(self, "_last_prop", [None])[0]
1070
1071         if prop_old is None:
1072             self.report({'ERROR'}, "Direct execution not supported")
1073             return {'CANCELLED'}
1074
1075         try:
1076             value_eval = eval(value)
1077             # assert else None -> None, not "None", see [#33431]
1078             assert(type(value_eval) in {str, float, int, bool, tuple, list})
1079         except:
1080             value_eval = value
1081
1082         # First remove
1083         item = eval("context.%s" % data_path)
1084         prop_type_old = type(item[prop_old])
1085
1086         rna_idprop_ui_prop_clear(item, prop_old)
1087         exec_str = "del item[%r]" % prop_old
1088         # print(exec_str)
1089         exec(exec_str)
1090
1091         # Reassign
1092         exec_str = "item[%r] = %s" % (prop, repr(value_eval))
1093         # print(exec_str)
1094         exec(exec_str)
1095         self._last_prop[:] = [prop]
1096
1097         prop_type = type(item[prop])
1098
1099         prop_ui = rna_idprop_ui_prop_get(item, prop)
1100
1101         if prop_type in {float, int}:
1102             prop_ui["soft_min"] = prop_ui["min"] = prop_type(self.min)
1103             prop_ui["soft_max"] = prop_ui["max"] = prop_type(self.max)
1104
1105         prop_ui["description"] = self.description
1106
1107         # If we have changed the type of the property, update its potential anim curves!
1108         if prop_type_old != prop_type:
1109             data_path = '["%s"]' % bpy.utils.escape_identifier(prop)
1110             done = set()
1111
1112             def _update(fcurves):
1113                 for fcu in fcurves:
1114                     if fcu not in done and fcu.data_path == data_path:
1115                         fcu.update_autoflags(item)
1116                         done.add(fcu)
1117
1118             def _update_strips(strips):
1119                 for st in strips:
1120                     if st.type == 'CLIP' and st.action:
1121                         _update(st.action.fcurves)
1122                     elif st.type == 'META':
1123                         _update_strips(st.strips)
1124
1125             adt = getattr(item, "animation_data", None)
1126             if adt is not None:
1127                 if adt.action:
1128                     _update(adt.action.fcurves)
1129                 if adt.drivers:
1130                     _update(adt.drivers)
1131                 if adt.nla_tracks:
1132                     for nt in adt.nla_tracks:
1133                         _update_strips(nt.strips)
1134
1135         # otherwise existing buttons which reference freed
1136         # memory may crash blender [#26510]
1137         # context.area.tag_redraw()
1138         for win in context.window_manager.windows:
1139             for area in win.screen.areas:
1140                 area.tag_redraw()
1141
1142         return {'FINISHED'}
1143
1144     def invoke(self, context, event):
1145         from rna_prop_ui import rna_idprop_ui_prop_get
1146
1147         data_path = self.data_path
1148
1149         if not data_path:
1150             self.report({'ERROR'}, "Data path not set")
1151             return {'CANCELLED'}
1152
1153         self._last_prop = [self.property]
1154
1155         item = eval("context.%s" % data_path)
1156
1157         # setup defaults
1158         prop_ui = rna_idprop_ui_prop_get(item, self.property, False)  # don't create
1159         if prop_ui:
1160             self.min = prop_ui.get("min", -1000000000)
1161             self.max = prop_ui.get("max", 1000000000)
1162             self.description = prop_ui.get("description", "")
1163
1164         wm = context.window_manager
1165         return wm.invoke_props_dialog(self)
1166
1167
1168 class WM_OT_properties_add(Operator):
1169     bl_idname = "wm.properties_add"
1170     bl_label = "Add Property"
1171     bl_options = {'UNDO', 'INTERNAL'}
1172
1173     data_path = rna_path
1174
1175     def execute(self, context):
1176         from rna_prop_ui import rna_idprop_ui_prop_get
1177
1178         data_path = self.data_path
1179         item = eval("context.%s" % data_path)
1180
1181         def unique_name(names):
1182             prop = "prop"
1183             prop_new = prop
1184             i = 1
1185             while prop_new in names:
1186                 prop_new = prop + str(i)
1187                 i += 1
1188
1189             return prop_new
1190
1191         prop = unique_name(item.keys())
1192
1193         item[prop] = 1.0
1194
1195         # not essential, but without this we get [#31661]
1196         prop_ui = rna_idprop_ui_prop_get(item, prop)
1197         prop_ui["soft_min"] = prop_ui["min"] = 0.0
1198         prop_ui["soft_max"] = prop_ui["max"] = 1.0
1199
1200         return {'FINISHED'}
1201
1202
1203 class WM_OT_properties_context_change(Operator):
1204     "Jump to a different tab inside the properties editor"
1205     bl_idname = "wm.properties_context_change"
1206     bl_label = ""
1207     bl_options = {'INTERNAL'}
1208
1209     context = StringProperty(
1210             name="Context",
1211             maxlen=64,
1212             )
1213
1214     def execute(self, context):
1215         context.space_data.context = self.context
1216         return {'FINISHED'}
1217
1218
1219 class WM_OT_properties_remove(Operator):
1220     """Internal use (edit a property data_path)"""
1221     bl_idname = "wm.properties_remove"
1222     bl_label = "Remove Property"
1223     bl_options = {'UNDO', 'INTERNAL'}
1224
1225     data_path = rna_path
1226     property = rna_property
1227
1228     def execute(self, context):
1229         data_path = self.data_path
1230         item = eval("context.%s" % data_path)
1231         del item[self.property]
1232         return {'FINISHED'}
1233
1234
1235 class WM_OT_keyconfig_activate(Operator):
1236     bl_idname = "wm.keyconfig_activate"
1237     bl_label = "Activate Keyconfig"
1238
1239     filepath = StringProperty(
1240             subtype='FILE_PATH',
1241             )
1242
1243     def execute(self, context):
1244         if bpy.utils.keyconfig_set(self.filepath, report=self.report):
1245             return {'FINISHED'}
1246         else:
1247             return {'CANCELLED'}
1248
1249
1250 class WM_OT_appconfig_default(Operator):
1251     bl_idname = "wm.appconfig_default"
1252     bl_label = "Default Application Configuration"
1253
1254     def execute(self, context):
1255         import os
1256
1257         context.window_manager.keyconfigs.active = context.window_manager.keyconfigs.default
1258
1259         filepath = os.path.join(bpy.utils.preset_paths("interaction")[0], "blender.py")
1260
1261         if os.path.exists(filepath):
1262             bpy.ops.script.execute_preset(filepath=filepath, menu_idname="USERPREF_MT_interaction_presets")
1263
1264         return {'FINISHED'}
1265
1266
1267 class WM_OT_appconfig_activate(Operator):
1268     bl_idname = "wm.appconfig_activate"
1269     bl_label = "Activate Application Configuration"
1270
1271     filepath = StringProperty(
1272             subtype='FILE_PATH',
1273             )
1274
1275     def execute(self, context):
1276         import os
1277         bpy.utils.keyconfig_set(self.filepath)
1278
1279         filepath = self.filepath.replace("keyconfig", "interaction")
1280
1281         if os.path.exists(filepath):
1282             bpy.ops.script.execute_preset(filepath=filepath, menu_idname="USERPREF_MT_interaction_presets")
1283
1284         return {'FINISHED'}
1285
1286
1287 class WM_OT_sysinfo(Operator):
1288     """Generate System Info"""
1289     bl_idname = "wm.sysinfo"
1290     bl_label = "System Info"
1291
1292     def execute(self, context):
1293         import sys_info
1294         sys_info.write_sysinfo(self)
1295         return {'FINISHED'}
1296
1297
1298 class WM_OT_copy_prev_settings(Operator):
1299     """Copy settings from previous version"""
1300     bl_idname = "wm.copy_prev_settings"
1301     bl_label = "Copy Previous Settings"
1302
1303     def execute(self, context):
1304         import os
1305         import shutil
1306         ver = bpy.app.version
1307         ver_old = ((ver[0] * 100) + ver[1]) - 1
1308         path_src = bpy.utils.resource_path('USER', ver_old // 100, ver_old % 100)
1309         path_dst = bpy.utils.resource_path('USER')
1310
1311         if os.path.isdir(path_dst):
1312             self.report({'ERROR'}, "Target path %r exists" % path_dst)
1313         elif not os.path.isdir(path_src):
1314             self.report({'ERROR'}, "Source path %r exists" % path_src)
1315         else:
1316             shutil.copytree(path_src, path_dst, symlinks=True)
1317
1318             # reload recent-files.txt
1319             bpy.ops.wm.read_history()
1320
1321             # don't loose users work if they open the splash later.
1322             if bpy.data.is_saved is bpy.data.is_dirty is False:
1323                 bpy.ops.wm.read_homefile()
1324             else:
1325                 self.report({'INFO'}, "Reload Start-Up file to restore settings")
1326
1327             return {'FINISHED'}
1328
1329         return {'CANCELLED'}
1330
1331
1332 class WM_OT_blenderplayer_start(Operator):
1333     """Launch the blender-player with the current blend-file"""
1334     bl_idname = "wm.blenderplayer_start"
1335     bl_label = "Start Game In Player"
1336
1337     def execute(self, context):
1338         import os
1339         import sys
1340         import subprocess
1341
1342         gs = context.scene.game_settings
1343
1344         # these remain the same every execution
1345         blender_bin_path = bpy.app.binary_path
1346         blender_bin_dir = os.path.dirname(blender_bin_path)
1347         ext = os.path.splitext(blender_bin_path)[-1]
1348         player_path = os.path.join(blender_bin_dir, "blenderplayer" + ext)
1349         # done static vars
1350
1351         if sys.platform == "darwin":
1352             player_path = os.path.join(blender_bin_dir, "../../../blenderplayer.app/Contents/MacOS/blenderplayer")
1353
1354         if not os.path.exists(player_path):
1355             self.report({'ERROR'}, "Player path: %r not found" % player_path)
1356             return {'CANCELLED'}
1357
1358         filepath = bpy.data.filepath + '~' if bpy.data.is_saved else os.path.join(bpy.app.tempdir, "game.blend")
1359         bpy.ops.wm.save_as_mainfile('EXEC_DEFAULT', filepath=filepath, copy=True)
1360
1361         # start the command line call with the player path
1362         args = [player_path]
1363
1364         # handle some UI options as command line arguments
1365         args.extend([
1366             "-g", "show_framerate", "=", "%d" % gs.show_framerate_profile,
1367             "-g", "show_profile", "=", "%d" % gs.show_framerate_profile,
1368             "-g", "show_properties", "=", "%d" % gs.show_debug_properties,
1369             "-g", "ignore_deprecation_warnings", "=", "%d" % (not gs.use_deprecation_warnings),
1370             ])
1371
1372         # finish the call with the path to the blend file
1373         args.append(filepath)
1374
1375         subprocess.call(args)
1376         os.remove(filepath)
1377         return {'FINISHED'}
1378
1379
1380 class WM_OT_keyconfig_test(Operator):
1381     "Test key-config for conflicts"
1382     bl_idname = "wm.keyconfig_test"
1383     bl_label = "Test Key Configuration for Conflicts"
1384
1385     def execute(self, context):
1386         from bpy_extras import keyconfig_utils
1387
1388         wm = context.window_manager
1389         kc = wm.keyconfigs.default
1390
1391         if keyconfig_utils.keyconfig_test(kc):
1392             print("CONFLICT")
1393
1394         return {'FINISHED'}
1395
1396
1397 class WM_OT_keyconfig_import(Operator):
1398     "Import key configuration from a python script"
1399     bl_idname = "wm.keyconfig_import"
1400     bl_label = "Import Key Configuration..."
1401
1402     filepath = StringProperty(
1403             subtype='FILE_PATH',
1404             default="keymap.py",
1405             )
1406     filter_folder = BoolProperty(
1407             name="Filter folders",
1408             default=True,
1409             options={'HIDDEN'},
1410             )
1411     filter_text = BoolProperty(
1412             name="Filter text",
1413             default=True,
1414             options={'HIDDEN'},
1415             )
1416     filter_python = BoolProperty(
1417             name="Filter python",
1418             default=True,
1419             options={'HIDDEN'},
1420             )
1421     keep_original = BoolProperty(
1422             name="Keep original",
1423             description="Keep original file after copying to configuration folder",
1424             default=True,
1425             )
1426
1427     def execute(self, context):
1428         import os
1429         from os.path import basename
1430         import shutil
1431
1432         if not self.filepath:
1433             self.report({'ERROR'}, "Filepath not set")
1434             return {'CANCELLED'}
1435
1436         config_name = basename(self.filepath)
1437
1438         path = bpy.utils.user_resource('SCRIPTS', os.path.join("presets", "keyconfig"), create=True)
1439         path = os.path.join(path, config_name)
1440
1441         try:
1442             if self.keep_original:
1443                 shutil.copy(self.filepath, path)
1444             else:
1445                 shutil.move(self.filepath, path)
1446         except Exception as e:
1447             self.report({'ERROR'}, "Installing keymap failed: %s" % e)
1448             return {'CANCELLED'}
1449
1450         # sneaky way to check we're actually running the code.
1451         if bpy.utils.keyconfig_set(path, report=self.report):
1452             return {'FINISHED'}
1453         else:
1454             return {'CANCELLED'}
1455
1456     def invoke(self, context, event):
1457         wm = context.window_manager
1458         wm.fileselect_add(self)
1459         return {'RUNNING_MODAL'}
1460
1461 # This operator is also used by interaction presets saving - AddPresetBase
1462
1463
1464 class WM_OT_keyconfig_export(Operator):
1465     "Export key configuration to a python script"
1466     bl_idname = "wm.keyconfig_export"
1467     bl_label = "Export Key Configuration..."
1468
1469     filepath = StringProperty(
1470             subtype='FILE_PATH',
1471             default="keymap.py",
1472             )
1473     filter_folder = BoolProperty(
1474             name="Filter folders",
1475             default=True,
1476             options={'HIDDEN'},
1477             )
1478     filter_text = BoolProperty(
1479             name="Filter text",
1480             default=True,
1481             options={'HIDDEN'},
1482             )
1483     filter_python = BoolProperty(
1484             name="Filter python",
1485             default=True,
1486             options={'HIDDEN'},
1487             )
1488
1489     def execute(self, context):
1490         from bpy_extras import keyconfig_utils
1491
1492         if not self.filepath:
1493             raise Exception("Filepath not set")
1494
1495         if not self.filepath.endswith(".py"):
1496             self.filepath += ".py"
1497
1498         wm = context.window_manager
1499
1500         keyconfig_utils.keyconfig_export(wm,
1501                                          wm.keyconfigs.active,
1502                                          self.filepath,
1503                                          )
1504
1505         return {'FINISHED'}
1506
1507     def invoke(self, context, event):
1508         wm = context.window_manager
1509         wm.fileselect_add(self)
1510         return {'RUNNING_MODAL'}
1511
1512
1513 class WM_OT_keymap_restore(Operator):
1514     "Restore key map(s)"
1515     bl_idname = "wm.keymap_restore"
1516     bl_label = "Restore Key Map(s)"
1517
1518     all = BoolProperty(
1519             name="All Keymaps",
1520             description="Restore all keymaps to default",
1521             )
1522
1523     def execute(self, context):
1524         wm = context.window_manager
1525
1526         if self.all:
1527             for km in wm.keyconfigs.user.keymaps:
1528                 km.restore_to_default()
1529         else:
1530             km = context.keymap
1531             km.restore_to_default()
1532
1533         return {'FINISHED'}
1534
1535
1536 class WM_OT_keyitem_restore(Operator):
1537     "Restore key map item"
1538     bl_idname = "wm.keyitem_restore"
1539     bl_label = "Restore Key Map Item"
1540
1541     item_id = IntProperty(
1542             name="Item Identifier",
1543             description="Identifier of the item to remove",
1544             )
1545
1546     @classmethod
1547     def poll(cls, context):
1548         keymap = getattr(context, "keymap", None)
1549         return keymap
1550
1551     def execute(self, context):
1552         km = context.keymap
1553         kmi = km.keymap_items.from_id(self.item_id)
1554
1555         if (not kmi.is_user_defined) and kmi.is_user_modified:
1556             km.restore_item_to_default(kmi)
1557
1558         return {'FINISHED'}
1559
1560
1561 class WM_OT_keyitem_add(Operator):
1562     "Add key map item"
1563     bl_idname = "wm.keyitem_add"
1564     bl_label = "Add Key Map Item"
1565
1566     def execute(self, context):
1567         km = context.keymap
1568
1569         if km.is_modal:
1570             km.keymap_items.new_modal("", 'A', 'PRESS')
1571         else:
1572             km.keymap_items.new("none", 'A', 'PRESS')
1573
1574         # clear filter and expand keymap so we can see the newly added item
1575         if context.space_data.filter_text != "":
1576             context.space_data.filter_text = ""
1577             km.show_expanded_items = True
1578             km.show_expanded_children = True
1579
1580         return {'FINISHED'}
1581
1582
1583 class WM_OT_keyitem_remove(Operator):
1584     "Remove key map item"
1585     bl_idname = "wm.keyitem_remove"
1586     bl_label = "Remove Key Map Item"
1587
1588     item_id = IntProperty(
1589             name="Item Identifier",
1590             description="Identifier of the item to remove",
1591             )
1592
1593     @classmethod
1594     def poll(cls, context):
1595         return hasattr(context, "keymap")
1596
1597     def execute(self, context):
1598         km = context.keymap
1599         kmi = km.keymap_items.from_id(self.item_id)
1600         km.keymap_items.remove(kmi)
1601         return {'FINISHED'}
1602
1603
1604 class WM_OT_keyconfig_remove(Operator):
1605     "Remove key config"
1606     bl_idname = "wm.keyconfig_remove"
1607     bl_label = "Remove Key Config"
1608
1609     @classmethod
1610     def poll(cls, context):
1611         wm = context.window_manager
1612         keyconf = wm.keyconfigs.active
1613         return keyconf and keyconf.is_user_defined
1614
1615     def execute(self, context):
1616         wm = context.window_manager
1617         keyconfig = wm.keyconfigs.active
1618         wm.keyconfigs.remove(keyconfig)
1619         return {'FINISHED'}
1620
1621
1622 class WM_OT_operator_cheat_sheet(Operator):
1623     bl_idname = "wm.operator_cheat_sheet"
1624     bl_label = "Operator Cheat Sheet"
1625
1626     def execute(self, context):
1627         op_strings = []
1628         tot = 0
1629         for op_module_name in dir(bpy.ops):
1630             op_module = getattr(bpy.ops, op_module_name)
1631             for op_submodule_name in dir(op_module):
1632                 op = getattr(op_module, op_submodule_name)
1633                 text = repr(op)
1634                 if text.split("\n")[-1].startswith("bpy.ops."):
1635                     op_strings.append(text)
1636                     tot += 1
1637
1638             op_strings.append('')
1639
1640         textblock = bpy.data.texts.new("OperatorList.txt")
1641         textblock.write('# %d Operators\n\n' % tot)
1642         textblock.write('\n'.join(op_strings))
1643         self.report({'INFO'}, "See OperatorList.txt textblock")
1644         return {'FINISHED'}
1645
1646
1647 # -----------------------------------------------------------------------------
1648 # Addon Operators
1649
1650 class WM_OT_addon_enable(Operator):
1651     "Enable an addon"
1652     bl_idname = "wm.addon_enable"
1653     bl_label = "Enable Addon"
1654
1655     module = StringProperty(
1656             name="Module",
1657             description="Module name of the addon to enable",
1658             )
1659
1660     def execute(self, context):
1661         import addon_utils
1662
1663         err_str = ""
1664
1665         def err_cb():
1666             import traceback
1667             nonlocal err_str
1668             err_str = traceback.format_exc()
1669             print(err_str)
1670
1671         mod = addon_utils.enable(self.module, handle_error=err_cb)
1672
1673         if mod:
1674             info = addon_utils.module_bl_info(mod)
1675
1676             info_ver = info.get("blender", (0, 0, 0))
1677
1678             if info_ver > bpy.app.version:
1679                 self.report({'WARNING'},
1680                             ("This script was written Blender "
1681                              "version %d.%d.%d and might not "
1682                              "function (correctly), "
1683                              "though it is enabled" %
1684                              info_ver))
1685             return {'FINISHED'}
1686         else:
1687
1688             if err_str:
1689                 self.report({'ERROR'}, err_str)
1690
1691             return {'CANCELLED'}
1692
1693
1694 class WM_OT_addon_disable(Operator):
1695     "Disable an addon"
1696     bl_idname = "wm.addon_disable"
1697     bl_label = "Disable Addon"
1698
1699     module = StringProperty(
1700             name="Module",
1701             description="Module name of the addon to disable",
1702             )
1703
1704     def execute(self, context):
1705         import addon_utils
1706
1707         err_str = ""
1708
1709         def err_cb():
1710             import traceback
1711             nonlocal err_str
1712             err_str = traceback.format_exc()
1713             print(err_str)
1714
1715         addon_utils.disable(self.module, handle_error=err_cb)
1716
1717         if err_str:
1718             self.report({'ERROR'}, err_str)
1719
1720         return {'FINISHED'}
1721
1722
1723 class WM_OT_theme_install(Operator):
1724     "Load and apply a Blender XML theme file"
1725     bl_idname = "wm.theme_install"
1726     bl_label = "Install Theme..."
1727
1728     overwrite = BoolProperty(
1729             name="Overwrite",
1730             description="Remove existing theme file if exists",
1731             default=True,
1732             )
1733     filepath = StringProperty(
1734             subtype='FILE_PATH',
1735             )
1736     filter_folder = BoolProperty(
1737             name="Filter folders",
1738             default=True,
1739             options={'HIDDEN'},
1740             )
1741     filter_glob = StringProperty(
1742             default="*.xml",
1743             options={'HIDDEN'},
1744             )
1745
1746     def execute(self, context):
1747         import os
1748         import shutil
1749         import traceback
1750
1751         xmlfile = self.filepath
1752
1753         path_themes = bpy.utils.user_resource('SCRIPTS', "presets/interface_theme", create=True)
1754
1755         if not path_themes:
1756             self.report({'ERROR'}, "Failed to get themes path")
1757             return {'CANCELLED'}
1758
1759         path_dest = os.path.join(path_themes, os.path.basename(xmlfile))
1760
1761         if not self.overwrite:
1762             if os.path.exists(path_dest):
1763                 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
1764                 return {'CANCELLED'}
1765
1766         try:
1767             shutil.copyfile(xmlfile, path_dest)
1768             bpy.ops.script.execute_preset(filepath=path_dest, menu_idname="USERPREF_MT_interface_theme_presets")
1769
1770         except:
1771             traceback.print_exc()
1772             return {'CANCELLED'}
1773
1774         return {'FINISHED'}
1775
1776     def invoke(self, context, event):
1777         wm = context.window_manager
1778         wm.fileselect_add(self)
1779         return {'RUNNING_MODAL'}
1780
1781
1782 class WM_OT_addon_refresh(Operator):
1783     "Scan addon directories for new modules"
1784     bl_idname = "wm.addon_refresh"
1785     bl_label = "Refresh"
1786
1787     def execute(self, context):
1788         import addon_utils
1789
1790         addon_utils.modules_refresh()
1791
1792         return {'FINISHED'}
1793
1794
1795 class WM_OT_addon_install(Operator):
1796     "Install an addon"
1797     bl_idname = "wm.addon_install"
1798     bl_label = "Install from File..."
1799
1800     overwrite = BoolProperty(
1801             name="Overwrite",
1802             description="Remove existing addons with the same ID",
1803             default=True,
1804             )
1805     target = EnumProperty(
1806             name="Target Path",
1807             items=(('DEFAULT', "Default", ""),
1808                    ('PREFS', "User Prefs", "")),
1809             )
1810
1811     filepath = StringProperty(
1812             subtype='FILE_PATH',
1813             )
1814     filter_folder = BoolProperty(
1815             name="Filter folders",
1816             default=True,
1817             options={'HIDDEN'},
1818             )
1819     filter_python = BoolProperty(
1820             name="Filter python",
1821             default=True,
1822             options={'HIDDEN'},
1823             )
1824     filter_glob = StringProperty(
1825             default="*.py;*.zip",
1826             options={'HIDDEN'},
1827             )
1828
1829     @staticmethod
1830     def _module_remove(path_addons, module):
1831         import os
1832         module = os.path.splitext(module)[0]
1833         for f in os.listdir(path_addons):
1834             f_base = os.path.splitext(f)[0]
1835             if f_base == module:
1836                 f_full = os.path.join(path_addons, f)
1837
1838                 if os.path.isdir(f_full):
1839                     os.rmdir(f_full)
1840                 else:
1841                     os.remove(f_full)
1842
1843     def execute(self, context):
1844         import addon_utils
1845         import traceback
1846         import zipfile
1847         import shutil
1848         import os
1849
1850         pyfile = self.filepath
1851
1852         if self.target == 'DEFAULT':
1853             # don't use bpy.utils.script_paths("addons") because we may not be able to write to it.
1854             path_addons = bpy.utils.user_resource('SCRIPTS', "addons", create=True)
1855         else:
1856             path_addons = context.user_preferences.filepaths.script_directory
1857             if path_addons:
1858                 path_addons = os.path.join(path_addons, "addons")
1859
1860         if not path_addons:
1861             self.report({'ERROR'}, "Failed to get addons path")
1862             return {'CANCELLED'}
1863
1864         if not os.path.isdir(path_addons):
1865             try:
1866                 os.makedirs(path_addons, exist_ok=True)
1867             except:
1868                 import traceback
1869                 traceback.print_exc()
1870
1871         # Check if we are installing from a target path,
1872         # doing so causes 2+ addons of same name or when the same from/to
1873         # location is used, removal of the file!
1874         addon_path = ""
1875         pyfile_dir = os.path.dirname(pyfile)
1876         for addon_path in addon_utils.paths():
1877             if os.path.samefile(pyfile_dir, addon_path):
1878                 self.report({'ERROR'}, "Source file is in the addon search path: %r" % addon_path)
1879                 return {'CANCELLED'}
1880         del addon_path
1881         del pyfile_dir
1882         # done checking for exceptional case
1883
1884         addons_old = {mod.__name__ for mod in addon_utils.modules()}
1885
1886         #check to see if the file is in compressed format (.zip)
1887         if zipfile.is_zipfile(pyfile):
1888             try:
1889                 file_to_extract = zipfile.ZipFile(pyfile, 'r')
1890             except:
1891                 traceback.print_exc()
1892                 return {'CANCELLED'}
1893
1894             if self.overwrite:
1895                 for f in file_to_extract.namelist():
1896                     WM_OT_addon_install._module_remove(path_addons, f)
1897             else:
1898                 for f in file_to_extract.namelist():
1899                     path_dest = os.path.join(path_addons, os.path.basename(f))
1900                     if os.path.exists(path_dest):
1901                         self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
1902                         return {'CANCELLED'}
1903
1904             try:  # extract the file to "addons"
1905                 file_to_extract.extractall(path_addons)
1906             except:
1907                 traceback.print_exc()
1908                 return {'CANCELLED'}
1909
1910         else:
1911             path_dest = os.path.join(path_addons, os.path.basename(pyfile))
1912
1913             if self.overwrite:
1914                 WM_OT_addon_install._module_remove(path_addons, os.path.basename(pyfile))
1915             elif os.path.exists(path_dest):
1916                 self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
1917                 return {'CANCELLED'}
1918
1919             #if not compressed file just copy into the addon path
1920             try:
1921                 shutil.copyfile(pyfile, path_dest)
1922
1923             except:
1924                 traceback.print_exc()
1925                 return {'CANCELLED'}
1926
1927         addons_new = {mod.__name__ for mod in addon_utils.modules()} - addons_old
1928         addons_new.discard("modules")
1929
1930         # disable any addons we may have enabled previously and removed.
1931         # this is unlikely but do just in case. bug [#23978]
1932         for new_addon in addons_new:
1933             addon_utils.disable(new_addon)
1934
1935         # possible the zip contains multiple addons, we could disallow this
1936         # but for now just use the first
1937         for mod in addon_utils.modules(refresh=False):
1938             if mod.__name__ in addons_new:
1939                 info = addon_utils.module_bl_info(mod)
1940
1941                 # show the newly installed addon.
1942                 context.window_manager.addon_filter = 'All'
1943                 context.window_manager.addon_search = info["name"]
1944                 break
1945
1946         # in case a new module path was created to install this addon.
1947         bpy.utils.refresh_script_paths()
1948
1949         # print message
1950         msg = tip_("Modules Installed from %r into %r (%s)") % (pyfile, path_addons, ", ".join(sorted(addons_new)))
1951         print(msg)
1952         self.report({'INFO'}, msg)
1953
1954         return {'FINISHED'}
1955
1956     def invoke(self, context, event):
1957         wm = context.window_manager
1958         wm.fileselect_add(self)
1959         return {'RUNNING_MODAL'}
1960
1961
1962 class WM_OT_addon_remove(Operator):
1963     "Delete the addon from the file system"
1964     bl_idname = "wm.addon_remove"
1965     bl_label = "Remove Addon"
1966
1967     module = StringProperty(
1968             name="Module",
1969             description="Module name of the addon to remove",
1970             )
1971
1972     @staticmethod
1973     def path_from_addon(module):
1974         import os
1975         import addon_utils
1976
1977         for mod in addon_utils.modules():
1978             if mod.__name__ == module:
1979                 filepath = mod.__file__
1980                 if os.path.exists(filepath):
1981                     if os.path.splitext(os.path.basename(filepath))[0] == "__init__":
1982                         return os.path.dirname(filepath), True
1983                     else:
1984                         return filepath, False
1985         return None, False
1986
1987     def execute(self, context):
1988         import addon_utils
1989         import os
1990
1991         path, isdir = WM_OT_addon_remove.path_from_addon(self.module)
1992         if path is None:
1993             self.report({'WARNING'}, "Addon path %r could not be found" % path)
1994             return {'CANCELLED'}
1995
1996         # in case its enabled
1997         addon_utils.disable(self.module)
1998
1999         import shutil
2000         if isdir:
2001             shutil.rmtree(path)
2002         else:
2003             os.remove(path)
2004
2005         addon_utils.modules_refresh()
2006
2007         context.area.tag_redraw()
2008         return {'FINISHED'}
2009
2010     # lame confirmation check
2011     def draw(self, context):
2012         self.layout.label(text="Remove Addon: %r?" % self.module)
2013         path, isdir = WM_OT_addon_remove.path_from_addon(self.module)
2014         self.layout.label(text="Path: %r" % path)
2015
2016     def invoke(self, context, event):
2017         wm = context.window_manager
2018         return wm.invoke_props_dialog(self, width=600)
2019
2020
2021 class WM_OT_addon_expand(Operator):
2022     "Display more information on this addon"
2023     bl_idname = "wm.addon_expand"
2024     bl_label = ""
2025     bl_options = {'INTERNAL'}
2026
2027     module = StringProperty(
2028             name="Module",
2029             description="Module name of the addon to expand",
2030             )
2031
2032     def execute(self, context):
2033         import addon_utils
2034
2035         module_name = self.module
2036
2037         # unlikely to fail, module should have already been imported
2038         try:
2039             # mod = __import__(module_name)
2040             mod = addon_utils.addons_fake_modules.get(module_name)
2041         except:
2042             import traceback
2043             traceback.print_exc()
2044             return {'CANCELLED'}
2045
2046         info = addon_utils.module_bl_info(mod)
2047         info["show_expanded"] = not info["show_expanded"]
2048         return {'FINISHED'}