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