Files
test2/scripts/startup/bl_ui/space_userpref.py
Nika Kutsniashvili 7d2b024151 UI: Theme: Move curve handle properties in common
Continuing #140360, after 39c066ee53

Moving curve handle colors & size from space editors into common theme properties.

They are now shared by 3D Viewport, Image Editor, Graph Editor, and Movie Clip Editor,
instead of each of them having their own properties.

(Video in PR)

---

Details:
- NURB U/V lines, active spline, and last selected point colors were defined
in `rna_def_userdef_theme_spaces_curves` but were optional and only called by
3D viewport. Since no other editor calls that function anymore, I removed it and
moved those properties directly inside `rna_def_userdef_theme_space_view3d`.
- In Image Editor & Movie Clip Editor (Mask mode), curves don't ever show
selection colors, and Vector handles are generally never visible, that is
bug/missing in Blender in general, not a result of this PR.
- Handle vertex size were included in Dope Sheet and NLA, where handles don't
exist. Now as a side effect they're removed as well.

Pull Request: https://projects.blender.org/blender/blender/pulls/143762
2025-08-15 21:40:56 +02:00

3063 lines
102 KiB
Python

# SPDX-FileCopyrightText: 2009-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from bpy.types import (
Header,
Menu,
Panel,
UIList,
)
from bpy.app.translations import (
contexts as i18n_contexts,
pgettext_iface as iface_,
pgettext_rpt as rpt_,
)
from bl_ui.utils import PresetPanel
# -----------------------------------------------------------------------------
# Main Header
class USERPREF_HT_header(Header):
bl_space_type = 'PREFERENCES'
@staticmethod
def draw_buttons(layout, context):
prefs = context.preferences
layout.operator_context = 'EXEC_AREA'
if prefs.use_preferences_save and (not bpy.app.use_userpref_skip_save_on_exit):
pass
else:
# Show '*' to let users know the preferences have been modified.
layout.operator(
"wm.save_userpref",
text=iface_("Save Preferences") + (" *" if prefs.is_dirty else ""),
translate=False,
)
def draw(self, context):
layout = self.layout
layout.operator_context = 'EXEC_AREA'
layout.template_header()
USERPREF_MT_editor_menus.draw_collapsible(context, layout)
layout.separator_spacer()
self.draw_buttons(layout, context)
# -----------------------------------------------------------------------------
# Main Navigation Bar
class USERPREF_PT_navigation_bar(Panel):
bl_label = "Preferences Navigation"
bl_space_type = 'PREFERENCES'
bl_region_type = 'NAVIGATION_BAR'
bl_options = {'HIDE_HEADER'}
def draw(self, context):
layout = self.layout
prefs = context.preferences
col = layout.column()
col.scale_x = 1.3
col.scale_y = 1.3
col.prop(prefs, "active_section", expand=True)
class USERPREF_MT_editor_menus(Menu):
bl_idname = "USERPREF_MT_editor_menus"
bl_label = ""
def draw(self, _context):
layout = self.layout
layout.menu("USERPREF_MT_view")
layout.menu("USERPREF_MT_save_load", text="Preferences")
class USERPREF_MT_view(Menu):
bl_label = "View"
def draw(self, _context):
layout = self.layout
layout.menu("INFO_MT_area")
class USERPREF_MT_save_load(Menu):
bl_label = "Save & Load"
def draw(self, context):
layout = self.layout
prefs = context.preferences
row = layout.row()
row.active = not bpy.app.use_userpref_skip_save_on_exit
row.prop(prefs, "use_preferences_save", text="Auto-Save Preferences")
layout.separator()
layout.operator_context = 'EXEC_AREA'
if prefs.use_preferences_save:
layout.operator("wm.save_userpref", text="Save Preferences")
sub_revert = layout.column(align=True)
# NOTE: regarding `factory_startup`. To correctly show the active state of this menu item,
# the user preferences themselves would need to have a `factory_startup` state.
# Since showing an active menu item whenever factory-startup is used is not such a problem, leave this as-is.
sub_revert.active = prefs.is_dirty or bpy.app.factory_startup
sub_revert.operator("wm.read_userpref", text="Revert to Saved Preferences")
layout.operator_context = 'INVOKE_AREA'
app_template = prefs.app_template
if app_template:
display_name = bpy.path.display_name(iface_(app_template))
layout.operator("wm.read_factory_userpref", text="Load Factory Blender Preferences")
props = layout.operator(
"wm.read_factory_userpref",
text=iface_("Load Factory {:s} Preferences").format(display_name),
translate=False,
)
props.use_factory_startup_app_template_only = True
del display_name
else:
layout.operator("wm.read_factory_userpref", text="Load Factory Preferences")
class USERPREF_PT_save_preferences(Panel):
bl_label = "Save Preferences"
bl_space_type = 'PREFERENCES'
bl_region_type = 'EXECUTE'
bl_options = {'HIDE_HEADER'}
@classmethod
def poll(cls, context):
# Hide when header is visible
for region in context.area.regions:
if region.type == 'HEADER' and region.height <= 1:
return True
return False
def draw(self, context):
layout = self.layout.row()
layout.operator_context = 'EXEC_AREA'
layout.menu("USERPREF_MT_save_load", text="", icon='COLLAPSEMENU')
USERPREF_HT_header.draw_buttons(layout, context)
# -----------------------------------------------------------------------------
# Min-In Helpers
# Panel mix-in.
class CenterAlignMixIn:
"""
Base class for panels to center align contents with some horizontal margin.
Deriving classes need to implement a ``draw_centered(context, layout)`` function.
"""
def draw(self, context):
layout = self.layout
width = context.region.width
ui_scale = context.preferences.system.ui_scale
# No horizontal margin if region is rather small.
is_wide = width > (350 * ui_scale)
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
row = layout.row()
if is_wide:
row.label() # Needed so col below is centered.
col = row.column()
col.ui_units_x = 50
# Implemented by sub-classes.
self.draw_centered(context, col)
if is_wide:
row.label() # Needed so col above is centered.
# -----------------------------------------------------------------------------
# Interface Panels
class InterfacePanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "interface"
class USERPREF_PT_interface_display(InterfacePanel, CenterAlignMixIn, Panel):
bl_label = "Display"
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
col = layout.column()
col.prop(view, "ui_scale", text="Resolution Scale")
col.prop(view, "ui_line_width", text="Line Width")
col.prop(view, "show_splash", text="Splash Screen")
col.prop(view, "show_developer_ui")
col.separator()
col = layout.column(heading="Tooltips", align=True)
col.prop(view, "show_tooltips", text="User Tooltips")
sub = col.column()
sub.active = view.show_tooltips
sub.prop(view, "show_tooltips_python")
col.separator()
col = layout.column(heading="Search", align=True)
col.prop(prefs, "use_recent_searches", text="Sort by Most Recent")
class USERPREF_PT_interface_text(InterfacePanel, CenterAlignMixIn, Panel):
bl_label = "Text Rendering"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
flow.prop(view, "use_text_antialiasing", text="Anti-Aliasing")
sub = flow.column()
sub.active = view.use_text_antialiasing
sub.prop(view, "use_text_render_subpixelaa", text="Subpixel Anti-Aliasing")
sub.prop(view, "text_hinting", text="Hinting")
flow.prop(view, "font_path_ui")
flow.prop(view, "font_path_ui_mono")
class USERPREF_PT_interface_translation(InterfacePanel, CenterAlignMixIn, Panel):
bl_label = "Language"
bl_translation_context = i18n_contexts.id_windowmanager
@classmethod
def poll(cls, _context):
return bpy.app.build_options.international
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
layout.prop(view, "language")
col = layout.column(heading="Translate", heading_ctxt=i18n_contexts.editor_preferences)
col.active = (bpy.app.translations.locale != "en_US")
col.prop(view, "use_translate_tooltips", text="Tooltips")
col.prop(view, "use_translate_interface", text="Interface")
col.prop(view, "use_translate_reports", text="Reports")
col.prop(view, "use_translate_new_dataname", text="New Data")
class USERPREF_PT_interface_editors(InterfacePanel, CenterAlignMixIn, Panel):
bl_label = "Editors"
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
system = prefs.system
col = layout.column()
col.prop(system, "use_region_overlap")
col.prop(view, "show_navigate_ui")
col.prop(view, "border_width")
col.prop(view, "color_picker_type")
col.row().prop(view, "header_align")
col.prop(view, "factor_display_type")
class USERPREF_PT_interface_temporary_windows(InterfacePanel, CenterAlignMixIn, Panel):
bl_label = "Temporary Editors"
bl_parent_id = "USERPREF_PT_interface_editors"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
col = layout.column()
col.prop(view, "render_display_type", text="Render In")
col.prop(view, "filebrowser_display_type", text="File Browser")
col.prop(view, "preferences_display_type", text="Preferences")
class USERPREF_PT_interface_statusbar(InterfacePanel, CenterAlignMixIn, Panel):
bl_label = "Status Bar"
bl_parent_id = "USERPREF_PT_interface_editors"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
col = layout.column(heading="Show")
col.prop(view, "show_statusbar_stats", text="Scene Statistics")
col.prop(view, "show_statusbar_scene_duration", text="Scene Duration")
col.prop(view, "show_statusbar_memory", text="System Memory")
col.prop(view, "show_statusbar_vram", text="Video Memory")
col.prop(view, "show_extensions_updates", text="Extensions Updates")
col.prop(view, "show_statusbar_version", text="Blender Version")
class USERPREF_PT_interface_menus(InterfacePanel, Panel):
bl_label = "Menus"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
pass
class USERPREF_PT_interface_menus_mouse_over(InterfacePanel, CenterAlignMixIn, Panel):
bl_label = "Open on Mouse Over"
bl_parent_id = "USERPREF_PT_interface_menus"
def draw_header(self, context):
prefs = context.preferences
view = prefs.view
self.layout.prop(view, "use_mouse_over_open", text="")
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
layout.active = view.use_mouse_over_open
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
flow.prop(view, "open_toplevel_delay", text="Top Level")
flow.prop(view, "open_sublevel_delay", text="Sub Level")
class USERPREF_PT_interface_menus_pie(InterfacePanel, CenterAlignMixIn, Panel):
bl_label = "Pie Menus"
bl_parent_id = "USERPREF_PT_interface_menus"
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
flow.prop(view, "pie_animation_timeout")
flow.prop(view, "pie_tap_timeout")
flow.prop(view, "pie_initial_timeout")
flow.prop(view, "pie_menu_radius")
flow.prop(view, "pie_menu_threshold")
flow.prop(view, "pie_menu_confirm")
# -----------------------------------------------------------------------------
# Editing Panels
class EditingPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "editing"
class USERPREF_PT_edit_objects(EditingPanel, Panel):
bl_label = "Objects"
def draw(self, context):
pass
class USERPREF_PT_edit_objects_new(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "New Objects"
bl_parent_id = "USERPREF_PT_edit_objects"
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
flow.prop(edit, "material_link", text="Link Materials To")
flow.prop(edit, "object_align", text="Align To")
flow.prop(edit, "use_enter_edit_mode", text="Enter Edit Mode")
flow.prop(edit, "collection_instance_empty_size", text="Instance Empty Size")
class USERPREF_PT_edit_objects_duplicate_data(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "Copy on Duplicate"
bl_parent_id = "USERPREF_PT_edit_objects"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
layout.use_property_split = False
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=True)
datablock_types = (
("use_duplicate_action", "Action", 'ACTION', ""),
("use_duplicate_armature", "Armature", 'OUTLINER_DATA_ARMATURE', ""),
("use_duplicate_camera", "Camera", 'OUTLINER_DATA_CAMERA', ""),
("use_duplicate_curve", "Curve", 'OUTLINER_DATA_CURVE', ""),
("use_duplicate_curves", "Curves", 'OUTLINER_DATA_CURVES', ""),
("use_duplicate_grease_pencil", "Grease Pencil", 'OUTLINER_OB_GREASEPENCIL', ""),
("use_duplicate_lattice", "Lattice", 'OUTLINER_DATA_LATTICE', ""),
(None, None, None, None),
("use_duplicate_light", "Light", 'OUTLINER_DATA_LIGHT', ""),
("use_duplicate_lightprobe", "Light Probe", 'OUTLINER_DATA_LIGHTPROBE', ""),
("use_duplicate_material", "Material", 'MATERIAL_DATA', ""),
("use_duplicate_mesh", "Mesh", 'OUTLINER_DATA_MESH', ""),
("use_duplicate_metaball", "Metaball", 'OUTLINER_DATA_META', ""),
("use_duplicate_node_tree", "Node Tree", 'NODETREE', ""),
("use_duplicate_particle", "Particle", 'PARTICLES', ""),
(None, None, None, None),
("use_duplicate_pointcloud", "Point Cloud", 'OUTLINER_DATA_POINTCLOUD', ""),
("use_duplicate_speaker", "Speaker", 'OUTLINER_DATA_SPEAKER', ""),
("use_duplicate_surface", "Surface", 'OUTLINER_DATA_SURFACE', ""),
("use_duplicate_text", "Text", 'OUTLINER_DATA_FONT', ""),
("use_duplicate_volume", "Volume", 'OUTLINER_DATA_VOLUME', "i18n_contexts.id_id"),
)
col = flow.column()
for prop, type_name, type_icon, type_ctx in datablock_types:
if prop is None:
col = flow.column()
continue
row = col.row()
row_checkbox = row.row()
row_checkbox.prop(edit, prop, text="", text_ctxt=type_ctx)
row_label = row.row()
row_label.label(text=type_name, icon=type_icon)
class USERPREF_PT_edit_cursor(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "3D Cursor"
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
col = layout.column(heading="Cursor")
col.prop(edit, "use_mouse_depth_cursor", text="Surface Project")
col.prop(edit, "use_cursor_lock_adjust", text="Lock Adjust")
class USERPREF_PT_edit_gpencil(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "Grease Pencil"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
col = layout.column(heading="Distance")
col.prop(edit, "grease_pencil_manhattan_distance", text="Manhattan")
col.prop(edit, "grease_pencil_euclidean_distance", text="Euclidean")
class USERPREF_PT_edit_annotations(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "Annotations"
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
col = layout.column()
col.prop(edit, "grease_pencil_default_color", text="Default Color")
col.prop(edit, "grease_pencil_eraser_radius", text="Eraser Radius")
class USERPREF_PT_edit_weight_paint(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "Weight Paint"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
layout.use_property_split = False
layout.prop(view, "use_weight_color_range", text="Custom Gradient")
col = layout.column()
col.active = view.use_weight_color_range
col.template_color_ramp(view, "weight_color_range", expand=True)
class USERPREF_PT_edit_text_editor(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "Text Editor"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
layout.prop(edit, "use_text_edit_auto_close")
class USERPREF_PT_edit_node_editor(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "Node Editor"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
col = layout.column(heading="Auto-Offset")
row = col.row()
row.prop(edit, "node_use_insert_offset", text="")
subrow = row.row()
subrow.prop(edit, "node_margin", text="")
subrow.active = edit.node_use_insert_offset
layout.prop(edit, "node_preview_resolution", text="Preview Resolution")
class USERPREF_PT_edit_sequence_editor(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "Video Sequencer"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
layout.prop(edit, "use_sequencer_simplified_tweaking")
layout.prop(edit, "connect_strips_by_default")
class USERPREF_PT_edit_misc(EditingPanel, CenterAlignMixIn, Panel):
bl_label = "Miscellaneous"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
col = layout.column()
col.prop(edit, "sculpt_paint_overlay_color", text="Sculpt Overlay Color")
# -----------------------------------------------------------------------------
# Animation Panels
class AnimationPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "animation"
class USERPREF_PT_animation_timeline(AnimationPanel, CenterAlignMixIn, Panel):
bl_label = "Timeline"
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
edit = prefs.edit
col = layout.column()
col.prop(edit, "use_negative_frames")
col.prop(view, "view2d_grid_spacing_min", text="Minimum Grid Spacing")
col.prop(view, "timecode_style")
col.prop(view, "view_frame_type")
if view.view_frame_type == 'SECONDS':
col.prop(view, "view_frame_seconds")
elif view.view_frame_type == 'KEYFRAMES':
col.prop(view, "view_frame_keyframes")
class USERPREF_PT_animation_keyframes(AnimationPanel, CenterAlignMixIn, Panel):
bl_label = "Keyframes"
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
layout.prop(edit, "key_insert_channels", expand=True)
row = layout.row(align=True, heading="Only Insert Needed")
row.prop(edit, "use_keyframe_insert_needed", text="Manual", toggle=1)
row.prop(edit, "use_auto_keyframe_insert_needed", text="Auto", toggle=1)
col = layout.column(heading="Keyframing")
col.prop(edit, "use_visual_keying")
col = layout.column(heading="Auto-Keyframing")
col.prop(edit, "use_auto_keying", text="Enable in New Scenes")
col.prop(edit, "use_auto_keying_warning", text="Show Warning")
col.prop(edit, "use_keyframe_insert_available", text="Only Insert Available")
class USERPREF_PT_animation_fcurves(AnimationPanel, CenterAlignMixIn, Panel):
bl_label = "F-Curves"
def draw_centered(self, context, layout):
prefs = context.preferences
edit = prefs.edit
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
flow.prop(edit, "fcurve_unselected_alpha", text="Unselected Opacity")
flow.prop(edit, "fcurve_new_auto_smoothing", text="Default Smoothing Mode")
flow.prop(edit, "keyframe_new_interpolation_type", text="Default Interpolation")
flow.prop(edit, "keyframe_new_handle_type", text="Default Handles")
flow.prop(edit, "use_insertkey_xyz_to_rgb", text="XYZ to RGB")
flow.prop(edit, "use_anim_channel_group_colors")
flow.prop(edit, "show_only_selected_curve_keyframes")
flow.prop(edit, "use_fcurve_high_quality_drawing")
# -----------------------------------------------------------------------------
# System Panels
class SystemPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "system"
class USERPREF_PT_system_sound(SystemPanel, CenterAlignMixIn, Panel):
bl_label = "Sound"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
system = prefs.system
layout.prop(system, "audio_device", expand=False)
sub = layout.grid_flow(row_major=False, columns=0, even_columns=False, even_rows=False, align=False)
sub.active = system.audio_device not in {'NONE', 'None'}
sub.prop(system, "audio_channels", text="Channels")
sub.prop(system, "audio_mixing_buffer", text="Mixing Buffer")
sub.prop(system, "audio_sample_rate", text="Sample Rate")
sub.prop(system, "audio_sample_format", text="Sample Format")
class USERPREF_PT_system_cycles_devices(SystemPanel, CenterAlignMixIn, Panel):
bl_label = "Cycles Render Devices"
def draw_centered(self, context, layout):
prefs = context.preferences
col = layout.column()
col.use_property_split = False
if bpy.app.build_options.cycles:
addon = prefs.addons.get("cycles")
if addon is None:
layout.label(text="Enable Cycles Render Engine add-on to use Cycles", icon='INFO')
else:
addon.preferences.draw_impl(col, context)
del addon
else:
layout.label(text="Cycles is disabled in this build", icon='INFO')
class USERPREF_PT_system_display_graphics(SystemPanel, CenterAlignMixIn, Panel):
bl_label = "Display Graphics"
@classmethod
def poll(cls, _context):
import platform
return platform.system() != 'Darwin'
def draw_centered(self, context, layout):
prefs = context.preferences
system = prefs.system
import gpu
import sys
col = layout.column()
col.prop(system, "gpu_backend", text="Backend")
if system.gpu_backend == 'VULKAN':
col = layout.column()
col.enabled = gpu.platform.backend_type_get() == 'VULKAN'
col.prop(system, "gpu_preferred_device")
if system.gpu_backend != gpu.platform.backend_type_get():
layout.label(text="A restart of Blender is required", icon='INFO')
if system.gpu_backend == 'VULKAN':
col = layout.column()
col.label(text="Current Vulkan backend limitations:", icon='INFO')
col.label(text="\u2022 Low performance in VR", icon='BLANK1')
if sys.platform == "win32" and gpu.platform.device_type_get() == 'QUALCOMM':
col.label(text="\u2022 Windows on ARM requires driver 31.0.112.0 or higher", icon='BLANK1')
class USERPREF_PT_system_os_settings(SystemPanel, CenterAlignMixIn, Panel):
bl_label = "Operating System Settings"
@classmethod
def poll(cls, _context):
# macOS isn't supported.
from sys import platform
if platform == "darwin":
return False
return True
@staticmethod
def _draw_associate_supported_or_label(context, layout):
from sys import platform
if platform[:3] == "win":
if context.preferences.system.is_microsoft_store_install:
layout.label(text="Microsoft Store installation")
layout.label(text="Use Windows 'Default Apps' to associate with blend files")
return False
else:
# Linux.
if not bpy.app.portable:
layout.label(text="System Installation")
layout.label(text="File association is handled by the package manager")
return False
import os
if os.environ.get("SNAP"):
layout.label(text="Snap Package Installation")
layout.label(text="File association is handled by the package manager")
return False
return True
def draw_centered(self, context, layout):
if self._draw_associate_supported_or_label(context, layout):
layout.label(text="Open blend files with this Blender version")
split = layout.split(factor=0.5)
split.alignment = 'LEFT'
split.operator("preferences.associate_blend", text="Register")
split.operator("preferences.unassociate_blend", text="Unregister")
layout.prop(bpy.context.preferences.system, "register_all_users", text="For All Users")
class USERPREF_PT_system_network(SystemPanel, CenterAlignMixIn, Panel):
bl_label = "Network"
def draw_centered(self, context, layout):
prefs = context.preferences
system = prefs.system
row = layout.row()
row.prop(system, "use_online_access", text="Allow Online Access")
# Show when the preference has been overridden and doesn't match the current preference.
runtime_online_access = bpy.app.online_access
if system.use_online_access != runtime_online_access:
row = layout.split(factor=0.4)
row.label(text="")
if runtime_online_access:
text = iface_("Enabled on startup, overriding the preference.")
else:
text = iface_("Disabled on startup, overriding the preference.")
row.label(text=text, translate=False)
layout.row().prop(system, "network_timeout", text="Time Out")
layout.row().prop(system, "network_connection_limit", text="Connection Limit")
class USERPREF_PT_system_memory(SystemPanel, CenterAlignMixIn, Panel):
bl_label = "Memory & Limits"
def draw_centered(self, context, layout):
import sys
prefs = context.preferences
system = prefs.system
edit = prefs.edit
col = layout.column()
col.prop(edit, "undo_steps", text="Undo Steps")
col.prop(edit, "undo_memory_limit", text="Undo Memory Limit")
col.prop(edit, "use_global_undo")
layout.separator()
col = layout.column()
col.prop(system, "scrollback", text="Console Scrollback Lines")
layout.separator()
col = layout.column()
col.prop(system, "texture_time_out", text="Texture Time Out")
col.prop(system, "texture_collection_rate", text="Garbage Collection Rate")
layout.separator()
col = layout.column()
col.prop(system, "vbo_time_out", text="VBO Time Out")
col.prop(system, "vbo_collection_rate", text="Garbage Collection Rate")
if sys.platform != "darwin":
layout.separator()
col = layout.column(align=True)
col.active = system.gpu_backend != 'VULKAN'
col.row().prop(system, "shader_compilation_method", expand=True)
label = "Threads" if system.shader_compilation_method == 'THREAD' else "Subprocesses"
col.prop(system, "gpu_shader_workers", text=label)
class USERPREF_PT_system_video_sequencer(SystemPanel, CenterAlignMixIn, Panel):
bl_label = "Video Sequencer"
def draw_centered(self, context, layout):
prefs = context.preferences
system = prefs.system
layout.prop(system, "memory_cache_limit")
layout.separator()
layout.prop(system, "sequencer_proxy_setup")
# -----------------------------------------------------------------------------
# Viewport Panels
class ViewportPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "viewport"
class USERPREF_PT_viewport_display(ViewportPanel, CenterAlignMixIn, Panel):
bl_label = "Display"
def draw_centered(self, context, layout):
prefs = context.preferences
view = prefs.view
col = layout.column(heading="Text Info Overlay")
col.prop(view, "show_object_info", text="Object Info")
col.prop(view, "show_view_name", text="View Name")
col = layout.column(heading="Playback Frame Rate (FPS)")
row = col.row()
row.prop(view, "show_playback_fps", text="")
subrow = row.row()
subrow.active = view.show_playback_fps
subrow.prop(view, "playback_fps_samples", text="Samples")
layout.separator()
col = layout.column()
col.prop(view, "gizmo_size")
col.prop(view, "lookdev_sphere_size")
col.separator()
col.prop(view, "mini_axis_type", text="3D Viewport Axes")
if view.mini_axis_type == 'MINIMAL':
col.prop(view, "mini_axis_size", text="Size")
col.prop(view, "mini_axis_brightness", text="Brightness")
if view.mini_axis_type == 'GIZMO':
col.prop(view, "gizmo_size_navigate_v3d", text="Size")
layout.separator()
col = layout.column(heading="Fresnel")
col.prop(view, "use_fresnel_edit")
class USERPREF_PT_viewport_quality(ViewportPanel, CenterAlignMixIn, Panel):
bl_label = "Quality"
def draw_centered(self, context, layout):
prefs = context.preferences
system = prefs.system
col = layout.column()
col.prop(system, "viewport_aa")
col = layout.column(heading="Smooth Wires")
col.prop(system, "use_overlay_smooth_wire", text="Overlay")
col.prop(system, "use_edit_mode_smooth_wire", text="Edit Mode")
class USERPREF_PT_viewport_textures(ViewportPanel, CenterAlignMixIn, Panel):
bl_label = "Textures"
def draw_centered(self, context, layout):
prefs = context.preferences
system = prefs.system
col = layout.column()
col.prop(system, "gl_texture_limit", text="Limit Size")
col.prop(system, "anisotropic_filter")
col.prop(system, "gl_clip_alpha", slider=True)
col.prop(system, "image_draw_method", text="Image Display Method")
class USERPREF_PT_viewport_subdivision(ViewportPanel, CenterAlignMixIn, Panel):
bl_label = "Subdivision"
bl_options = {'DEFAULT_CLOSED'}
def draw_centered(self, context, layout):
prefs = context.preferences
system = prefs.system
layout.prop(system, "use_gpu_subdivision")
# -----------------------------------------------------------------------------
# Theme Panels
class ThemePanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "themes"
class USERPREF_MT_interface_theme_presets(Menu):
# NOTE: this label is currently not used, see: !134844.
bl_label = "Presets"
preset_subdir = "interface_theme"
preset_operator = "script.execute_preset"
preset_type = 'XML'
preset_xml_map = (
("preferences.themes[0]", "Theme"),
("preferences.ui_styles[0]", "ThemeStyle"),
)
# Prevent untrusted XML files "escaping" from these types.
preset_xml_secure_types = {
"Theme",
"ThemeAssetShelf",
"ThemeBoneColorSet",
"ThemeClipEditor",
"ThemeCollectionColor",
"ThemeCommon",
"ThemeCommonAnim",
"ThemeCommonCurves",
"ThemeConsole",
"ThemeDopeSheet",
"ThemeFileBrowser",
"ThemeFontStyle",
"ThemeGradientColors",
"ThemeGraphEditor",
"ThemeImageEditor",
"ThemeInfo",
"ThemeNLAEditor",
"ThemeNodeEditor",
"ThemeOutliner",
"ThemePreferences",
"ThemeProperties",
"ThemeSequenceEditor",
"ThemeSpaceGeneric",
"ThemeSpaceGradient",
"ThemeSpaceListGeneric",
"ThemeSpaceRegionGeneric",
"ThemeSpreadsheet",
"ThemeStatusBar",
"ThemeStripColor",
"ThemeStyle",
"ThemeTextEditor",
"ThemeTopBar",
"ThemeUserInterface",
"ThemeView3D",
"ThemeWidgetColors",
"ThemeWidgetStateColors",
}
draw = Menu.draw_preset
@staticmethod
def reset_cb(_context, _filepath):
bpy.ops.preferences.reset_default_theme()
@staticmethod
def post_cb(context, filepath):
context.preferences.themes[0].filepath = filepath
class USERPREF_PT_theme(ThemePanel, Panel):
bl_label = "Themes"
bl_options = {'HIDE_HEADER'}
def draw(self, context):
import os
layout = self.layout
split = layout.split(factor=0.6)
row = split.row(align=True)
# Unlike most presets (which use the classes bl_label),
# themes store the path, use this when set.
if filepath := context.preferences.themes[0].filepath:
preset_label = bpy.path.display_name(os.path.basename(filepath))
else:
# If the `filepath` is empty, assume the theme was reset and use the default theme name as a label.
# This would typically be:
# `preset_label = USERPREF_MT_interface_theme_presets.bl_label`
# However the operator to reset the preferences doesn't clear the value,
# so it's simplest to hard-code "Presets" here.
preset_label = "Presets"
row.menu("USERPREF_MT_interface_theme_presets", text=preset_label)
del filepath, preset_label
row.operator("wm.interface_theme_preset_add", text="", icon='ADD')
row.operator("wm.interface_theme_preset_remove", text="", icon='REMOVE')
row.operator("wm.interface_theme_preset_save", text="", icon='FILE_TICK')
row = split.row(align=True)
row.operator("preferences.theme_install", text="Install...", icon='IMPORT')
row.operator("preferences.reset_default_theme", text="Reset", icon='LOOP_BACK')
class USERPREF_PT_theme_user_interface(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "User Interface"
bl_options = {'DEFAULT_CLOSED'}
def draw_header(self, _context):
layout = self.layout
layout.label(icon='WORKSPACE')
def draw(self, context):
pass
# Base class for dynamically defined widget color panels.
# This is not registered.
class PreferenceThemeWidgetColorPanel:
bl_parent_id = "USERPREF_PT_theme_user_interface"
def draw(self, context):
theme = context.preferences.themes[0]
ui = theme.user_interface
widget_style = getattr(ui, self.wcol)
layout = self.layout
layout.use_property_split = True
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
col = flow.column(align=True)
col.prop(widget_style, "text")
col.prop(widget_style, "text_sel", text="Selected")
col.prop(widget_style, "item", slider=True)
col = flow.column(align=True)
col.prop(widget_style, "inner", slider=True)
col.prop(widget_style, "inner_sel", text="Selected", slider=True)
col = flow.column(align=True)
col.prop(widget_style, "outline")
col.prop(widget_style, "outline_sel", text="Selected", slider=True)
col.separator()
col.prop(widget_style, "roundness")
# Base class for dynamically defined widget color panels.
# This is not registered.
class PreferenceThemeWidgetShadePanel:
def draw(self, context):
theme = context.preferences.themes[0]
ui = theme.user_interface
widget_style = getattr(ui, self.wcol)
layout = self.layout
layout.use_property_split = True
col = layout.column(align=True)
col.active = widget_style.show_shaded
col.prop(widget_style, "shadetop", text="Shade Top")
col.prop(widget_style, "shadedown", text="Down")
def draw_header(self, context):
theme = context.preferences.themes[0]
ui = theme.user_interface
widget_style = getattr(ui, self.wcol)
self.layout.prop(widget_style, "show_shaded", text="")
class USERPREF_PT_theme_interface_panel(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "Panel"
bl_options = {'DEFAULT_CLOSED'}
bl_parent_id = "USERPREF_PT_theme_user_interface"
def draw_centered(self, context, layout):
theme = context.preferences.themes[0]
ui = theme.user_interface
flow = layout.grid_flow(row_major=False, columns=2, even_columns=True, even_rows=False, align=False)
col = flow.column()
col.prop(ui, "panel_header", text="Header")
col = col.column(align=True)
col.prop(ui, "panel_back", text="Background")
col.prop(ui, "panel_sub_back", text="Sub-Panel")
col = flow.column(align=True)
col.prop(ui, "panel_title", text="Title")
col.prop(ui, "panel_text", text="Text")
col = col.column()
col.prop(ui, "panel_outline", text="Outline")
col.prop(ui, "panel_roundness", text="Roundness")
class USERPREF_PT_theme_interface_state(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "State"
bl_options = {'DEFAULT_CLOSED'}
bl_parent_id = "USERPREF_PT_theme_user_interface"
def draw_centered(self, context, layout):
theme = context.preferences.themes[0]
ui_state = theme.user_interface.wcol_state
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
col = flow.column(align=True)
col.prop(ui_state, "error")
col.prop(ui_state, "warning")
col.prop(ui_state, "info")
col.prop(ui_state, "success")
col = flow.column(align=True)
col.prop(ui_state, "inner_anim")
col.prop(ui_state, "inner_anim_sel", text="Selected")
col = flow.column(align=True)
col.prop(ui_state, "inner_driven")
col.prop(ui_state, "inner_driven_sel", text="Selected")
col = flow.column(align=True)
col.prop(ui_state, "inner_key")
col.prop(ui_state, "inner_key_sel", text="Selected")
col = flow.column(align=True)
col.prop(ui_state, "inner_overridden")
col.prop(ui_state, "inner_overridden_sel", text="Selected")
col = flow.column(align=True)
col.prop(ui_state, "inner_changed")
col.prop(ui_state, "inner_changed_sel", text="Selected")
col = flow.column(align=True)
col.prop(ui_state, "blend")
class USERPREF_PT_theme_interface_styles(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "Styles"
bl_options = {'DEFAULT_CLOSED'}
bl_parent_id = "USERPREF_PT_theme_user_interface"
def draw_centered(self, context, layout):
theme = context.preferences.themes[0]
ui = theme.user_interface
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
col = flow.column(align=True)
col.prop(ui, "editor_border")
col.prop(ui, "editor_outline")
col.prop(ui, "editor_outline_active")
col = flow.column()
col.prop(ui, "widget_text_cursor")
col = flow.column(align=True)
col.prop(ui, "icon_alpha")
col.prop(ui, "icon_saturation", text="Saturation")
flow.separator()
col = flow.column()
col.prop(ui, "widget_emboss")
col = flow.column(align=True)
col.prop(ui, "menu_shadow_fac")
col.prop(ui, "menu_shadow_width", text="Shadow Width")
class USERPREF_PT_theme_interface_transparent_checker(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "Transparent Checkerboard"
bl_options = {'DEFAULT_CLOSED'}
bl_parent_id = "USERPREF_PT_theme_user_interface"
def draw_centered(self, context, layout):
theme = context.preferences.themes[0]
ui = theme.user_interface
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
col = flow.column(align=True)
col.prop(ui, "transparent_checker_primary")
col.prop(ui, "transparent_checker_secondary")
col = flow.column()
col.prop(ui, "transparent_checker_size")
class USERPREF_PT_theme_interface_gizmos(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "Axis & Gizmo Colors"
bl_options = {'DEFAULT_CLOSED'}
bl_parent_id = "USERPREF_PT_theme_user_interface"
def draw_centered(self, context, layout):
theme = context.preferences.themes[0]
ui = theme.user_interface
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=True, align=False)
col = flow.column(align=True)
col.prop(ui, "axis_x", text="Axis X")
col.prop(ui, "axis_y", text="Y")
col.prop(ui, "axis_z", text="Z")
col.prop(ui, "axis_w", text="W")
col = flow.column()
col.prop(ui, "gizmo_primary")
col.prop(ui, "gizmo_secondary", text="Secondary")
col.prop(ui, "gizmo_view_align", text="View Align")
col = flow.column()
col.prop(ui, "gizmo_a")
col.prop(ui, "gizmo_b", text="B")
class USERPREF_PT_theme_interface_icons(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "Icon Colors"
bl_options = {'DEFAULT_CLOSED'}
bl_parent_id = "USERPREF_PT_theme_user_interface"
def draw_centered(self, context, layout):
theme = context.preferences.themes[0]
ui = theme.user_interface
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
flow.prop(ui, "icon_scene")
flow.prop(ui, "icon_collection")
flow.prop(ui, "icon_object")
flow.prop(ui, "icon_object_data")
flow.prop(ui, "icon_modifier")
flow.prop(ui, "icon_shading")
flow.prop(ui, "icon_folder")
flow.prop(ui, "icon_autokey")
flow.prop(ui, "icon_border_intensity")
class USERPREF_PT_theme_text_style(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "Text Style"
bl_options = {'DEFAULT_CLOSED'}
@staticmethod
def _ui_font_style(layout, font_style):
layout.use_property_split = True
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
col = flow.column()
col.prop(font_style, "points")
col.prop(font_style, "character_weight", text="Weight", text_ctxt=i18n_contexts.id_text)
col = flow.column(align=True)
col.prop(font_style, "shadow_offset_x", text="Shadow Offset X")
col.prop(font_style, "shadow_offset_y", text="Y")
col = flow.column(align=True)
col.prop(font_style, "shadow")
col.prop(font_style, "shadow_alpha", text="Alpha")
col.prop(font_style, "shadow_value", text="Brightness")
def draw_header(self, _context):
layout = self.layout
layout.label(icon='FONTPREVIEW')
def draw_centered(self, context, layout):
style = context.preferences.ui_styles[0]
layout.label(text="Panel Title")
self._ui_font_style(layout, style.panel_title)
layout.separator()
layout.label(text="Widget")
self._ui_font_style(layout, style.widget)
layout.separator()
layout.label(text="Tooltip")
self._ui_font_style(layout, style.tooltip)
class USERPREF_PT_theme_bone_color_sets(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "Bone Color Sets"
bl_options = {'DEFAULT_CLOSED'}
def draw_header(self, _context):
layout = self.layout
layout.label(icon='COLOR')
def draw_centered(self, context, layout):
theme = context.preferences.themes[0]
layout.use_property_split = True
for i, ui in enumerate(theme.bone_color_sets, 1):
layout.label(text=iface_("Color Set {:d}").format(i), translate=False)
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=True)
flow.prop(ui, "normal")
flow.prop(ui, "select", text="Selected")
flow.prop(ui, "active")
flow.prop(ui, "show_colored_constraints")
class USERPREF_PT_theme_collection_colors(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "Collection Colors"
bl_options = {'DEFAULT_CLOSED'}
def draw_header(self, _context):
layout = self.layout
layout.label(icon='OUTLINER_COLLECTION')
def draw_centered(self, context, layout):
theme = context.preferences.themes[0]
layout.use_property_split = True
flow = layout.grid_flow(row_major=False, columns=2, even_columns=True, even_rows=False, align=False)
for i, ui in enumerate(theme.collection_color, 1):
flow.prop(ui, "color", text=iface_("Color {:d}").format(i), translate=False)
class USERPREF_PT_theme_strip_colors(ThemePanel, CenterAlignMixIn, Panel):
bl_label = "Strip Color Tags"
bl_options = {'DEFAULT_CLOSED'}
def draw_header(self, _context):
layout = self.layout
layout.label(icon='SEQ_STRIP_DUPLICATE')
def draw_centered(self, context, layout):
theme = context.preferences.themes[0]
layout.use_property_split = True
flow = layout.grid_flow(row_major=False, columns=2, even_columns=True, even_rows=False, align=False)
for i, ui in enumerate(theme.strip_color, 1):
flow.prop(ui, "color", text=iface_("Color {:d}").format(i), translate=False)
# Base class for dynamically defined theme-space panels.
# This is not registered.
class PreferenceThemeSpacePanel:
@staticmethod
def _theme_generic(layout, themedata):
layout.use_property_split = True
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
props_type = {}
for prop in themedata.rna_type.properties:
if prop.identifier == "rna_type":
continue
props_type.setdefault((prop.type, prop.subtype), []).append(prop)
for props_type, props_ls in sorted(props_type.items()):
if props_type[0] == 'POINTER':
continue
for prop in props_ls:
flow.prop(themedata, prop.identifier)
def draw_header(self, _context):
icon = getattr(self, "icon", 'NONE')
if icon != 'NONE':
layout = self.layout
layout.label(icon=icon)
def draw(self, context):
layout = self.layout
theme = context.preferences.themes[0]
datapath_list = self.datapath.split(".")
data = theme
for datapath_item in datapath_list:
data = getattr(data, datapath_item)
PreferenceThemeSpacePanel._theme_generic(layout, data)
class ThemeGenericClassGenerator:
@staticmethod
def generate_panel_classes_for_wcols():
wcols = [
("Box", "wcol_box"),
("List Item", "wcol_list_item"),
("Menu", "wcol_menu"),
("Menu Background", "wcol_menu_back"),
("Menu Item", "wcol_menu_item"),
("Number Field", "wcol_num"),
("Option", "wcol_option"),
("Pie Menu", "wcol_pie_menu"),
("Progress Bar", "wcol_progress"),
("Pulldown", "wcol_pulldown"),
("Radio Buttons", "wcol_radio"),
("Regular", "wcol_regular"),
("Scroll Bar", "wcol_scroll"),
("Tab", "wcol_tab"),
("Text", "wcol_text"),
("Toggle", "wcol_toggle"),
("Tool", "wcol_tool"),
("Toolbar Item", "wcol_toolbar_item"),
("Tooltip", "wcol_tooltip"),
("Value Slider", "wcol_numslider"),
]
for (name, wcol) in wcols:
panel_id = "USERPREF_PT_theme_interface_" + wcol
yield type(panel_id, (PreferenceThemeWidgetColorPanel, ThemePanel, Panel), {
"bl_label": name,
"bl_options": {'DEFAULT_CLOSED'},
"draw": PreferenceThemeWidgetColorPanel.draw,
"wcol": wcol,
})
panel_shade_id = "USERPREF_PT_theme_interface_shade_" + wcol
yield type(panel_shade_id, (PreferenceThemeWidgetShadePanel, ThemePanel, Panel), {
"bl_label": "Shaded",
"bl_options": {'DEFAULT_CLOSED'},
"bl_parent_id": panel_id,
"draw": PreferenceThemeWidgetShadePanel.draw,
"wcol": wcol,
})
@staticmethod
def generate_theme_area_child_panel_classes(parent_id, rna_type, theme_area, datapath):
def generate_child_panel_classes_recurse(parent_id, rna_type, theme_area, datapath):
props_type = {}
for prop in rna_type.properties:
if prop.identifier == "rna_type":
continue
props_type.setdefault((prop.type, prop.subtype), []).append(prop)
for props_type, props_ls in sorted(props_type.items()):
if props_type[0] == 'POINTER':
for prop in props_ls:
new_datapath = datapath + "." + prop.identifier if datapath else prop.identifier
panel_id = parent_id + "_" + prop.identifier
yield type(panel_id, (PreferenceThemeSpacePanel, ThemePanel, Panel), {
"bl_label": rna_type.properties[prop.identifier].name,
"bl_parent_id": parent_id,
"bl_options": {'DEFAULT_CLOSED'},
"draw": PreferenceThemeSpacePanel.draw,
"theme_area": theme_area.identifier,
"datapath": new_datapath,
})
yield from generate_child_panel_classes_recurse(
panel_id,
prop.fixed_type,
theme_area,
new_datapath,
)
yield from generate_child_panel_classes_recurse(parent_id, rna_type, theme_area, datapath)
@staticmethod
def generate_panel_classes_from_theme_areas():
from bpy.types import Theme
for theme_area in Theme.bl_rna.properties["theme_area"].enum_items_static:
if theme_area.identifier in {'USER_INTERFACE', 'STYLE', 'BONE_COLOR_SETS'}:
continue
panel_id = "USERPREF_PT_theme_" + theme_area.identifier.lower()
# Generate panel-class from theme_area
yield type(panel_id, (PreferenceThemeSpacePanel, ThemePanel, Panel), {
"bl_label": theme_area.name,
"bl_options": {'DEFAULT_CLOSED'},
"draw_header": PreferenceThemeSpacePanel.draw_header,
"draw": PreferenceThemeSpacePanel.draw,
"theme_area": theme_area.identifier,
"icon": theme_area.icon,
"datapath": theme_area.identifier.lower(),
})
yield from ThemeGenericClassGenerator.generate_theme_area_child_panel_classes(
panel_id, Theme.bl_rna.properties[theme_area.identifier.lower()].fixed_type,
theme_area, theme_area.identifier.lower())
# -----------------------------------------------------------------------------
# File Paths Panels
# Panel mix-in.
class FilePathsPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "file_paths"
class USERPREF_PT_file_paths_data(FilePathsPanel, Panel):
bl_label = "Data"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
paths = context.preferences.filepaths
col = self.layout.column()
col.prop(paths, "font_directory", text="Fonts")
col.prop(paths, "texture_directory", text="Textures")
col.prop(paths, "sound_directory", text="Sounds")
col.prop(paths, "temporary_directory", text="Temporary Files")
class USERPREF_PT_file_paths_script_directories(FilePathsPanel, Panel):
bl_label = "Script Directories"
def draw(self, context):
layout = self.layout
paths = context.preferences.filepaths
if len(paths.script_directories) == 0:
layout.operator("preferences.script_directory_add", text="Add", icon='ADD')
return
layout.use_property_split = False
layout.use_property_decorate = False
box = layout.box()
split = box.split(factor=0.35)
name_col = split.column()
path_col = split.column()
row = name_col.row(align=True) # Padding
row.separator()
row.label(text="Name")
row = path_col.row(align=True) # Padding
row.separator()
row.label(text="Path", text_ctxt=i18n_contexts.editor_filebrowser)
row.operator("preferences.script_directory_add", text="", icon='ADD', emboss=False)
for i, script_directory in enumerate(paths.script_directories):
row = name_col.row()
row.alert = not script_directory.name
row.prop(script_directory, "name", text="")
row = path_col.row()
subrow = row.row()
subrow.alert = not script_directory.directory
subrow.prop(script_directory, "directory", text="")
row.operator("preferences.script_directory_remove", text="", icon='X', emboss=False).index = i
class USERPREF_PT_file_paths_render(FilePathsPanel, Panel):
bl_label = "Render"
bl_parent_id = "USERPREF_PT_file_paths_data"
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
paths = context.preferences.filepaths
col = self.layout.column()
col.prop(paths, "render_output_directory", text="Render Output")
col.prop(paths, "render_cache_directory", text="Render Cache")
class USERPREF_PT_text_editor_presets(PresetPanel, Panel):
bl_label = "Text Editor Presets"
preset_subdir = "text_editor"
preset_operator = "script.execute_preset"
preset_add_operator = "text_editor.preset_add"
class USERPREF_PT_file_paths_applications(FilePathsPanel, Panel):
bl_label = "Applications"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
paths = context.preferences.filepaths
col = layout.column()
col.prop(paths, "image_editor", text="Image Editor")
col.prop(paths, "animation_player_preset", text="Animation Player")
if paths.animation_player_preset == 'CUSTOM':
col.prop(paths, "animation_player", text="Player")
class USERPREF_PT_text_editor(FilePathsPanel, Panel):
bl_label = "Text Editor"
bl_parent_id = "USERPREF_PT_file_paths_applications"
def draw_header_preset(self, _context):
USERPREF_PT_text_editor_presets.draw_panel_header(self.layout)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
paths = context.preferences.filepaths
col = layout.column()
col.prop(paths, "text_editor", text="Program")
col.prop(paths, "text_editor_args", text="Arguments")
class USERPREF_PT_file_paths_development(FilePathsPanel, Panel):
bl_label = "Development"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
prefs = context.preferences
return prefs.view.show_developer_ui
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
paths = context.preferences.filepaths
layout.prop(paths, "i18n_branches_directory", text="I18n Branches")
class USERPREF_PT_saveload_autorun(FilePathsPanel, Panel):
bl_label = "Auto Run Python Scripts"
bl_parent_id = "USERPREF_PT_saveload_blend"
def draw_header(self, context):
prefs = context.preferences
paths = prefs.filepaths
self.layout.prop(paths, "use_scripts_auto_execute", text="")
def draw(self, context):
layout = self.layout
prefs = context.preferences
paths = prefs.filepaths
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
layout.active = paths.use_scripts_auto_execute
box = layout.box()
row = box.row()
row.label(text="Excluded Paths")
row.operator("preferences.autoexec_path_add", text="", icon='ADD', emboss=False)
for i, path_cmp in enumerate(prefs.autoexec_paths):
row = box.row()
row.prop(path_cmp, "path", text="")
row.prop(path_cmp, "use_glob", text="", icon='FILTER')
row.operator("preferences.autoexec_path_remove", text="", icon='X', emboss=False).index = i
class USERPREF_PT_file_paths_asset_libraries(FilePathsPanel, Panel):
bl_label = "Asset Libraries"
def draw(self, context):
layout = self.layout
layout.use_property_split = False
layout.use_property_decorate = False
paths = context.preferences.filepaths
active_library_index = paths.active_asset_library
row = layout.row()
row.template_list(
"USERPREF_UL_asset_libraries", "user_asset_libraries",
paths, "asset_libraries",
paths, "active_asset_library",
)
col = row.column(align=True)
col.operator("preferences.asset_library_add", text="", icon='ADD')
props = col.operator("preferences.asset_library_remove", text="", icon='REMOVE')
props.index = active_library_index
try:
active_library = None if active_library_index < 0 else paths.asset_libraries[active_library_index]
except IndexError:
active_library = None
if active_library is None:
return
layout.separator()
layout.prop(active_library, "path")
layout.prop(active_library, "import_method", text="Import Method")
layout.prop(active_library, "use_relative_path")
class USERPREF_UL_asset_libraries(UIList):
def draw_item(self, _context, layout, _data, item, _icon, _active_data, _active_propname, _index):
asset_library = item
layout.prop(asset_library, "name", text="", emboss=False)
class USERPREF_UL_extension_repos(UIList):
def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index):
repo = item
icon = 'INTERNET' if repo.use_remote_url else 'DISK_DRIVE'
layout.prop(repo, "name", text="", icon=icon, emboss=False)
# Show an error icon if this repository has unusable settings.
if repo.enabled:
if (
(repo.use_custom_directory and repo.custom_directory == "") or
(repo.use_remote_url and repo.remote_url == "")
):
layout.label(text="", icon='ERROR')
layout.prop(repo, "enabled", text="", emboss=False, icon='CHECKBOX_HLT' if repo.enabled else 'CHECKBOX_DEHLT')
def filter_items(self, _context, data, propname):
# Repositories has no index, converting to a list.
items = list(getattr(data, propname))
flags = [self.bitflag_filter_item] * len(items)
indices = [None] * len(items)
for index, orig_index in enumerate(sorted(
range(len(items)),
key=lambda i: (
# Order [Remote, User, System].
0 if (repo := items[i]).use_remote_url else (1 if (repo.source != 'SYSTEM') else 2),
repo.name.casefold(),
)
)):
indices[orig_index] = index
return flags, indices
# -----------------------------------------------------------------------------
# Save/Load Panels
class SaveLoadPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "save_load"
class USERPREF_PT_saveload_blend(SaveLoadPanel, CenterAlignMixIn, Panel):
bl_label = "Blend Files"
def draw_centered(self, context, layout):
prefs = context.preferences
paths = prefs.filepaths
view = prefs.view
col = layout.column(heading="Save")
col.prop(view, "use_save_prompt")
col = layout.column()
col.prop(paths, "save_version")
col.prop(paths, "recent_files")
layout.separator()
col = layout.column(heading="Auto-Save")
row = col.row()
row.prop(paths, "use_auto_save_temporary_files", text="")
subrow = row.row()
subrow.active = paths.use_auto_save_temporary_files
subrow.prop(paths, "auto_save_time", text="Timer (Minutes)")
layout.separator()
layout.prop(paths, "file_preview_type")
layout.separator()
layout.separator()
col = layout.column(heading="Default To")
col.prop(paths, "use_relative_paths")
col.prop(paths, "use_file_compression")
col.prop(paths, "use_load_ui")
col = layout.column(heading="Text Files")
col.prop(paths, "use_tabs_as_spaces")
class USERPREF_PT_saveload_file_browser(SaveLoadPanel, CenterAlignMixIn, Panel):
bl_label = "File Browser"
def draw_centered(self, context, layout):
prefs = context.preferences
paths = prefs.filepaths
col = layout.column(heading="Show Locations")
col.prop(paths, "show_recent_locations", text="Recent")
col.prop(paths, "show_system_bookmarks", text="System")
col = layout.column(heading="Defaults")
col.prop(paths, "use_filter_files")
col.prop(paths, "show_hidden_files_datablocks")
# -----------------------------------------------------------------------------
# Input Panels
class InputPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "input"
class USERPREF_PT_input_keyboard(InputPanel, CenterAlignMixIn, Panel):
bl_label = "Keyboard"
def draw_centered(self, context, layout):
prefs = context.preferences
inputs = prefs.inputs
layout.prop(inputs, "use_emulate_numpad")
layout.prop(inputs, "use_numeric_input_advanced")
class USERPREF_PT_input_mouse(InputPanel, CenterAlignMixIn, Panel):
bl_label = "Mouse"
def draw_centered(self, context, layout):
import sys
prefs = context.preferences
inputs = prefs.inputs
flow = layout.grid_flow(row_major=False, columns=0, even_columns=True, even_rows=False, align=False)
if sys.platform[:3] == "win":
flow.prop(inputs, "use_mouse_emulate_3_button")
else:
col = flow.column(heading="Emulate 3 Button Mouse")
row = col.row()
row.prop(inputs, "use_mouse_emulate_3_button", text="")
subrow = row.row()
subrow.prop(inputs, "mouse_emulate_3_button_modifier", text="")
subrow.active = inputs.use_mouse_emulate_3_button
flow.prop(inputs, "use_mouse_continuous")
flow.prop(inputs, "use_drag_immediately")
flow.prop(inputs, "mouse_double_click_time", text="Double Click Speed")
flow.prop(inputs, "drag_threshold_mouse")
flow.prop(inputs, "drag_threshold_tablet")
flow.prop(inputs, "drag_threshold")
flow.prop(inputs, "move_threshold")
class USERPREF_PT_input_touchpad(InputPanel, CenterAlignMixIn, Panel):
bl_label = "Touchpad"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
import sys
if sys.platform[:3] == "win" or sys.platform == "darwin":
return True
# WAYLAND supports multi-touch, X11 and SDL don't.
from _bpy import _ghost_backend
if _ghost_backend() == 'WAYLAND':
return True
return False
def draw_centered(self, context, layout):
prefs = context.preferences
inputs = prefs.inputs
col = layout.column()
col.prop(inputs, "use_multitouch_gestures")
from _bpy import _wm_capabilities
capabilities = _wm_capabilities()
if not capabilities['TRACKPAD_PHYSICAL_DIRECTION']:
row = col.row()
row.active = inputs.use_multitouch_gestures
row.prop(inputs, "touchpad_scroll_direction", text="Scroll Direction")
class USERPREF_PT_input_tablet(InputPanel, CenterAlignMixIn, Panel):
bl_label = "Tablet"
def draw_centered(self, context, layout):
prefs = context.preferences
inputs = prefs.inputs
import sys
if sys.platform[:3] == "win":
layout.prop(inputs, "tablet_api")
layout.separator()
col = layout.column()
col.prop(inputs, "pressure_threshold_max")
col.prop(inputs, "pressure_softness")
class USERPREF_PT_input_ndof(InputPanel, CenterAlignMixIn, Panel):
bl_label = "NDOF"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return bpy.app.build_options.input_ndof
def draw_centered(self, context, layout):
prefs = context.preferences
inputs = prefs.inputs
USERPREF_PT_ndof_settings.draw_settings(layout, inputs)
# -----------------------------------------------------------------------------
# Navigation Panels
class NavigationPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "navigation"
class USERPREF_PT_navigation_orbit(NavigationPanel, CenterAlignMixIn, Panel):
bl_label = "Orbit & Pan"
def draw_centered(self, context, layout):
prefs = context.preferences
inputs = prefs.inputs
view = prefs.view
col = layout.column()
col.row().prop(inputs, "view_rotate_method", expand=True)
if inputs.view_rotate_method == 'TURNTABLE':
col.prop(inputs, "view_rotate_sensitivity_turntable")
else:
col.prop(inputs, "view_rotate_sensitivity_trackball")
col.prop(inputs, "use_rotate_around_active")
col.separator()
col = layout.column(heading="Auto")
col.prop(inputs, "use_auto_perspective", text="Perspective")
col.prop(inputs, "use_mouse_depth_navigate", text="Depth")
col = layout.column()
col.prop(view, "smooth_view")
col.prop(view, "rotation_angle")
class USERPREF_PT_navigation_zoom(NavigationPanel, CenterAlignMixIn, Panel):
bl_label = "Zoom"
def draw_centered(self, context, layout):
prefs = context.preferences
inputs = prefs.inputs
col = layout.column()
col.row().prop(inputs, "view_zoom_method", text="Zoom Method")
if inputs.view_zoom_method in {'DOLLY', 'CONTINUE'}:
col.row().prop(inputs, "view_zoom_axis")
col.prop(inputs, "use_zoom_to_mouse")
col = layout.column(heading="Invert Zoom Direction", align=True)
col.prop(inputs, "invert_mouse_zoom", text="Mouse")
col.prop(inputs, "invert_zoom_wheel", text="Wheel")
else:
col.prop(inputs, "use_zoom_to_mouse")
col.prop(inputs, "invert_zoom_wheel", text="Invert Wheel Zoom Direction")
class USERPREF_PT_navigation_fly_walk(NavigationPanel, CenterAlignMixIn, Panel):
bl_label = "Fly & Walk"
def draw_centered(self, context, layout):
prefs = context.preferences
inputs = prefs.inputs
layout.row().prop(inputs, "navigation_mode", expand=True)
class USERPREF_PT_navigation_fly_walk_navigation(NavigationPanel, CenterAlignMixIn, Panel):
bl_label = "Walk"
bl_parent_id = "USERPREF_PT_navigation_fly_walk"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
prefs = context.preferences
return prefs.inputs.navigation_mode == 'WALK'
def draw_centered(self, context, layout):
prefs = context.preferences
inputs = prefs.inputs
walk = inputs.walk_navigation
col = layout.column()
col.prop(walk, "use_mouse_reverse")
col.prop(walk, "mouse_speed")
col.prop(walk, "teleport_time")
col = layout.column(align=True)
col.prop(walk, "walk_speed")
col.prop(walk, "walk_speed_factor")
class USERPREF_PT_navigation_fly_walk_gravity(NavigationPanel, CenterAlignMixIn, Panel):
bl_label = "Gravity"
bl_parent_id = "USERPREF_PT_navigation_fly_walk"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
prefs = context.preferences
return prefs.inputs.navigation_mode == 'WALK'
def draw_header(self, context):
prefs = context.preferences
inputs = prefs.inputs
walk = inputs.walk_navigation
self.layout.prop(walk, "use_gravity", text="")
def draw_centered(self, context, layout):
prefs = context.preferences
inputs = prefs.inputs
walk = inputs.walk_navigation
layout.active = walk.use_gravity
col = layout.column()
col.prop(walk, "view_height")
col.prop(walk, "jump_height")
# Special case, this is only exposed as a popover.
class USERPREF_PT_ndof_settings(Panel):
bl_label = "3D Mouse Settings"
bl_space_type = 'TOPBAR' # dummy.
bl_region_type = 'HEADER'
bl_ui_units_x = 12
@staticmethod
def draw_settings(layout, props, show_3dview_settings=True):
layout.separator()
if show_3dview_settings:
col = layout.column()
col.row().prop(props, "ndof_navigation_mode", text="Navigation Mode")
col.prop(props, "ndof_lock_horizon", text="Lock Horizon")
layout.separator()
if show_3dview_settings:
col = layout.column(heading="Orbit Center")
col.active = props.ndof_navigation_mode == 'OBJECT'
col.prop(props, "ndof_orbit_center_auto")
colsub = col.column()
colsub.active = props.ndof_orbit_center_auto
colsub.prop(props, "ndof_orbit_center_selected")
del colsub
col.separator()
col = layout.column(heading="Show")
col.prop(props, "ndof_show_guide_orbit_axis", text="Orbit Axis")
colsub = col.column()
colsub.active = props.ndof_navigation_mode == 'OBJECT'
colsub.prop(props, "ndof_show_guide_orbit_center", text="Orbit Center")
del colsub
layout.separator()
layout_header, layout_advanced = layout.panel("NDOF_advanced", default_closed=True)
layout_header.label(text="Advanced")
if layout_advanced:
col = layout_advanced.column()
col.prop(props, "ndof_translation_sensitivity")
col.prop(props, "ndof_rotation_sensitivity")
col.prop(props, "ndof_deadzone")
col.separator()
col.row().prop(props, "ndof_zoom_direction", expand=True)
col.separator()
row = col.row(heading=("Invert Pan" if show_3dview_settings else "Invert Pan Axis"))
for text, attr in (
("X", "ndof_panx_invert_axis"),
("Y", "ndof_pany_invert_axis"),
("Z", "ndof_panz_invert_axis"),
):
row.prop(props, attr, text=text, toggle=True)
if show_3dview_settings:
row = col.row(heading="Invert Rotate")
for text, attr in (
("X", "ndof_rotx_invert_axis"),
("Y", "ndof_roty_invert_axis"),
("Z", "ndof_rotz_invert_axis"),
):
row.prop(props, attr, text=text, toggle=True)
if show_3dview_settings:
col.prop(props, "ndof_lock_camera_pan_zoom")
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False # No animation.
input_prefs = context.preferences.inputs
is_view3d = context.space_data.type == 'VIEW_3D'
self.draw_settings(layout, input_prefs, is_view3d)
# -----------------------------------------------------------------------------
# Key-Map Editor Panels
class KeymapPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "keymap"
class USERPREF_MT_keyconfigs(Menu):
bl_label = "KeyPresets"
preset_subdir = "keyconfig"
preset_operator = "preferences.keyconfig_activate"
def draw(self, context):
Menu.draw_preset(self, context)
class USERPREF_PT_keymap(KeymapPanel, Panel):
bl_label = "Keymap"
bl_options = {'HIDE_HEADER'}
def draw(self, context):
from rna_keymap_ui import draw_keymaps
layout = self.layout
# import time
# start = time.time()
# Keymap Settings
draw_keymaps(context, layout)
# print("runtime", time.time() - start)
# -----------------------------------------------------------------------------
# Extension Panels
class USERPREF_MT_extensions_active_repo(Menu):
bl_label = "Active Repository"
def draw(self, _context):
# Add-ons may extend.
pass
class USERPREF_MT_extensions_active_repo_remove(Menu):
bl_label = "Remove Extension Repository"
def draw(self, context):
layout = self.layout
extensions = context.preferences.extensions
active_repo_index = extensions.active_repo
try:
active_repo = None if active_repo_index < 0 else extensions.repos[active_repo_index]
except IndexError:
active_repo = None
is_system_repo = (active_repo.use_remote_url is False) and (active_repo.source == 'SYSTEM')
props = layout.operator("preferences.extension_repo_remove", text="Remove Repository")
props.index = active_repo_index
if not is_system_repo:
props = layout.operator("preferences.extension_repo_remove", text="Remove Repository & Files")
props.index = active_repo_index
props.remove_files = True
class USERPREF_PT_extensions_repos(Panel):
bl_label = "Repositories"
bl_options = {'HIDE_HEADER'}
bl_space_type = 'TOPBAR' # dummy.
bl_region_type = 'HEADER'
# Show wider than most panels so the URL & directory aren't overly clipped.
bl_ui_units_x = 16
def draw(self, context):
layout = self.layout
layout.use_property_split = False
layout.use_property_decorate = False
extensions = context.preferences.extensions
active_repo_index = extensions.active_repo
row = layout.row()
row.template_list(
"USERPREF_UL_extension_repos", "user_extension_repos",
extensions, "repos",
extensions, "active_repo",
)
col = row.column(align=True)
col.operator_menu_enum("preferences.extension_repo_add", "type", text="", icon='ADD')
col.menu("USERPREF_MT_extensions_active_repo_remove", text="", icon='REMOVE')
col.separator()
col.menu_contents("USERPREF_MT_extensions_active_repo")
try:
active_repo = None if active_repo_index < 0 else extensions.repos[active_repo_index]
except IndexError:
active_repo = None
if active_repo is None:
return
# NOTE: changing repositories from remote to local & vice versa could be supported but is obscure enough
# that it can be hidden entirely. If there is a some justification to show this, it can be exposed.
# For now it can be accessed from Python if someone is.
# `layout.prop(active_repo, "use_remote_url", text="Use Remote URL")`
use_remote_url = active_repo.use_remote_url
if use_remote_url:
row = layout.row()
split = row.split(factor=0.936)
if active_repo.remote_url == "":
split.alert = True
split.prop(active_repo, "remote_url", text="", icon='INTERNET', placeholder="Repository URL")
split = row.split()
if active_repo.use_access_token:
access_token_icon = 'LOCKED' if active_repo.access_token else 'UNLOCKED'
row = layout.row()
split = row.split(factor=0.936)
split.prop(active_repo, "access_token", icon=access_token_icon)
split = row.split()
layout.prop(active_repo, "use_sync_on_startup")
layout_header, layout_panel = layout.panel("advanced", default_closed=True)
layout_header.label(text="Advanced")
if layout_panel:
layout_panel.use_property_split = True
use_custom_directory = active_repo.use_custom_directory
col = layout_panel.column(align=False, heading="Custom Directory")
row = col.row(align=True)
sub = row.row(align=True)
sub.prop(active_repo, "use_custom_directory", text="")
sub = sub.row(align=True)
sub.active = use_custom_directory
if use_custom_directory:
if active_repo.custom_directory == "":
sub.alert = True
sub.prop(active_repo, "custom_directory", text="")
else:
# Show the read-only directory property.
# Apart from being consistent with the custom directory UI,
# prefer a read-only property over a label because this is not necessarily
# valid UTF-8 which will raise a Python exception when passed in as text.
sub.prop(active_repo, "directory", text="")
if use_remote_url:
row = layout_panel.row(align=True, heading="Authentication")
row.prop(active_repo, "use_access_token")
layout_panel.prop(active_repo, "use_cache")
else:
layout_panel.prop(active_repo, "source")
layout_panel.separator()
layout_panel.prop(active_repo, "module")
# -----------------------------------------------------------------------------
# Extensions Panels
class ExtensionsPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "extensions"
class USERPREF_PT_extensions(ExtensionsPanel, Panel):
bl_label = "Extensions"
bl_options = {'HIDE_HEADER'}
def draw(self, context):
pass
# -----------------------------------------------------------------------------
# Add-on Panels
# Only a popover.
class USERPREF_PT_addons_filter(Panel):
bl_label = "Add-ons Filter"
bl_space_type = 'TOPBAR' # dummy.
bl_region_type = 'HEADER'
bl_ui_units_x = 12
def draw(self, context):
USERPREF_PT_addons._draw_addon_header_for_extensions_popover(self.layout, context)
class AddOnPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "addons"
class USERPREF_PT_addons(AddOnPanel, Panel):
bl_label = "Add-ons"
bl_options = {'HIDE_HEADER'}
_support_icon_mapping = {
'OFFICIAL': 'BLENDER',
'COMMUNITY': 'COMMUNITY',
'TESTING': 'EXPERIMENTAL',
}
@staticmethod
def is_user_addon(mod, user_addon_paths):
import os
if not user_addon_paths:
for path in (
bpy.utils.script_path_user(),
*bpy.utils.script_paths_pref(),
):
if path is not None:
user_addon_paths.append(os.path.join(path, "addons"))
for path in user_addon_paths:
if bpy.path.is_subdir(mod.__file__, path):
return True
return False
@staticmethod
def draw_addon_preferences(layout, context, addon_preferences):
if (draw := getattr(addon_preferences, "draw", None)) is None:
return
addon_preferences_class = type(addon_preferences)
layout.label(text=" Preferences")
box_prefs = layout.box()
addon_preferences_class.layout = box_prefs
try:
draw(context)
except Exception:
import traceback
traceback.print_exc()
box_prefs.label(text="Error (see console)", icon='ERROR')
del addon_preferences_class.layout
@staticmethod
def draw_error(layout, message):
lines = message.split("\n")
box = layout.box()
sub = box.row()
sub.label(text=lines[0])
sub.label(icon='ERROR')
for line in lines[1:]:
box.label(text=line)
@staticmethod
def _draw_addon_header(layout, prefs, wm):
split = layout.split(factor=0.6)
row = split.row()
row.prop(wm, "addon_support", expand=True)
row = split.row(align=True)
row.operator("preferences.addon_install", icon='IMPORT', text="Install...")
row.operator("preferences.addon_refresh", icon='FILE_REFRESH', text="Refresh")
row = layout.row()
row.prop(prefs.view, "show_addons_enabled_only")
row.prop(wm, "addon_filter", text="")
row.prop(wm, "addon_search", text="", icon='VIEWZOOM')
@staticmethod
def _draw_addon_header_for_extensions_popover(layout, context):
wm = context.window_manager
prefs = context.preferences
row = layout.row()
row.prop(wm, "addon_support", expand=True)
row = layout.row()
row.prop(prefs.view, "show_addons_enabled_only")
# Not filter, we could expose elsewhere.
row = layout.row()
row.operator("preferences.addon_install", icon='IMPORT', text="Install...")
row.operator("preferences.addon_refresh", icon='FILE_REFRESH', text="Refresh")
def draw(self, context):
import os
import addon_utils
prefs = context.preferences
if self.is_extended():
# Rely on the draw function being extended by the extensions add-on (`bl_pkg`).
return
layout = self.layout
wm = context.window_manager
used_addon_module_name_map = {addon.module: addon for addon in prefs.addons}
addon_user_dirs = tuple(
p for p in (
*[os.path.join(pref_p, "addons") for pref_p in bpy.utils.script_paths_pref()],
bpy.utils.user_resource('SCRIPTS', path="addons"),
)
if p
)
self._draw_addon_header(layout, prefs, wm)
layout_topmost = layout.column()
col = layout.column()
# set in addon_utils.modules_refresh()
if addon_utils.error_duplicates:
box = col.box()
row = box.row()
row.label(text="Multiple add-ons with the same name found!")
row.label(icon='ERROR')
box.label(text="Delete one of each pair to resolve:")
for (addon_name, addon_file, addon_path) in addon_utils.error_duplicates:
box.separator()
sub_col = box.column(align=True)
sub_col.label(text=addon_name + ":")
sub_row = sub_col.row()
sub_row.label(text=" " + addon_file)
sub_row.operator("wm.path_open", text="", icon='FILE_FOLDER').filepath = os.path.dirname(addon_file)
sub_row = sub_col.row()
sub_row.label(text=" " + addon_path)
sub_row.operator("wm.path_open", text="", icon='FILE_FOLDER').filepath = os.path.dirname(addon_path)
if addon_utils.error_encoding:
self.draw_error(
col,
"One or more addons do not have UTF-8 encoding\n"
"(see console for details)",
)
show_enabled_only = prefs.view.show_addons_enabled_only
filter = wm.addon_filter
search = wm.addon_search.casefold()
support = wm.addon_support
module_names = set()
# initialized on demand
user_addon_paths = []
for mod in addon_utils.modules(refresh=False):
module_names.add(addon_module_name := mod.__name__)
bl_info = addon_utils.module_bl_info(mod)
is_enabled = addon_module_name in used_addon_module_name_map
if bl_info["support"] not in support:
continue
# check if addon should be visible with current filters
is_visible = (
(filter == "All") or
(filter == bl_info["category"]) or
(filter == "User" and (mod.__file__.startswith(addon_user_dirs)))
)
if show_enabled_only:
is_visible = is_visible and is_enabled
if not is_visible:
continue
if search and not (
(search in bl_info["name"].casefold() or
search in iface_(bl_info["name"]).casefold()) or
(bl_info["author"] and (search in bl_info["author"].casefold())) or
((filter == "All") and (search in bl_info["category"].casefold() or
search in iface_(bl_info["category"]).casefold()))
):
continue
# Addon UI Code
col_box = col.column()
box = col_box.box()
colsub = box.column()
row = colsub.row(align=True)
row.operator(
"preferences.addon_expand",
icon='DISCLOSURE_TRI_DOWN' if bl_info["show_expanded"] else 'DISCLOSURE_TRI_RIGHT',
emboss=False,
).module = addon_module_name
row.operator(
"preferences.addon_disable" if is_enabled else "preferences.addon_enable",
icon='CHECKBOX_HLT' if is_enabled else 'CHECKBOX_DEHLT', text="",
emboss=False,
).module = addon_module_name
sub = row.row()
sub.active = is_enabled
sub.label(text="{:s}: {:s}".format(iface_(bl_info["category"]), iface_(bl_info["name"])))
if bl_info["warning"]:
sub.label(icon='ERROR')
# icon showing support level.
sub.label(icon=self._support_icon_mapping.get(bl_info["support"], 'QUESTION'))
# Expanded UI (only if additional bl_info is available)
if bl_info["show_expanded"]:
if value := bl_info["description"]:
split = colsub.row().split(factor=0.15)
split.label(text="Description:")
split.label(text=iface_(value))
if value := bl_info["location"]:
split = colsub.row().split(factor=0.15)
split.label(text="Location:")
split.label(text=iface_(value))
if mod:
split = colsub.row().split(factor=0.15)
split.label(text="File:")
split.label(text=mod.__file__, translate=False)
if value := bl_info["author"]:
split = colsub.row().split(factor=0.15)
split.label(text="Author:")
split.label(text=value, translate=False)
if value := bl_info["version"]:
split = colsub.row().split(factor=0.15)
split.label(text="Version:")
split.label(text=".".join(str(x) for x in value), translate=False)
if value := bl_info["warning"]:
split = colsub.row().split(factor=0.15)
split.label(text="Warning:")
split.label(text=" " + iface_(value), icon='ERROR')
del value
user_addon = USERPREF_PT_addons.is_user_addon(mod, user_addon_paths)
if bl_info["doc_url"] or bl_info.get("tracker_url"):
split = colsub.row().split(factor=0.15)
split.label(text="Internet:")
sub = split.row()
if bl_info["doc_url"]:
sub.operator(
"wm.url_open", text="Documentation", icon='HELP',
).url = bl_info["doc_url"]
# Only add "Report a Bug" button if tracker_url is set.
# None of the core add-ons are expected to have tracker info (glTF is the exception).
if bl_info.get("tracker_url"):
sub.operator(
"wm.url_open", text="Report a Bug", icon='URL',
).url = bl_info["tracker_url"]
if user_addon:
split = colsub.row().split(factor=0.15)
split.label(text="User:")
split.operator(
"preferences.addon_remove", text="Remove", icon='CANCEL',
).module = mod.__name__
# Show addon user preferences
if is_enabled:
if (addon_preferences := used_addon_module_name_map[addon_module_name].preferences) is not None:
self.draw_addon_preferences(col_box, context, addon_preferences)
if filter in {"All", "Enabled"}:
# Append missing scripts
# First collect scripts that are used but have no script file.
missing_modules = {
addon_module_name for addon_module_name in used_addon_module_name_map
if addon_module_name not in module_names
}
if missing_modules:
layout_topmost.column().separator()
layout_topmost.column().label(text="Missing script files")
for addon_module_name in sorted(missing_modules):
is_enabled = addon_module_name in used_addon_module_name_map
# Addon UI Code
box = layout_topmost.column().box()
colsub = box.column()
row = colsub.row(align=True)
row.label(text="", icon='ERROR')
if is_enabled:
row.operator(
"preferences.addon_disable", icon='CHECKBOX_HLT', text="", emboss=False,
).module = addon_module_name
row.label(text=addon_module_name, translate=False)
# -----------------------------------------------------------------------------
# Studio Light Panels
class StudioLightPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "lights"
class StudioLightPanelMixin:
def _get_lights(self, prefs):
return [light for light in prefs.studio_lights if light.is_user_defined and light.type == self.sl_type]
def draw(self, context):
layout = self.layout
prefs = context.preferences
lights = self._get_lights(prefs)
self.draw_light_list(layout, lights)
def draw_light_list(self, layout, lights):
if lights:
flow = layout.grid_flow(row_major=False, columns=4, even_columns=True, even_rows=True, align=False)
for studio_light in lights:
self.draw_studio_light(flow, studio_light)
else:
layout.label(text=self.get_error_message())
def get_error_message(self):
return rpt_("No custom {:s} configured").format(self.bl_label)
def draw_studio_light(self, layout, studio_light):
box = layout.box()
row = box.row()
row.template_icon(layout.icon(studio_light), scale=3.0)
col = row.column()
props = col.operator("preferences.studiolight_uninstall", text="", icon='REMOVE')
props.index = studio_light.index
if studio_light.type == 'STUDIO':
props = col.operator("preferences.studiolight_copy_settings", text="", icon='IMPORT')
props.index = studio_light.index
box.label(text=studio_light.name)
class USERPREF_PT_studiolight_matcaps(StudioLightPanel, StudioLightPanelMixin, Panel):
bl_label = "MatCaps"
sl_type = 'MATCAP'
def draw_header_preset(self, _context):
layout = self.layout
layout.operator("preferences.studiolight_install", icon='IMPORT', text="Install...").type = 'MATCAP'
layout.separator()
def get_error_message(self):
return rpt_("No custom MatCaps configured")
class USERPREF_PT_studiolight_world(StudioLightPanel, StudioLightPanelMixin, Panel):
bl_label = "HDRIs"
sl_type = 'WORLD'
def draw_header_preset(self, _context):
layout = self.layout
layout.operator("preferences.studiolight_install", icon='IMPORT', text="Install...").type = 'WORLD'
layout.separator()
def get_error_message(self):
return rpt_("No custom HDRIs configured")
class USERPREF_PT_studiolight_lights(StudioLightPanel, StudioLightPanelMixin, Panel):
bl_label = "Studio Lights"
sl_type = 'STUDIO'
def draw_header_preset(self, _context):
layout = self.layout
props = layout.operator("preferences.studiolight_install", icon='IMPORT', text="Install...")
props.type = 'STUDIO'
props.filter_glob = ".sl"
layout.separator()
def get_error_message(self):
return rpt_("No custom Studio Lights configured")
class USERPREF_PT_studiolight_light_editor(StudioLightPanel, Panel):
bl_label = "Editor"
bl_parent_id = "USERPREF_PT_studiolight_lights"
bl_options = {'DEFAULT_CLOSED'}
@staticmethod
def opengl_light_buttons(layout, light):
col = layout.column()
box = col.box()
box.active = light.use
box.prop(light, "use", text="Use Light")
box.prop(light, "diffuse_color", text="Diffuse")
box.prop(light, "specular_color", text="Specular")
box.prop(light, "smooth")
box.prop(light, "direction")
col.separator()
def draw(self, context):
layout = self.layout
prefs = context.preferences
system = prefs.system
row = layout.row()
row.prop(system, "use_studio_light_edit", toggle=True)
row.operator("preferences.studiolight_new", text="Save as Studio light", icon='FILE_TICK')
layout.separator()
layout.use_property_split = True
flow = layout.grid_flow(row_major=True, columns=2, even_rows=True, even_columns=True)
flow.active = system.use_studio_light_edit
for light in system.solid_lights:
self.opengl_light_buttons(flow, light)
layout.prop(system, "light_ambient")
# -----------------------------------------------------------------------------
# Experimental Panels
class ExperimentalPanel:
bl_space_type = 'PREFERENCES'
bl_region_type = 'WINDOW'
bl_context = "experimental"
url_prefix = "https://projects.blender.org/"
@classmethod
def poll(cls, _context):
return bpy.app.version_cycle == "alpha"
def _draw_items(self, context, items):
prefs = context.preferences
experimental = prefs.experimental
layout = self.layout
layout.use_property_split = False
layout.use_property_decorate = False
for prop_keywords, reference in items:
split = layout.split(factor=0.66)
col = split.split()
col.prop(experimental, **prop_keywords)
if reference:
if type(reference) is tuple:
url_ext = reference[0]
text = reference[1]
else:
url_ext = reference
text = reference
col = split.split()
col.operator("wm.url_open", text=text, icon='URL').url = self.url_prefix + url_ext
"""
# Example panel, leave it here so we always have a template to follow even
# after the features are gone from the experimental panel.
class USERPREF_PT_experimental_virtual_reality(ExperimentalPanel, Panel):
bl_label = "Virtual Reality"
def draw(self, context):
self._draw_items(
context, (
({"property": "use_virtual_reality_scene_inspection"}, ("blender/blender/issues/71347", "#71347")),
({"property": "use_virtual_reality_immersive_drawing"}, ("blender/blender/issues/71348", "#71348")),
),
)
"""
class USERPREF_PT_experimental_new_features(ExperimentalPanel, Panel):
bl_label = "New Features"
def draw(self, context):
self._draw_items(
context, (
({"property": "use_extended_asset_browser"},
("blender/blender/projects/10", "Pipeline, Assets & IO Project Page")),
({"property": "use_new_volume_nodes"}, ("blender/blender/issues/103248", "#103248")),
({"property": "use_shader_node_previews"}, ("blender/blender/issues/110353", "#110353")),
({"property": "use_geometry_nodes_lists"}, ("blender/blender/issues/140918", "#140918")),
),
)
class USERPREF_PT_experimental_prototypes(ExperimentalPanel, Panel):
bl_label = "Prototypes"
def draw(self, context):
self._draw_items(
context, (
({"property": "use_new_curves_tools"}, ("blender/blender/issues/68981", "#68981")),
({"property": "use_sculpt_texture_paint"}, ("blender/blender/issues/96225", "#96225")),
({"property": "write_legacy_blend_file_format"}, ("/blender/blender/issues/129309", "#129309")),
),
)
# Keep this as tweaks can be useful to restore.
"""
class USERPREF_PT_experimental_tweaks(ExperimentalPanel, Panel):
bl_label = "Tweaks"
def draw(self, context):
self._draw_items(
context, (
({"property": "use_select_nearest_on_first_click"}, ("blender/blender/issues/96752", "#96752")),
),
)
"""
class USERPREF_PT_experimental_debugging(ExperimentalPanel, Panel):
bl_label = "Debugging"
@classmethod
def poll(cls, _context):
# Unlike the other experimental panels, the debugging one is always visible
# even in beta or release.
return True
def draw(self, context):
self._draw_items(
context, (
({"property": "use_undo_legacy"}, ("blender/blender/issues/60695", "#60695")),
({"property": "override_auto_resync"}, ("blender/blender/issues/83811", "#83811")),
({"property": "use_all_linked_data_direct"}, None),
({"property": "use_recompute_usercount_on_save_debug"}, None),
({"property": "use_cycles_debug"}, None),
({"property": "show_asset_debug_info"}, None),
({"property": "use_asset_indexing"}, None),
({"property": "use_viewport_debug"}, None),
({"property": "use_eevee_debug"}, None),
({"property": "use_extensions_debug"}, ("/blender/blender/issues/119521", "#119521")),
),
)
# -----------------------------------------------------------------------------
# Class Registration
# Order of registration defines order in UI,
# so dynamically generated classes are "injected" in the intended order.
classes = (
USERPREF_PT_theme_user_interface,
*ThemeGenericClassGenerator.generate_panel_classes_for_wcols(),
USERPREF_HT_header,
USERPREF_PT_navigation_bar,
USERPREF_PT_save_preferences,
USERPREF_MT_editor_menus,
USERPREF_MT_view,
USERPREF_MT_save_load,
USERPREF_PT_interface_display,
USERPREF_PT_interface_editors,
USERPREF_PT_interface_temporary_windows,
USERPREF_PT_interface_statusbar,
USERPREF_PT_interface_translation,
USERPREF_PT_interface_text,
USERPREF_PT_interface_menus,
USERPREF_PT_interface_menus_mouse_over,
USERPREF_PT_interface_menus_pie,
USERPREF_PT_viewport_display,
USERPREF_PT_viewport_quality,
USERPREF_PT_viewport_textures,
USERPREF_PT_viewport_subdivision,
USERPREF_PT_edit_objects,
USERPREF_PT_edit_objects_new,
USERPREF_PT_edit_objects_duplicate_data,
USERPREF_PT_edit_cursor,
USERPREF_PT_edit_annotations,
USERPREF_PT_edit_weight_paint,
USERPREF_PT_edit_gpencil,
USERPREF_PT_edit_text_editor,
USERPREF_PT_edit_node_editor,
USERPREF_PT_edit_sequence_editor,
USERPREF_PT_edit_misc,
USERPREF_PT_animation_timeline,
USERPREF_PT_animation_keyframes,
USERPREF_PT_animation_fcurves,
USERPREF_PT_system_cycles_devices,
USERPREF_PT_system_display_graphics,
USERPREF_PT_system_os_settings,
USERPREF_PT_system_network,
USERPREF_PT_system_memory,
USERPREF_PT_system_video_sequencer,
USERPREF_PT_system_sound,
USERPREF_MT_interface_theme_presets,
USERPREF_PT_theme,
USERPREF_PT_theme_interface_panel,
USERPREF_PT_theme_interface_gizmos,
USERPREF_PT_theme_interface_icons,
USERPREF_PT_theme_interface_state,
USERPREF_PT_theme_interface_styles,
USERPREF_PT_theme_interface_transparent_checker,
USERPREF_PT_theme_text_style,
USERPREF_PT_theme_bone_color_sets,
USERPREF_PT_theme_collection_colors,
USERPREF_PT_theme_strip_colors,
USERPREF_PT_file_paths_data,
USERPREF_PT_file_paths_render,
USERPREF_PT_file_paths_asset_libraries,
USERPREF_PT_file_paths_script_directories,
USERPREF_PT_file_paths_applications,
USERPREF_PT_text_editor,
USERPREF_PT_text_editor_presets,
USERPREF_PT_file_paths_development,
USERPREF_PT_saveload_blend,
USERPREF_PT_saveload_autorun,
USERPREF_PT_saveload_file_browser,
USERPREF_MT_keyconfigs,
USERPREF_PT_input_keyboard,
USERPREF_PT_input_mouse,
USERPREF_PT_input_tablet,
USERPREF_PT_input_touchpad,
USERPREF_PT_input_ndof,
USERPREF_PT_navigation_orbit,
USERPREF_PT_navigation_zoom,
USERPREF_PT_navigation_fly_walk,
USERPREF_PT_navigation_fly_walk_navigation,
USERPREF_PT_navigation_fly_walk_gravity,
USERPREF_PT_keymap,
USERPREF_PT_extensions,
USERPREF_PT_addons,
USERPREF_MT_extensions_active_repo,
USERPREF_MT_extensions_active_repo_remove,
USERPREF_PT_extensions_repos,
USERPREF_PT_studiolight_lights,
USERPREF_PT_studiolight_light_editor,
USERPREF_PT_studiolight_matcaps,
USERPREF_PT_studiolight_world,
# Popovers.
USERPREF_PT_ndof_settings,
USERPREF_PT_addons_filter,
USERPREF_PT_experimental_new_features,
USERPREF_PT_experimental_prototypes,
# USERPREF_PT_experimental_tweaks,
USERPREF_PT_experimental_debugging,
# UI lists
USERPREF_UL_asset_libraries,
USERPREF_UL_extension_repos,
# Add dynamically generated editor theme panels last,
# so they show up last in the theme section.
*ThemeGenericClassGenerator.generate_panel_classes_from_theme_areas(),
)
if __name__ == "__main__": # only for live edit.
from bpy.utils import register_class
for cls in classes:
register_class(cls)