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