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