Relative imports are intentionally avoided in UI code (`scripts/startup/bl_ui`) because it makes it impossible to run individual files in isolation. Common workflow for making mock-ups and quick UI edits is to load the UI file in Blender's text editor with "Edit Source" operator, make changes, and run the script to see the changes. But in the current version it's impossible because files include relative imports, which don't allow Python to run scripts individually. Pull Request: https://projects.blender.org/blender/blender/pulls/138246
541 lines
18 KiB
Python
541 lines
18 KiB
Python
# SPDX-FileCopyrightText: 2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
import bpy
|
|
from bpy.types import Panel, Menu, UIList
|
|
from rna_prop_ui import PropertyPanel
|
|
from bl_ui.space_properties import PropertiesAnimationMixin
|
|
|
|
|
|
class DataButtonsPanel:
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "data"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return hasattr(context, "grease_pencil") and context.grease_pencil
|
|
|
|
|
|
class LayerDataButtonsPanel:
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "data"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
grease_pencil = context.grease_pencil
|
|
return grease_pencil and grease_pencil.layers.active
|
|
|
|
|
|
class GREASE_PENCIL_UL_masks(UIList):
|
|
def draw_item(self, _context, layout, _data, item, icon, _active_data, _active_propname, _index):
|
|
mask = item
|
|
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
|
row = layout.row(align=True)
|
|
row.prop(mask, "name", text="", emboss=False, icon_value=icon)
|
|
row.prop(mask, "invert", text="", emboss=False)
|
|
row.prop(mask, "hide", text="", emboss=False)
|
|
elif self.layout_type == 'GRID':
|
|
layout.alignment = 'CENTER'
|
|
layout.prop(mask, "name", text="", emboss=False, icon_value=icon)
|
|
|
|
|
|
class GreasePencil_LayerMaskPanel:
|
|
def draw_header(self, context):
|
|
grease_pencil = context.grease_pencil
|
|
layer = grease_pencil.layers.active
|
|
|
|
self.layout.prop(layer, "use_masks", text="", toggle=0)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
grease_pencil = context.grease_pencil
|
|
layer = grease_pencil.layers.active
|
|
|
|
layout = self.layout
|
|
layout.enabled = layer.use_masks
|
|
|
|
if not layer:
|
|
return
|
|
|
|
rows = 4
|
|
row = layout.row()
|
|
col = row.column()
|
|
col.template_list(
|
|
"GREASE_PENCIL_UL_masks", "", layer, "mask_layers", layer.mask_layers,
|
|
"active_mask_index", rows=rows, sort_lock=True,
|
|
)
|
|
|
|
col = row.column(align=True)
|
|
col.menu("GREASE_PENCIL_MT_layer_mask_add", icon='ADD', text="")
|
|
col.operator("grease_pencil.layer_mask_remove", icon='REMOVE', text="")
|
|
|
|
col.separator()
|
|
|
|
sub = col.column(align=True)
|
|
sub.operator("grease_pencil.layer_mask_reorder", icon='TRIA_UP', text="").direction = 'UP'
|
|
sub.operator("grease_pencil.layer_mask_reorder", icon='TRIA_DOWN', text="").direction = 'DOWN'
|
|
|
|
|
|
class GreasePencil_LayerTransformPanel:
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
grease_pencil = context.grease_pencil
|
|
layer = grease_pencil.layers.active
|
|
layout.active = not layer.lock
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(layer, "translation")
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(layer, "rotation")
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(layer, "scale")
|
|
|
|
|
|
class GreasePencil_LayerAdjustmentsPanel:
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
grease_pencil = context.grease_pencil
|
|
layer = grease_pencil.layers.active
|
|
layout.active = not layer.lock
|
|
|
|
# Layer options
|
|
col = layout.column(align=True)
|
|
|
|
col.prop(layer, "tint_color")
|
|
col.prop(layer, "tint_factor", text="Factor", slider=True)
|
|
|
|
col = layout.row(align=True)
|
|
col.prop(layer, "radius_offset", text="Stroke Thickness")
|
|
|
|
|
|
class GreasePencil_LayerRelationsPanel:
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
grease_pencil = context.grease_pencil
|
|
layer = grease_pencil.layers.active
|
|
layout.active = not layer.lock
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(layer, "parent", text="Parent")
|
|
|
|
if layer.parent and layer.parent.type == 'ARMATURE':
|
|
row = layout.row(align=True)
|
|
row.prop_search(layer, "parent_bone", layer.parent.data, "bones", text="Bone")
|
|
|
|
layout.separator()
|
|
|
|
col = layout.row(align=True)
|
|
col.prop(layer, "pass_index")
|
|
|
|
col = layout.row(align=True)
|
|
col.prop_search(layer, "viewlayer_render", context.scene, "view_layers", text="View Layer")
|
|
|
|
col = layout.row(align=True)
|
|
# Only enable this property when a view layer is selected.
|
|
col.enabled = bool(layer.viewlayer_render)
|
|
col.prop(layer, "use_viewlayer_masks")
|
|
|
|
|
|
class GreasePencil_LayerDisplayPanel:
|
|
bl_label = "Display"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
grease_pencil = context.grease_pencil
|
|
layer = grease_pencil.layers.active
|
|
|
|
layout.prop(layer, "channel_color", text="Channel Color")
|
|
|
|
|
|
class GREASE_PENCIL_MT_layer_mask_add(Menu):
|
|
bl_label = "Add Mask"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
grease_pencil = context.grease_pencil
|
|
active_layer = grease_pencil.layers.active
|
|
found = False
|
|
for layer in grease_pencil.layers:
|
|
if layer == active_layer or layer.name in active_layer.mask_layers:
|
|
continue
|
|
|
|
found = True
|
|
layout.operator("grease_pencil.layer_mask_add", text=layer.name).name = layer.name
|
|
|
|
if not found:
|
|
layout.label(text="No layers to add")
|
|
|
|
|
|
class DATA_PT_context_grease_pencil(DataButtonsPanel, Panel):
|
|
bl_label = ""
|
|
bl_options = {'HIDE_HEADER'}
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
ob = context.object
|
|
grease_pencil = context.grease_pencil
|
|
space = context.space_data
|
|
|
|
if ob:
|
|
layout.template_ID(ob, "data")
|
|
elif grease_pencil:
|
|
layout.template_ID(space, "pin_id")
|
|
|
|
|
|
class GREASE_PENCIL_MT_grease_pencil_add_layer_extra(Menu):
|
|
bl_label = "Add Extra"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
grease_pencil = context.grease_pencil
|
|
layer = grease_pencil.layers.active
|
|
|
|
layout.separator()
|
|
layout.operator("grease_pencil.layer_duplicate", text="Duplicate", icon='DUPLICATE').empty_keyframes = False
|
|
layout.operator("grease_pencil.layer_duplicate", text="Duplicate Empty Keyframes").empty_keyframes = True
|
|
|
|
layout.separator()
|
|
layout.operator("grease_pencil.layer_reveal", icon='RESTRICT_VIEW_OFF', text="Show All")
|
|
layout.operator("grease_pencil.layer_hide", icon='RESTRICT_VIEW_ON', text="Hide Others").unselected = True
|
|
|
|
layout.separator()
|
|
layout.operator("grease_pencil.layer_lock_all", icon='LOCKED', text="Lock All").lock = True
|
|
layout.operator("grease_pencil.layer_lock_all", icon='UNLOCKED', text="Unlock All").lock = False
|
|
|
|
layout.separator()
|
|
layout.prop(grease_pencil, "use_autolock_layers", text="Autolock Inactive Layers")
|
|
|
|
if layer:
|
|
layout.prop(layer, "ignore_locked_materials")
|
|
|
|
layout.separator()
|
|
layout.operator("grease_pencil.layer_merge", text="Merge Down").mode = 'ACTIVE'
|
|
layout.operator("grease_pencil.layer_merge", text="Merge Group").mode = 'GROUP'
|
|
layout.operator("grease_pencil.layer_merge", text="Merge All").mode = 'ALL'
|
|
|
|
layout.separator()
|
|
layout.operator("grease_pencil.relative_layer_mask_add", text="Mask with Layer Above").mode = 'ABOVE'
|
|
layout.operator("grease_pencil.relative_layer_mask_add", text="Mask with Layer Below").mode = 'BELOW'
|
|
|
|
layout.separator()
|
|
layout.operator("grease_pencil.layer_duplicate_object", text="Copy Layer to Selected").only_active = True
|
|
layout.operator("grease_pencil.layer_duplicate_object", text="Copy All Layers to Selected").only_active = False
|
|
|
|
|
|
class GREASE_PENCIL_MT_group_context_menu(Menu):
|
|
bl_label = "Layer Group"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.operator("grease_pencil.layer_group_remove", text="Delete Group").keep_children = False
|
|
layout.operator("grease_pencil.layer_group_remove", text="Ungroup").keep_children = True
|
|
layout.operator("grease_pencil.layer_merge", text="Merge Group").mode = 'GROUP'
|
|
|
|
layout.separator()
|
|
row = layout.row(align=True)
|
|
row.operator_enum("grease_pencil.layer_group_color_tag", "color_tag", icon_only=True)
|
|
|
|
|
|
class DATA_PT_grease_pencil_layers(DataButtonsPanel, Panel):
|
|
bl_label = "Layers"
|
|
|
|
@classmethod
|
|
def draw_settings(cls, layout, grease_pencil):
|
|
layer = grease_pencil.layers.active
|
|
is_layer_active = layer is not None
|
|
is_group_active = grease_pencil.layer_groups.active is not None
|
|
|
|
row = layout.row()
|
|
row.template_grease_pencil_layer_tree()
|
|
|
|
col = row.column()
|
|
sub = col.column(align=True)
|
|
sub.operator_context = 'EXEC_DEFAULT'
|
|
sub.operator("grease_pencil.layer_add", icon='ADD', text="")
|
|
sub.operator("grease_pencil.layer_group_add", icon='NEWFOLDER', text="")
|
|
sub.separator()
|
|
|
|
if is_layer_active:
|
|
sub.operator("grease_pencil.layer_remove", icon='REMOVE', text="")
|
|
if is_group_active:
|
|
sub.operator("grease_pencil.layer_group_remove", icon='REMOVE', text="").keep_children = True
|
|
|
|
sub.separator()
|
|
|
|
sub.menu("GREASE_PENCIL_MT_grease_pencil_add_layer_extra", icon='DOWNARROW_HLT', text="")
|
|
|
|
col.separator()
|
|
|
|
sub = col.column(align=True)
|
|
sub.operator("grease_pencil.layer_move", icon='TRIA_UP', text="").direction = 'UP'
|
|
sub.operator("grease_pencil.layer_move", icon='TRIA_DOWN', text="").direction = 'DOWN'
|
|
|
|
if not is_layer_active:
|
|
return
|
|
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = True
|
|
col = layout.column(align=True)
|
|
|
|
# Layer main properties
|
|
row = layout.row(align=True)
|
|
row.prop(layer, "blend_mode", text="Blend Mode")
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(layer, "opacity", text="Opacity", slider=True)
|
|
|
|
row = layout.row(align=True)
|
|
row.prop(layer, "use_lights", text="Lights")
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
grease_pencil = context.grease_pencil
|
|
|
|
self.draw_settings(layout, grease_pencil)
|
|
|
|
|
|
class DATA_PT_grease_pencil_layer_masks(LayerDataButtonsPanel, GreasePencil_LayerMaskPanel, Panel):
|
|
bl_label = "Masks"
|
|
bl_parent_id = "DATA_PT_grease_pencil_layers"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
class DATA_PT_grease_pencil_layer_transform(LayerDataButtonsPanel, GreasePencil_LayerTransformPanel, Panel):
|
|
bl_label = "Transform"
|
|
bl_parent_id = "DATA_PT_grease_pencil_layers"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
class DATA_PT_grease_pencil_layer_adjustments(LayerDataButtonsPanel, GreasePencil_LayerAdjustmentsPanel, Panel):
|
|
bl_label = "Adjustments"
|
|
bl_parent_id = "DATA_PT_grease_pencil_layers"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
class DATA_PT_grease_pencil_layer_relations(LayerDataButtonsPanel, GreasePencil_LayerRelationsPanel, Panel):
|
|
bl_label = "Relations"
|
|
bl_parent_id = "DATA_PT_grease_pencil_layers"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
class DATA_PT_grease_pencil_layer_display(LayerDataButtonsPanel, GreasePencil_LayerDisplayPanel, Panel):
|
|
bl_label = "Display"
|
|
bl_parent_id = "DATA_PT_grease_pencil_layers"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
|
|
class DATA_PT_grease_pencil_layer_group_display(Panel):
|
|
bl_label = "Display"
|
|
bl_space_type = 'PROPERTIES'
|
|
bl_region_type = 'WINDOW'
|
|
bl_context = "data"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
grease_pencil = context.grease_pencil
|
|
return grease_pencil and grease_pencil.layer_groups.active
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
grease_pencil = context.grease_pencil
|
|
group = grease_pencil.layer_groups.active
|
|
|
|
layout.prop(group, "channel_color", text="Channel Color")
|
|
|
|
|
|
class DATA_PT_grease_pencil_onion_skinning(DataButtonsPanel, Panel):
|
|
bl_label = "Onion Skinning"
|
|
|
|
def draw(self, context):
|
|
grease_pencil = context.grease_pencil
|
|
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
|
|
col = layout.column()
|
|
col.prop(grease_pencil, "onion_mode")
|
|
col.prop(grease_pencil, "onion_factor", text="Opacity", slider=True)
|
|
col.prop(grease_pencil, "onion_keyframe_type")
|
|
|
|
if grease_pencil.onion_mode == 'ABSOLUTE':
|
|
col = layout.column(align=True)
|
|
col.prop(grease_pencil, "ghost_before_range", text="Frames Before")
|
|
col.prop(grease_pencil, "ghost_after_range", text="Frames After")
|
|
elif grease_pencil.onion_mode == 'RELATIVE':
|
|
col = layout.column(align=True)
|
|
col.prop(grease_pencil, "ghost_before_range", text="Keyframes Before")
|
|
col.prop(grease_pencil, "ghost_after_range", text="Keyframes After")
|
|
|
|
|
|
class DATA_PT_grease_pencil_onion_skinning_custom_colors(DataButtonsPanel, Panel):
|
|
bl_parent_id = "DATA_PT_grease_pencil_onion_skinning"
|
|
bl_label = "Custom Colors"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw_header(self, context):
|
|
grease_pencil = context.grease_pencil
|
|
self.layout.prop(grease_pencil, "use_ghost_custom_colors", text="")
|
|
|
|
def draw(self, context):
|
|
grease_pencil = context.grease_pencil
|
|
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.enabled = grease_pencil.users <= 1 and grease_pencil.use_ghost_custom_colors
|
|
|
|
layout.prop(grease_pencil, "before_color", text="Before")
|
|
layout.prop(grease_pencil, "after_color", text="After")
|
|
|
|
|
|
class DATA_PT_grease_pencil_onion_skinning_display(DataButtonsPanel, Panel):
|
|
bl_parent_id = "DATA_PT_grease_pencil_onion_skinning"
|
|
bl_label = "Display"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
grease_pencil = context.grease_pencil
|
|
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
# This was done in GPv2 but it's not entirely clear why. Presumably it was
|
|
# to indicate that the settings will affect the onion skinning of the
|
|
# other users.
|
|
layout.enabled = grease_pencil.users <= 1
|
|
|
|
col = layout.column(align=True)
|
|
col.prop(grease_pencil, "use_onion_fade", text="Fade")
|
|
sub = layout.column()
|
|
sub.active = grease_pencil.onion_mode in {'RELATIVE', 'SELECTED'}
|
|
sub.prop(grease_pencil, "use_onion_loop", text="Show Start Frame")
|
|
|
|
|
|
class DATA_PT_grease_pencil_settings(DataButtonsPanel, Panel):
|
|
bl_label = "Settings"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
|
|
grease_pencil = context.grease_pencil
|
|
col = layout.column(align=True)
|
|
col.prop(grease_pencil, "stroke_depth_order", text="Stroke Depth Order")
|
|
|
|
|
|
class DATA_PT_grease_pencil_animation(DataButtonsPanel, PropertiesAnimationMixin, PropertyPanel, Panel):
|
|
_animated_id_context_property = "grease_pencil"
|
|
|
|
|
|
class DATA_PT_grease_pencil_custom_props(DataButtonsPanel, PropertyPanel, Panel):
|
|
_context_path = "object.data"
|
|
_property_type = bpy.types.GreasePencilv3
|
|
|
|
|
|
class GREASE_PENCIL_UL_attributes(UIList):
|
|
def filter_items(self, _context, data, property):
|
|
attributes = getattr(data, property)
|
|
flags = []
|
|
indices = [i for i in range(len(attributes))]
|
|
|
|
# Filtering by name
|
|
if self.filter_name:
|
|
flags = bpy.types.UI_UL_list.filter_items_by_name(
|
|
self.filter_name, self.bitflag_filter_item, attributes, "name", reverse=self.use_filter_invert,
|
|
)
|
|
if not flags:
|
|
flags = [self.bitflag_filter_item] * len(attributes)
|
|
|
|
# Filtering internal attributes
|
|
for idx, item in enumerate(attributes):
|
|
flags[idx] = 0 if item.is_internal else flags[idx]
|
|
|
|
# Reorder by name.
|
|
if self.use_filter_sort_alpha:
|
|
indices = bpy.types.UI_UL_list.sort_items_by_name(attributes, "name")
|
|
|
|
return flags, indices
|
|
|
|
def draw_item(self, _context, layout, _data, attribute, _icon, _active_data, _active_propname, _index):
|
|
data_type = attribute.bl_rna.properties["data_type"].enum_items[attribute.data_type]
|
|
|
|
split = layout.split(factor=0.50)
|
|
split.emboss = 'NONE'
|
|
split.prop(attribute, "name", text="")
|
|
sub = split.row()
|
|
sub.alignment = 'RIGHT'
|
|
sub.active = False
|
|
sub.label(text=data_type.name)
|
|
|
|
|
|
class DATA_PT_grease_pencil_attributes(DataButtonsPanel, Panel):
|
|
bl_label = "Attributes"
|
|
bl_options = {'DEFAULT_CLOSED'}
|
|
|
|
def draw(self, context):
|
|
grease_pencil = context.grease_pencil
|
|
|
|
layout = self.layout
|
|
row = layout.row()
|
|
|
|
col = row.column()
|
|
col.template_list(
|
|
"GREASE_PENCIL_UL_attributes",
|
|
"attributes",
|
|
grease_pencil,
|
|
"attributes",
|
|
grease_pencil.attributes,
|
|
"active_index",
|
|
rows=3,
|
|
)
|
|
|
|
col = row.column(align=True)
|
|
col.operator("geometry.attribute_add", icon='ADD', text="")
|
|
col.operator("geometry.attribute_remove", icon='REMOVE', text="")
|
|
|
|
|
|
classes = (
|
|
GREASE_PENCIL_UL_masks,
|
|
GREASE_PENCIL_MT_layer_mask_add,
|
|
DATA_PT_context_grease_pencil,
|
|
DATA_PT_grease_pencil_layers,
|
|
DATA_PT_grease_pencil_layer_masks,
|
|
DATA_PT_grease_pencil_layer_transform,
|
|
DATA_PT_grease_pencil_layer_adjustments,
|
|
DATA_PT_grease_pencil_layer_relations,
|
|
DATA_PT_grease_pencil_layer_display,
|
|
DATA_PT_grease_pencil_layer_group_display,
|
|
DATA_PT_grease_pencil_onion_skinning,
|
|
DATA_PT_grease_pencil_onion_skinning_custom_colors,
|
|
DATA_PT_grease_pencil_onion_skinning_display,
|
|
DATA_PT_grease_pencil_settings,
|
|
DATA_PT_grease_pencil_custom_props,
|
|
GREASE_PENCIL_MT_grease_pencil_add_layer_extra,
|
|
GREASE_PENCIL_MT_group_context_menu,
|
|
DATA_PT_grease_pencil_animation,
|
|
GREASE_PENCIL_UL_attributes,
|
|
DATA_PT_grease_pencil_attributes,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__": # only for live edit.
|
|
from bpy.utils import register_class
|
|
for cls in classes:
|
|
register_class(cls)
|