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