userpref.py 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111
  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. # <pep8 compliant>
  19. # TODO, use PREFERENCES_OT_* prefix for operators.
  20. import bpy
  21. from bpy.types import (
  22. Operator,
  23. OperatorFileListElement
  24. )
  25. from bpy.props import (
  26. BoolProperty,
  27. EnumProperty,
  28. IntProperty,
  29. StringProperty,
  30. CollectionProperty,
  31. )
  32. from bpy.app.translations import pgettext_tip as tip_
  33. def module_filesystem_remove(path_base, module_name):
  34. import os
  35. module_name = os.path.splitext(module_name)[0]
  36. for f in os.listdir(path_base):
  37. f_base = os.path.splitext(f)[0]
  38. if f_base == module_name:
  39. f_full = os.path.join(path_base, f)
  40. if os.path.isdir(f_full):
  41. os.rmdir(f_full)
  42. else:
  43. os.remove(f_full)
  44. class PREFERENCES_OT_keyconfig_activate(Operator):
  45. bl_idname = "preferences.keyconfig_activate"
  46. bl_label = "Activate Keyconfig"
  47. filepath: StringProperty(
  48. subtype='FILE_PATH',
  49. )
  50. def execute(self, _context):
  51. if bpy.utils.keyconfig_set(self.filepath, report=self.report):
  52. return {'FINISHED'}
  53. else:
  54. return {'CANCELLED'}
  55. class PREFERENCES_OT_copy_prev(Operator):
  56. """Copy settings from previous version"""
  57. bl_idname = "preferences.copy_prev"
  58. bl_label = "Copy Previous Settings"
  59. @staticmethod
  60. def previous_version():
  61. ver = bpy.app.version
  62. ver_old = ((ver[0] * 100) + ver[1]) - 1
  63. return ver_old // 100, ver_old % 100
  64. @staticmethod
  65. def _old_path():
  66. ver = bpy.app.version
  67. ver_old = ((ver[0] * 100) + ver[1]) - 1
  68. return bpy.utils.resource_path('USER', ver_old // 100, ver_old % 100)
  69. @staticmethod
  70. def _new_path():
  71. return bpy.utils.resource_path('USER')
  72. @classmethod
  73. def poll(cls, _context):
  74. import os
  75. old = cls._old_path()
  76. new = cls._new_path()
  77. # Disable operator in case config path is overriden with environment
  78. # variable. That case has no automatic per-version configuration.
  79. userconfig_path = os.path.normpath(bpy.utils.user_resource('CONFIG'))
  80. new_userconfig_path = os.path.normpath(os.path.join(new, "config"))
  81. if userconfig_path != new_userconfig_path:
  82. return False
  83. # Enable operator if new config path does not exist yet.
  84. if os.path.isdir(old) and not os.path.isdir(new):
  85. return True
  86. # Enable operator also if there are no new user preference yet.
  87. old_userpref = os.path.join(old, "config", "userpref.blend")
  88. new_userpref = os.path.join(new, "config", "userpref.blend")
  89. return os.path.isfile(old_userpref) and not os.path.isfile(new_userpref)
  90. def execute(self, _context):
  91. import shutil
  92. shutil.copytree(self._old_path(), self._new_path(), symlinks=True)
  93. # reload recent-files.txt
  94. bpy.ops.wm.read_history()
  95. # don't loose users work if they open the splash later.
  96. if bpy.data.is_saved is bpy.data.is_dirty is False:
  97. bpy.ops.wm.read_homefile()
  98. else:
  99. self.report({'INFO'}, "Reload Start-Up file to restore settings")
  100. return {'FINISHED'}
  101. class PREFERENCES_OT_keyconfig_test(Operator):
  102. """Test key-config for conflicts"""
  103. bl_idname = "preferences.keyconfig_test"
  104. bl_label = "Test Key Configuration for Conflicts"
  105. def execute(self, context):
  106. from bpy_extras import keyconfig_utils
  107. wm = context.window_manager
  108. kc = wm.keyconfigs.default
  109. if keyconfig_utils.keyconfig_test(kc):
  110. print("CONFLICT")
  111. return {'FINISHED'}
  112. class PREFERENCES_OT_keyconfig_import(Operator):
  113. """Import key configuration from a python script"""
  114. bl_idname = "preferences.keyconfig_import"
  115. bl_label = "Import Key Configuration..."
  116. filepath: StringProperty(
  117. subtype='FILE_PATH',
  118. default="keymap.py",
  119. )
  120. filter_folder: BoolProperty(
  121. name="Filter folders",
  122. default=True,
  123. options={'HIDDEN'},
  124. )
  125. filter_text: BoolProperty(
  126. name="Filter text",
  127. default=True,
  128. options={'HIDDEN'},
  129. )
  130. filter_python: BoolProperty(
  131. name="Filter python",
  132. default=True,
  133. options={'HIDDEN'},
  134. )
  135. keep_original: BoolProperty(
  136. name="Keep original",
  137. description="Keep original file after copying to configuration folder",
  138. default=True,
  139. )
  140. def execute(self, _context):
  141. import os
  142. from os.path import basename
  143. import shutil
  144. if not self.filepath:
  145. self.report({'ERROR'}, "Filepath not set")
  146. return {'CANCELLED'}
  147. config_name = basename(self.filepath)
  148. path = bpy.utils.user_resource('SCRIPTS', os.path.join("presets", "keyconfig"), create=True)
  149. path = os.path.join(path, config_name)
  150. try:
  151. if self.keep_original:
  152. shutil.copy(self.filepath, path)
  153. else:
  154. shutil.move(self.filepath, path)
  155. except Exception as ex:
  156. self.report({'ERROR'}, "Installing keymap failed: %s" % ex)
  157. return {'CANCELLED'}
  158. # sneaky way to check we're actually running the code.
  159. if bpy.utils.keyconfig_set(path, report=self.report):
  160. return {'FINISHED'}
  161. else:
  162. return {'CANCELLED'}
  163. def invoke(self, context, _event):
  164. wm = context.window_manager
  165. wm.fileselect_add(self)
  166. return {'RUNNING_MODAL'}
  167. # This operator is also used by interaction presets saving - AddPresetBase
  168. class PREFERENCES_OT_keyconfig_export(Operator):
  169. """Export key configuration to a python script"""
  170. bl_idname = "preferences.keyconfig_export"
  171. bl_label = "Export Key Configuration..."
  172. all: BoolProperty(
  173. name="All Keymaps",
  174. default=False,
  175. description="Write all keymaps (not just user modified)",
  176. )
  177. filepath: StringProperty(
  178. subtype='FILE_PATH',
  179. default="keymap.py",
  180. )
  181. filter_folder: BoolProperty(
  182. name="Filter folders",
  183. default=True,
  184. options={'HIDDEN'},
  185. )
  186. filter_text: BoolProperty(
  187. name="Filter text",
  188. default=True,
  189. options={'HIDDEN'},
  190. )
  191. filter_python: BoolProperty(
  192. name="Filter python",
  193. default=True,
  194. options={'HIDDEN'},
  195. )
  196. def execute(self, context):
  197. from bl_keymap_utils.io import keyconfig_export_as_data
  198. if not self.filepath:
  199. raise Exception("Filepath not set")
  200. if not self.filepath.endswith(".py"):
  201. self.filepath += ".py"
  202. wm = context.window_manager
  203. keyconfig_export_as_data(
  204. wm,
  205. wm.keyconfigs.active,
  206. self.filepath,
  207. all_keymaps=self.all,
  208. )
  209. return {'FINISHED'}
  210. def invoke(self, context, _event):
  211. wm = context.window_manager
  212. wm.fileselect_add(self)
  213. return {'RUNNING_MODAL'}
  214. class PREFERENCES_OT_keymap_restore(Operator):
  215. """Restore key map(s)"""
  216. bl_idname = "preferences.keymap_restore"
  217. bl_label = "Restore Key Map(s)"
  218. all: BoolProperty(
  219. name="All Keymaps",
  220. description="Restore all keymaps to default",
  221. )
  222. def execute(self, context):
  223. wm = context.window_manager
  224. if self.all:
  225. for km in wm.keyconfigs.user.keymaps:
  226. km.restore_to_default()
  227. else:
  228. km = context.keymap
  229. km.restore_to_default()
  230. context.preferences.is_dirty = True
  231. return {'FINISHED'}
  232. class PREFERENCES_OT_keyitem_restore(Operator):
  233. """Restore key map item"""
  234. bl_idname = "preferences.keyitem_restore"
  235. bl_label = "Restore Key Map Item"
  236. item_id: IntProperty(
  237. name="Item Identifier",
  238. description="Identifier of the item to remove",
  239. )
  240. @classmethod
  241. def poll(cls, context):
  242. keymap = getattr(context, "keymap", None)
  243. return keymap
  244. def execute(self, context):
  245. km = context.keymap
  246. kmi = km.keymap_items.from_id(self.item_id)
  247. if (not kmi.is_user_defined) and kmi.is_user_modified:
  248. km.restore_item_to_default(kmi)
  249. return {'FINISHED'}
  250. class PREFERENCES_OT_keyitem_add(Operator):
  251. """Add key map item"""
  252. bl_idname = "preferences.keyitem_add"
  253. bl_label = "Add Key Map Item"
  254. def execute(self, context):
  255. km = context.keymap
  256. if km.is_modal:
  257. km.keymap_items.new_modal("", 'A', 'PRESS')
  258. else:
  259. km.keymap_items.new("none", 'A', 'PRESS')
  260. # clear filter and expand keymap so we can see the newly added item
  261. if context.space_data.filter_text != "":
  262. context.space_data.filter_text = ""
  263. km.show_expanded_items = True
  264. km.show_expanded_children = True
  265. context.preferences.is_dirty = True
  266. return {'FINISHED'}
  267. class PREFERENCES_OT_keyitem_remove(Operator):
  268. """Remove key map item"""
  269. bl_idname = "preferences.keyitem_remove"
  270. bl_label = "Remove Key Map Item"
  271. item_id: IntProperty(
  272. name="Item Identifier",
  273. description="Identifier of the item to remove",
  274. )
  275. @classmethod
  276. def poll(cls, context):
  277. return hasattr(context, "keymap")
  278. def execute(self, context):
  279. km = context.keymap
  280. kmi = km.keymap_items.from_id(self.item_id)
  281. km.keymap_items.remove(kmi)
  282. context.preferences.is_dirty = True
  283. return {'FINISHED'}
  284. class PREFERENCES_OT_keyconfig_remove(Operator):
  285. """Remove key config"""
  286. bl_idname = "preferences.keyconfig_remove"
  287. bl_label = "Remove Key Config"
  288. @classmethod
  289. def poll(cls, context):
  290. wm = context.window_manager
  291. keyconf = wm.keyconfigs.active
  292. return keyconf and keyconf.is_user_defined
  293. def execute(self, context):
  294. wm = context.window_manager
  295. keyconfig = wm.keyconfigs.active
  296. wm.keyconfigs.remove(keyconfig)
  297. return {'FINISHED'}
  298. # -----------------------------------------------------------------------------
  299. # Add-on Operators
  300. class PREFERENCES_OT_addon_enable(Operator):
  301. """Enable an add-on"""
  302. bl_idname = "preferences.addon_enable"
  303. bl_label = "Enable Add-on"
  304. module: StringProperty(
  305. name="Module",
  306. description="Module name of the add-on to enable",
  307. )
  308. def execute(self, _context):
  309. import addon_utils
  310. err_str = ""
  311. def err_cb(ex):
  312. import traceback
  313. nonlocal err_str
  314. err_str = traceback.format_exc()
  315. print(err_str)
  316. mod = addon_utils.enable(self.module, default_set=True, handle_error=err_cb)
  317. if mod:
  318. info = addon_utils.module_bl_info(mod)
  319. info_ver = info.get("blender", (0, 0, 0))
  320. if info_ver > bpy.app.version:
  321. self.report(
  322. {'WARNING'},
  323. "This script was written Blender "
  324. "version %d.%d.%d and might not "
  325. "function (correctly), "
  326. "though it is enabled" %
  327. info_ver
  328. )
  329. return {'FINISHED'}
  330. else:
  331. if err_str:
  332. self.report({'ERROR'}, err_str)
  333. return {'CANCELLED'}
  334. class PREFERENCES_OT_addon_disable(Operator):
  335. """Disable an add-on"""
  336. bl_idname = "preferences.addon_disable"
  337. bl_label = "Disable Add-on"
  338. module: StringProperty(
  339. name="Module",
  340. description="Module name of the add-on to disable",
  341. )
  342. def execute(self, _context):
  343. import addon_utils
  344. err_str = ""
  345. def err_cb(ex):
  346. import traceback
  347. nonlocal err_str
  348. err_str = traceback.format_exc()
  349. print(err_str)
  350. addon_utils.disable(self.module, default_set=True, handle_error=err_cb)
  351. if err_str:
  352. self.report({'ERROR'}, err_str)
  353. return {'FINISHED'}
  354. class PREFERENCES_OT_theme_install(Operator):
  355. """Load and apply a Blender XML theme file"""
  356. bl_idname = "preferences.theme_install"
  357. bl_label = "Install Theme..."
  358. overwrite: BoolProperty(
  359. name="Overwrite",
  360. description="Remove existing theme file if exists",
  361. default=True,
  362. )
  363. filepath: StringProperty(
  364. subtype='FILE_PATH',
  365. )
  366. filter_folder: BoolProperty(
  367. name="Filter folders",
  368. default=True,
  369. options={'HIDDEN'},
  370. )
  371. filter_glob: StringProperty(
  372. default="*.xml",
  373. options={'HIDDEN'},
  374. )
  375. def execute(self, _context):
  376. import os
  377. import shutil
  378. import traceback
  379. xmlfile = self.filepath
  380. path_themes = bpy.utils.user_resource('SCRIPTS', "presets/interface_theme", create=True)
  381. if not path_themes:
  382. self.report({'ERROR'}, "Failed to get themes path")
  383. return {'CANCELLED'}
  384. path_dest = os.path.join(path_themes, os.path.basename(xmlfile))
  385. if not self.overwrite:
  386. if os.path.exists(path_dest):
  387. self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
  388. return {'CANCELLED'}
  389. try:
  390. shutil.copyfile(xmlfile, path_dest)
  391. bpy.ops.script.execute_preset(
  392. filepath=path_dest,
  393. menu_idname="USERPREF_MT_interface_theme_presets",
  394. )
  395. except:
  396. traceback.print_exc()
  397. return {'CANCELLED'}
  398. return {'FINISHED'}
  399. def invoke(self, context, _event):
  400. wm = context.window_manager
  401. wm.fileselect_add(self)
  402. return {'RUNNING_MODAL'}
  403. class PREFERENCES_OT_addon_refresh(Operator):
  404. """Scan add-on directories for new modules"""
  405. bl_idname = "preferences.addon_refresh"
  406. bl_label = "Refresh"
  407. def execute(self, _context):
  408. import addon_utils
  409. addon_utils.modules_refresh()
  410. return {'FINISHED'}
  411. # Note: shares some logic with PREFERENCES_OT_app_template_install
  412. # but not enough to de-duplicate. Fixed here may apply to both.
  413. class PREFERENCES_OT_addon_install(Operator):
  414. """Install an add-on"""
  415. bl_idname = "preferences.addon_install"
  416. bl_label = "Install Add-on from File..."
  417. overwrite: BoolProperty(
  418. name="Overwrite",
  419. description="Remove existing add-ons with the same ID",
  420. default=True,
  421. )
  422. target: EnumProperty(
  423. name="Target Path",
  424. items=(
  425. ('DEFAULT', "Default", ""),
  426. ('PREFS', "User Prefs", ""),
  427. ),
  428. )
  429. filepath: StringProperty(
  430. subtype='FILE_PATH',
  431. )
  432. filter_folder: BoolProperty(
  433. name="Filter folders",
  434. default=True,
  435. options={'HIDDEN'},
  436. )
  437. filter_python: BoolProperty(
  438. name="Filter python",
  439. default=True,
  440. options={'HIDDEN'},
  441. )
  442. filter_glob: StringProperty(
  443. default="*.py;*.zip",
  444. options={'HIDDEN'},
  445. )
  446. def execute(self, context):
  447. import addon_utils
  448. import traceback
  449. import zipfile
  450. import shutil
  451. import os
  452. pyfile = self.filepath
  453. if self.target == 'DEFAULT':
  454. # don't use bpy.utils.script_paths("addons") because we may not be able to write to it.
  455. path_addons = bpy.utils.user_resource('SCRIPTS', "addons", create=True)
  456. else:
  457. path_addons = context.preferences.filepaths.script_directory
  458. if path_addons:
  459. path_addons = os.path.join(path_addons, "addons")
  460. if not path_addons:
  461. self.report({'ERROR'}, "Failed to get add-ons path")
  462. return {'CANCELLED'}
  463. if not os.path.isdir(path_addons):
  464. try:
  465. os.makedirs(path_addons, exist_ok=True)
  466. except:
  467. traceback.print_exc()
  468. # Check if we are installing from a target path,
  469. # doing so causes 2+ addons of same name or when the same from/to
  470. # location is used, removal of the file!
  471. addon_path = ""
  472. pyfile_dir = os.path.dirname(pyfile)
  473. for addon_path in addon_utils.paths():
  474. if os.path.samefile(pyfile_dir, addon_path):
  475. self.report({'ERROR'}, "Source file is in the add-on search path: %r" % addon_path)
  476. return {'CANCELLED'}
  477. del addon_path
  478. del pyfile_dir
  479. # done checking for exceptional case
  480. addons_old = {mod.__name__ for mod in addon_utils.modules()}
  481. # check to see if the file is in compressed format (.zip)
  482. if zipfile.is_zipfile(pyfile):
  483. try:
  484. file_to_extract = zipfile.ZipFile(pyfile, 'r')
  485. except:
  486. traceback.print_exc()
  487. return {'CANCELLED'}
  488. if self.overwrite:
  489. for f in file_to_extract.namelist():
  490. module_filesystem_remove(path_addons, f)
  491. else:
  492. for f in file_to_extract.namelist():
  493. path_dest = os.path.join(path_addons, os.path.basename(f))
  494. if os.path.exists(path_dest):
  495. self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
  496. return {'CANCELLED'}
  497. try: # extract the file to "addons"
  498. file_to_extract.extractall(path_addons)
  499. except:
  500. traceback.print_exc()
  501. return {'CANCELLED'}
  502. else:
  503. path_dest = os.path.join(path_addons, os.path.basename(pyfile))
  504. if self.overwrite:
  505. module_filesystem_remove(path_addons, os.path.basename(pyfile))
  506. elif os.path.exists(path_dest):
  507. self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
  508. return {'CANCELLED'}
  509. # if not compressed file just copy into the addon path
  510. try:
  511. shutil.copyfile(pyfile, path_dest)
  512. except:
  513. traceback.print_exc()
  514. return {'CANCELLED'}
  515. addons_new = {mod.__name__ for mod in addon_utils.modules()} - addons_old
  516. addons_new.discard("modules")
  517. # disable any addons we may have enabled previously and removed.
  518. # this is unlikely but do just in case. bug [#23978]
  519. for new_addon in addons_new:
  520. addon_utils.disable(new_addon, default_set=True)
  521. # possible the zip contains multiple addons, we could disallow this
  522. # but for now just use the first
  523. for mod in addon_utils.modules(refresh=False):
  524. if mod.__name__ in addons_new:
  525. info = addon_utils.module_bl_info(mod)
  526. # show the newly installed addon.
  527. context.window_manager.addon_filter = 'All'
  528. context.window_manager.addon_search = info["name"]
  529. break
  530. # in case a new module path was created to install this addon.
  531. bpy.utils.refresh_script_paths()
  532. # print message
  533. msg = (
  534. tip_("Modules Installed (%s) from %r into %r") %
  535. (", ".join(sorted(addons_new)), pyfile, path_addons)
  536. )
  537. print(msg)
  538. self.report({'INFO'}, msg)
  539. return {'FINISHED'}
  540. def invoke(self, context, _event):
  541. wm = context.window_manager
  542. wm.fileselect_add(self)
  543. return {'RUNNING_MODAL'}
  544. class PREFERENCES_OT_addon_remove(Operator):
  545. """Delete the add-on from the file system"""
  546. bl_idname = "preferences.addon_remove"
  547. bl_label = "Remove Add-on"
  548. module: StringProperty(
  549. name="Module",
  550. description="Module name of the add-on to remove",
  551. )
  552. @staticmethod
  553. def path_from_addon(module):
  554. import os
  555. import addon_utils
  556. for mod in addon_utils.modules():
  557. if mod.__name__ == module:
  558. filepath = mod.__file__
  559. if os.path.exists(filepath):
  560. if os.path.splitext(os.path.basename(filepath))[0] == "__init__":
  561. return os.path.dirname(filepath), True
  562. else:
  563. return filepath, False
  564. return None, False
  565. def execute(self, context):
  566. import addon_utils
  567. import os
  568. path, isdir = PREFERENCES_OT_addon_remove.path_from_addon(self.module)
  569. if path is None:
  570. self.report({'WARNING'}, "Add-on path %r could not be found" % path)
  571. return {'CANCELLED'}
  572. # in case its enabled
  573. addon_utils.disable(self.module, default_set=True)
  574. import shutil
  575. if isdir and (not os.path.islink(path)):
  576. shutil.rmtree(path)
  577. else:
  578. os.remove(path)
  579. addon_utils.modules_refresh()
  580. context.area.tag_redraw()
  581. return {'FINISHED'}
  582. # lame confirmation check
  583. def draw(self, _context):
  584. self.layout.label(text="Remove Add-on: %r?" % self.module)
  585. path, _isdir = PREFERENCES_OT_addon_remove.path_from_addon(self.module)
  586. self.layout.label(text="Path: %r" % path)
  587. def invoke(self, context, _event):
  588. wm = context.window_manager
  589. return wm.invoke_props_dialog(self, width=600)
  590. class PREFERENCES_OT_addon_expand(Operator):
  591. """Display information and preferences for this add-on"""
  592. bl_idname = "preferences.addon_expand"
  593. bl_label = ""
  594. bl_options = {'INTERNAL'}
  595. module: StringProperty(
  596. name="Module",
  597. description="Module name of the add-on to expand",
  598. )
  599. def execute(self, _context):
  600. import addon_utils
  601. module_name = self.module
  602. mod = addon_utils.addons_fake_modules.get(module_name)
  603. if mod is not None:
  604. info = addon_utils.module_bl_info(mod)
  605. info["show_expanded"] = not info["show_expanded"]
  606. return {'FINISHED'}
  607. class PREFERENCES_OT_addon_show(Operator):
  608. """Show add-on preferences"""
  609. bl_idname = "preferences.addon_show"
  610. bl_label = ""
  611. bl_options = {'INTERNAL'}
  612. module: StringProperty(
  613. name="Module",
  614. description="Module name of the add-on to expand",
  615. )
  616. def execute(self, context):
  617. import addon_utils
  618. module_name = self.module
  619. _modules = addon_utils.modules(refresh=False)
  620. mod = addon_utils.addons_fake_modules.get(module_name)
  621. if mod is not None:
  622. info = addon_utils.module_bl_info(mod)
  623. info["show_expanded"] = True
  624. context.preferences.active_section = 'ADDONS'
  625. context.window_manager.addon_filter = 'All'
  626. context.window_manager.addon_search = info["name"]
  627. bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
  628. return {'FINISHED'}
  629. # Note: shares some logic with PREFERENCES_OT_addon_install
  630. # but not enough to de-duplicate. Fixes here may apply to both.
  631. class PREFERENCES_OT_app_template_install(Operator):
  632. """Install an application-template"""
  633. bl_idname = "preferences.app_template_install"
  634. bl_label = "Install Template from File..."
  635. overwrite: BoolProperty(
  636. name="Overwrite",
  637. description="Remove existing template with the same ID",
  638. default=True,
  639. )
  640. filepath: StringProperty(
  641. subtype='FILE_PATH',
  642. )
  643. filter_folder: BoolProperty(
  644. name="Filter folders",
  645. default=True,
  646. options={'HIDDEN'},
  647. )
  648. filter_glob: StringProperty(
  649. default="*.zip",
  650. options={'HIDDEN'},
  651. )
  652. def execute(self, _context):
  653. import traceback
  654. import zipfile
  655. import os
  656. filepath = self.filepath
  657. path_app_templates = bpy.utils.user_resource(
  658. 'SCRIPTS', os.path.join("startup", "bl_app_templates_user"),
  659. create=True,
  660. )
  661. if not path_app_templates:
  662. self.report({'ERROR'}, "Failed to get add-ons path")
  663. return {'CANCELLED'}
  664. if not os.path.isdir(path_app_templates):
  665. try:
  666. os.makedirs(path_app_templates, exist_ok=True)
  667. except:
  668. traceback.print_exc()
  669. app_templates_old = set(os.listdir(path_app_templates))
  670. # check to see if the file is in compressed format (.zip)
  671. if zipfile.is_zipfile(filepath):
  672. try:
  673. file_to_extract = zipfile.ZipFile(filepath, 'r')
  674. except:
  675. traceback.print_exc()
  676. return {'CANCELLED'}
  677. if self.overwrite:
  678. for f in file_to_extract.namelist():
  679. module_filesystem_remove(path_app_templates, f)
  680. else:
  681. for f in file_to_extract.namelist():
  682. path_dest = os.path.join(path_app_templates, os.path.basename(f))
  683. if os.path.exists(path_dest):
  684. self.report({'WARNING'}, "File already installed to %r\n" % path_dest)
  685. return {'CANCELLED'}
  686. try: # extract the file to "bl_app_templates_user"
  687. file_to_extract.extractall(path_app_templates)
  688. except:
  689. traceback.print_exc()
  690. return {'CANCELLED'}
  691. else:
  692. # Only support installing zipfiles
  693. self.report({'WARNING'}, "Expected a zip-file %r\n" % filepath)
  694. return {'CANCELLED'}
  695. app_templates_new = set(os.listdir(path_app_templates)) - app_templates_old
  696. # in case a new module path was created to install this addon.
  697. bpy.utils.refresh_script_paths()
  698. # print message
  699. msg = (
  700. tip_("Template Installed (%s) from %r into %r") %
  701. (", ".join(sorted(app_templates_new)), filepath, path_app_templates)
  702. )
  703. print(msg)
  704. self.report({'INFO'}, msg)
  705. return {'FINISHED'}
  706. def invoke(self, context, _event):
  707. wm = context.window_manager
  708. wm.fileselect_add(self)
  709. return {'RUNNING_MODAL'}
  710. # -----------------------------------------------------------------------------
  711. # Studio Light Operations
  712. class PREFERENCES_OT_studiolight_install(Operator):
  713. """Install a user defined studio light"""
  714. bl_idname = "preferences.studiolight_install"
  715. bl_label = "Install Custom Studio Light"
  716. files: CollectionProperty(
  717. name="File Path",
  718. type=OperatorFileListElement,
  719. )
  720. directory: StringProperty(
  721. subtype='DIR_PATH',
  722. )
  723. filter_folder: BoolProperty(
  724. name="Filter folders",
  725. default=True,
  726. options={'HIDDEN'},
  727. )
  728. filter_glob: StringProperty(
  729. default="*.png;*.jpg;*.hdr;*.exr",
  730. options={'HIDDEN'},
  731. )
  732. type: EnumProperty(
  733. items=(
  734. ('MATCAP', "MatCap", ""),
  735. ('WORLD', "World", ""),
  736. ('STUDIO', "Studio", ""),
  737. )
  738. )
  739. def execute(self, context):
  740. import os
  741. import shutil
  742. prefs = context.preferences
  743. path_studiolights = os.path.join("studiolights", self.type.lower())
  744. path_studiolights = bpy.utils.user_resource('DATAFILES', path_studiolights, create=True)
  745. if not path_studiolights:
  746. self.report({'ERROR'}, "Failed to create Studio Light path")
  747. return {'CANCELLED'}
  748. for e in self.files:
  749. shutil.copy(os.path.join(self.directory, e.name), path_studiolights)
  750. prefs.studio_lights.load(os.path.join(path_studiolights, e.name), self.type)
  751. # print message
  752. msg = (
  753. tip_("StudioLight Installed %r into %r") %
  754. (", ".join(e.name for e in self.files), path_studiolights)
  755. )
  756. print(msg)
  757. self.report({'INFO'}, msg)
  758. return {'FINISHED'}
  759. def invoke(self, context, _event):
  760. wm = context.window_manager
  761. if self.type == 'STUDIO':
  762. self.filter_glob = "*.sl"
  763. wm.fileselect_add(self)
  764. return {'RUNNING_MODAL'}
  765. class PREFERENCES_OT_studiolight_new(Operator):
  766. """Save custom studio light from the studio light editor settings"""
  767. bl_idname = "preferences.studiolight_new"
  768. bl_label = "Save Custom Studio Light"
  769. filename: StringProperty(
  770. name="Name",
  771. default="StudioLight",
  772. )
  773. ask_overide = False
  774. def execute(self, context):
  775. import os
  776. prefs = context.preferences
  777. wm = context.window_manager
  778. filename = bpy.path.ensure_ext(self.filename, ".sl")
  779. path_studiolights = bpy.utils.user_resource('DATAFILES', os.path.join("studiolights", "studio"), create=True)
  780. if not path_studiolights:
  781. self.report({'ERROR'}, "Failed to get Studio Light path")
  782. return {'CANCELLED'}
  783. filepath_final = os.path.join(path_studiolights, filename)
  784. if os.path.isfile(filepath_final):
  785. if not self.ask_overide:
  786. self.ask_overide = True
  787. return wm.invoke_props_dialog(self, width=600)
  788. else:
  789. for studio_light in prefs.studio_lights:
  790. if studio_light.name == filename:
  791. bpy.ops.preferences.studiolight_uninstall(index=studio_light.index)
  792. prefs.studio_lights.new(path=filepath_final)
  793. # print message
  794. msg = (
  795. tip_("StudioLight Installed %r into %r") %
  796. (self.filename, str(path_studiolights))
  797. )
  798. print(msg)
  799. self.report({'INFO'}, msg)
  800. return {'FINISHED'}
  801. def draw(self, _context):
  802. layout = self.layout
  803. if self.ask_overide:
  804. layout.label(text="Warning, file already exists. Overwrite existing file?")
  805. else:
  806. layout.prop(self, "filename")
  807. def invoke(self, context, _event):
  808. wm = context.window_manager
  809. return wm.invoke_props_dialog(self, width=600)
  810. class PREFERENCES_OT_studiolight_uninstall(Operator):
  811. """Delete Studio Light"""
  812. bl_idname = "preferences.studiolight_uninstall"
  813. bl_label = "Uninstall Studio Light"
  814. index: bpy.props.IntProperty()
  815. def execute(self, context):
  816. import os
  817. prefs = context.preferences
  818. for studio_light in prefs.studio_lights:
  819. if studio_light.index == self.index:
  820. for filepath in (
  821. studio_light.path,
  822. studio_light.path_irr_cache,
  823. studio_light.path_sh_cache,
  824. ):
  825. if filepath and os.path.exists(filepath):
  826. os.unlink(filepath)
  827. prefs.studio_lights.remove(studio_light)
  828. return {'FINISHED'}
  829. return {'CANCELLED'}
  830. class PREFERENCES_OT_studiolight_copy_settings(Operator):
  831. """Copy Studio Light settings to the Studio light editor"""
  832. bl_idname = "preferences.studiolight_copy_settings"
  833. bl_label = "Copy Studio Light settings"
  834. index: bpy.props.IntProperty()
  835. def execute(self, context):
  836. prefs = context.preferences
  837. system = prefs.system
  838. for studio_light in prefs.studio_lights:
  839. if studio_light.index == self.index:
  840. system.light_ambient = studio_light.light_ambient
  841. for sys_light, light in zip(system.solid_lights, studio_light.solid_lights):
  842. sys_light.use = light.use
  843. sys_light.diffuse_color = light.diffuse_color
  844. sys_light.specular_color = light.specular_color
  845. sys_light.smooth = light.smooth
  846. sys_light.direction = light.direction
  847. return {'FINISHED'}
  848. return {'CANCELLED'}
  849. class PREFERENCES_OT_studiolight_show(Operator):
  850. """Show light preferences"""
  851. bl_idname = "preferences.studiolight_show"
  852. bl_label = ""
  853. bl_options = {'INTERNAL'}
  854. def execute(self, context):
  855. context.preferences.active_section = 'LIGHTS'
  856. bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
  857. return {'FINISHED'}
  858. classes = (
  859. PREFERENCES_OT_addon_disable,
  860. PREFERENCES_OT_addon_enable,
  861. PREFERENCES_OT_addon_expand,
  862. PREFERENCES_OT_addon_install,
  863. PREFERENCES_OT_addon_refresh,
  864. PREFERENCES_OT_addon_remove,
  865. PREFERENCES_OT_addon_show,
  866. PREFERENCES_OT_app_template_install,
  867. PREFERENCES_OT_copy_prev,
  868. PREFERENCES_OT_keyconfig_activate,
  869. PREFERENCES_OT_keyconfig_export,
  870. PREFERENCES_OT_keyconfig_import,
  871. PREFERENCES_OT_keyconfig_remove,
  872. PREFERENCES_OT_keyconfig_test,
  873. PREFERENCES_OT_keyitem_add,
  874. PREFERENCES_OT_keyitem_remove,
  875. PREFERENCES_OT_keyitem_restore,
  876. PREFERENCES_OT_keymap_restore,
  877. PREFERENCES_OT_theme_install,
  878. PREFERENCES_OT_studiolight_install,
  879. PREFERENCES_OT_studiolight_new,
  880. PREFERENCES_OT_studiolight_uninstall,
  881. PREFERENCES_OT_studiolight_copy_settings,
  882. PREFERENCES_OT_studiolight_show,
  883. )