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