Compositor: Implement shortcuts for Viewer nodes
Artists often want to quickly switch back and forth between two or more nodes while compositing. This patch implements two operators `NODE_OT_viewer_shortcut_set` and `NODE_OT_viewer_shortcut_get` that allow users to map a viewer node to a shortcut. For example, pressing `cltr+1` while a node is selected, assigns that node to the shortcut `1`, creates a viewer node if it has none attached and sets that viewer node to active. Pressing `1` will set the active node with shortcut `1` to active. Shortcuts are saved in DNA to preserve them after saving/loading blend files. Limitations: - Only compositor node tree is supported, because shading editor has no viewer node and geometry nodes viewer works differently. Pull Request: https://projects.blender.org/blender/blender/pulls/123641
This commit is contained in:
parent
5eb8101efa
commit
b51c560f6e
@ -2277,6 +2277,34 @@ def km_node_editor(params):
|
||||
("wm.context_toggle", {"type": 'Z', "value": 'PRESS', "alt": True, "shift": True},
|
||||
{"properties": [("data_path", "space_data.overlay.show_overlays")]}),
|
||||
*_template_items_context_menu("NODE_MT_context_menu", params.context_menu_event),
|
||||
# Viewer shortcuts.
|
||||
("node.viewer_shortcut_get", {"type": 'ONE', "value": 'PRESS'}, {"properties": [("viewer_index", 1)]}),
|
||||
("node.viewer_shortcut_set", {"type": 'ONE', "value": 'PRESS',
|
||||
'ctrl': True}, {"properties": [("viewer_index", 1)]}),
|
||||
("node.viewer_shortcut_get", {"type": 'TWO', "value": 'PRESS'}, {"properties": [("viewer_index", 2)]}),
|
||||
("node.viewer_shortcut_set", {"type": 'TWO', "value": 'PRESS',
|
||||
'ctrl': True}, {"properties": [("viewer_index", 2)]}),
|
||||
("node.viewer_shortcut_get", {"type": 'THREE', "value": 'PRESS'}, {"properties": [("viewer_index", 3)]}),
|
||||
("node.viewer_shortcut_set", {"type": 'THREE', "value": 'PRESS', 'ctrl': True},
|
||||
{"properties": [("viewer_index", 3)]}),
|
||||
("node.viewer_shortcut_get", {"type": 'FOUR', "value": 'PRESS'}, {"properties": [("viewer_index", 4)]}),
|
||||
("node.viewer_shortcut_set", {"type": 'FOUR', "value": 'PRESS', 'ctrl': True},
|
||||
{"properties": [("viewer_index", 4)]}),
|
||||
("node.viewer_shortcut_get", {"type": 'FIVE', "value": 'PRESS'}, {"properties": [("viewer_index", 5)]}),
|
||||
("node.viewer_shortcut_set", {"type": 'FIVE', "value": 'PRESS', 'ctrl': True},
|
||||
{"properties": [("viewer_index", 5)]}),
|
||||
("node.viewer_shortcut_get", {"type": 'SIX', "value": 'PRESS'}, {"properties": [("viewer_index", 6)]}),
|
||||
("node.viewer_shortcut_set", {"type": 'SIX', "value": 'PRESS', 'ctrl': True},
|
||||
{"properties": [("viewer_index", 6)]}),
|
||||
("node.viewer_shortcut_get", {"type": 'SEVEN', "value": 'PRESS'}, {"properties": [("viewer_index", 7)]}),
|
||||
("node.viewer_shortcut_set", {"type": 'SEVEN', "value": 'PRESS', 'ctrl': True},
|
||||
{"properties": [("viewer_index", 7)]}),
|
||||
("node.viewer_shortcut_get", {"type": 'EIGHT', "value": 'PRESS'}, {"properties": [("viewer_index", 8)]}),
|
||||
("node.viewer_shortcut_set", {"type": 'EIGHT', "value": 'PRESS', 'ctrl': True},
|
||||
{"properties": [("viewer_index", 8)]}),
|
||||
("node.viewer_shortcut_get", {"type": 'NINE', "value": 'PRESS'}, {"properties": [("viewer_index", 9)]}),
|
||||
("node.viewer_shortcut_set", {"type": 'NINE', "value": 'PRESS', 'ctrl': True},
|
||||
{"properties": [("viewer_index", 9)]}),
|
||||
])
|
||||
|
||||
return keymap
|
||||
|
@ -16,6 +16,7 @@ from bpy.props import (
|
||||
EnumProperty,
|
||||
FloatVectorProperty,
|
||||
StringProperty,
|
||||
IntProperty,
|
||||
)
|
||||
from mathutils import (
|
||||
Vector,
|
||||
@ -408,6 +409,104 @@ class NODE_OT_interface_item_remove(NodeInterfaceOperator, Operator):
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class NODE_OT_viewer_shortcut_set(Operator):
|
||||
"""Create a compositor viewer shortcut for the selected node by pressing ctrl+1,2,..9"""
|
||||
bl_idname = "node.viewer_shortcut_set"
|
||||
bl_label = "Fast Preview"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
viewer_index: IntProperty(
|
||||
name="Viewer Index",
|
||||
description="Index corresponding to the shortcut, e.g. number key 1 corresponds to index 1 etc..")
|
||||
|
||||
def get_connected_viewer(self, node):
|
||||
for out in node.outputs:
|
||||
for link in out.links:
|
||||
nv = link.to_node
|
||||
if nv.type == 'VIEWER':
|
||||
return nv
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
return context.space_data.tree_type == 'CompositorNodeTree'
|
||||
|
||||
def execute(self, context):
|
||||
nodes = context.space_data.edit_tree.nodes
|
||||
links = context.space_data.edit_tree.links
|
||||
selected_nodes = context.selected_nodes
|
||||
|
||||
if len(selected_nodes) == 0:
|
||||
self.report({'ERROR'}, "Select a node to assign a shortcut")
|
||||
return {'CANCELLED'}
|
||||
|
||||
fav_node = selected_nodes[0]
|
||||
|
||||
# Only viewer nodes can be set to favorites. However, the user can
|
||||
# create a new favorite viewer by selecting any node and pressing ctrl+1.
|
||||
old_active = nodes.active
|
||||
if fav_node.type == 'VIEWER':
|
||||
viewer_node = fav_node
|
||||
else:
|
||||
viewer_node = self.get_connected_viewer(fav_node)
|
||||
if not viewer_node:
|
||||
# Calling link_viewer() if a viewer node is connected will connect the next available socket to the viewer node.
|
||||
# This behavior is not desired as we want to create a shortcut to the exisiting connected viewer node.
|
||||
# Therefore link_viewer() is called only when no viewer node is connected.
|
||||
bpy.ops.node.link_viewer()
|
||||
viewer_node = self.get_connected_viewer(fav_node)
|
||||
|
||||
if not viewer_node:
|
||||
self.report({'ERROR'}, "Unable to set shortcut, selected node is not a viewer node or does not support viewing")
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Use the node active status to enable this viewer node and disable others.
|
||||
nodes.active = viewer_node
|
||||
if old_active.type != 'VIEWER':
|
||||
nodes.active = old_active
|
||||
|
||||
viewer_node.ui_shortcut = self.viewer_index
|
||||
self.report({'INFO'}, "Assigned shortcut %i to %s" % (self.viewer_index, viewer_node.name))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class NODE_OT_viewer_shortcut_get(Operator):
|
||||
"""Activate a specific compositor viewer node using 1,2,..,9 keys"""
|
||||
bl_idname = "node.viewer_shortcut_get"
|
||||
bl_label = "Fast Preview"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
viewer_index: IntProperty(
|
||||
name="Viewer Index",
|
||||
description="Index corresponding to the shortcut, e.g. number key 1 corresponds to index 1 etc..")
|
||||
|
||||
@classmethod
|
||||
def poll(self, context):
|
||||
return context.space_data.tree_type == 'CompositorNodeTree'
|
||||
|
||||
def execute(self, context):
|
||||
nodes = context.space_data.edit_tree.nodes
|
||||
|
||||
# Get viewer node with exisiting shortcut.
|
||||
viewer_node = None
|
||||
for n in nodes:
|
||||
if n.type == 'VIEWER' and n.ui_shortcut == self.viewer_index:
|
||||
viewer_node = n
|
||||
|
||||
if not viewer_node:
|
||||
self.report({'INFO'}, "Shortcut %i is not assigned to a Viewer node yet" % self.viewer_index)
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Use the node active status to enable this viewer node and disable others.
|
||||
old_active = nodes.active
|
||||
nodes.active = viewer_node
|
||||
if old_active.type != "VIEWER":
|
||||
nodes.active = old_active
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class NODE_FH_image_node(FileHandler):
|
||||
bl_idname = "NODE_FH_image_node"
|
||||
bl_label = "Image node"
|
||||
@ -438,4 +537,6 @@ classes = (
|
||||
NODE_OT_interface_item_duplicate,
|
||||
NODE_OT_interface_item_remove,
|
||||
NODE_OT_tree_path_parent,
|
||||
NODE_OT_viewer_shortcut_get,
|
||||
NODE_OT_viewer_shortcut_set,
|
||||
)
|
||||
|
@ -3674,6 +3674,9 @@ void node_tree_set_output(bNodeTree *ntree)
|
||||
/* same type, exception for viewer */
|
||||
const bool tnode_is_output = tnode->type_legacy == CMP_NODE_VIEWER;
|
||||
const bool compositor_case = is_compositor && tnode_is_output && node_is_output;
|
||||
const bool has_same_shortcut = compositor_case && node != tnode &&
|
||||
tnode->custom1 == node->custom1 &&
|
||||
tnode->custom1 != NODE_VIEWER_SHORTCUT_NONE;
|
||||
if (tnode->type_legacy == node->type_legacy || compositor_case) {
|
||||
if (tnode->flag & NODE_DO_OUTPUT) {
|
||||
output++;
|
||||
@ -3682,6 +3685,9 @@ void node_tree_set_output(bNodeTree *ntree)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (has_same_shortcut) {
|
||||
tnode->custom1 = NODE_VIEWER_SHORTCUT_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
if (output == 0) {
|
||||
|
@ -1283,6 +1283,17 @@ static void do_version_color_to_float_conversion(bNodeTree *node_tree)
|
||||
}
|
||||
}
|
||||
|
||||
static void do_version_viewer_shortcut(bNodeTree *node_tree)
|
||||
{
|
||||
LISTBASE_FOREACH_MUTABLE (bNode *, node, &node_tree->nodes) {
|
||||
if (node->type_legacy != CMP_NODE_VIEWER) {
|
||||
continue;
|
||||
}
|
||||
/* custom1 was previously used for Tile Order for the Tiled Compositor. */
|
||||
node->custom1 = NODE_VIEWER_SHORTCUT_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
static bool all_scenes_use(Main *bmain, const blender::Span<const char *> engines)
|
||||
{
|
||||
if (!bmain->scenes.first) {
|
||||
@ -5807,6 +5818,15 @@ void blo_do_versions_400(FileData *fd, Library * /*lib*/, Main *bmain)
|
||||
}
|
||||
}
|
||||
|
||||
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 404, 27)) {
|
||||
FOREACH_NODETREE_BEGIN (bmain, ntree, id) {
|
||||
if (ntree->type == NTREE_COMPOSIT) {
|
||||
do_version_viewer_shortcut(ntree);
|
||||
}
|
||||
}
|
||||
FOREACH_NODETREE_END;
|
||||
}
|
||||
|
||||
/* Always run this versioning; meshes are written with the legacy format which always needs to
|
||||
* be converted to the new format on file load. Can be moved to a subversion check in a larger
|
||||
* breaking release. */
|
||||
|
@ -3268,6 +3268,36 @@ static void node_draw_extra_info_panel(const bContext &C,
|
||||
}
|
||||
}
|
||||
|
||||
static short get_viewer_shortcut_icon(const bNode &node)
|
||||
{
|
||||
BLI_assert(node.is_type("CompositorNodeViewer"));
|
||||
switch (node.custom1) {
|
||||
case NODE_VIEWER_SHORTCUT_NONE:
|
||||
/* No change by default. */
|
||||
return node.typeinfo->ui_icon;
|
||||
case NODE_VIEWER_SHORCTUT_SLOT_1:
|
||||
return ICON_EVENT_ONEKEY;
|
||||
case NODE_VIEWER_SHORCTUT_SLOT_2:
|
||||
return ICON_EVENT_TWOKEY;
|
||||
case NODE_VIEWER_SHORCTUT_SLOT_3:
|
||||
return ICON_EVENT_THREEKEY;
|
||||
case NODE_VIEWER_SHORCTUT_SLOT_4:
|
||||
return ICON_EVENT_FOURKEY;
|
||||
case NODE_VIEWER_SHORCTUT_SLOT_5:
|
||||
return ICON_EVENT_FIVEKEY;
|
||||
case NODE_VIEWER_SHORCTUT_SLOT_6:
|
||||
return ICON_EVENT_SIXKEY;
|
||||
case NODE_VIEWER_SHORCTUT_SLOT_7:
|
||||
return ICON_EVENT_SEVENKEY;
|
||||
case NODE_VIEWER_SHORCTUT_SLOT_8:
|
||||
return ICON_EVENT_EIGHTKEY;
|
||||
case NODE_VIEWER_SHORCTUT_SLOT_9:
|
||||
return ICON_EVENT_NINEKEY;
|
||||
}
|
||||
|
||||
return node.typeinfo->ui_icon;
|
||||
}
|
||||
|
||||
static void node_draw_basis(const bContext &C,
|
||||
TreeDrawContext &tree_draw_ctx,
|
||||
const View2D &v2d,
|
||||
@ -3461,6 +3491,25 @@ static void node_draw_basis(const bContext &C,
|
||||
but, node_toggle_button_cb, POINTER_FROM_INT(node.identifier), (void *)operator_idname);
|
||||
UI_block_emboss_set(&block, UI_EMBOSS);
|
||||
}
|
||||
/* Viewer node shortcuts. */
|
||||
if (node.is_type("CompositorNodeViewer")) {
|
||||
short shortcut_icon = get_viewer_shortcut_icon(node);
|
||||
iconofs -= iconbutw;
|
||||
UI_block_emboss_set(&block, UI_EMBOSS_NONE);
|
||||
uiDefIconBut(&block,
|
||||
UI_BTYPE_BUT,
|
||||
0,
|
||||
shortcut_icon,
|
||||
iconofs,
|
||||
rct.ymax - NODE_DY,
|
||||
iconbutw,
|
||||
UI_UNIT_Y,
|
||||
nullptr,
|
||||
0,
|
||||
0,
|
||||
"");
|
||||
UI_block_emboss_set(&block, UI_EMBOSS);
|
||||
}
|
||||
|
||||
node_add_error_message_button(tree_draw_ctx, node, block, rct, iconofs);
|
||||
|
||||
|
@ -739,7 +739,7 @@ static int view_socket(const bContext &C,
|
||||
/* Try to find a viewer that is already active. */
|
||||
for (bNode *node : btree.all_nodes()) {
|
||||
if (is_viewer_node(*node)) {
|
||||
if (node->flag & NODE_DO_OUTPUT) {
|
||||
if (node->flag & NODE_DO_OUTPUT && node->custom1 == NODE_VIEWER_SHORTCUT_NONE) {
|
||||
viewer_node = node;
|
||||
break;
|
||||
}
|
||||
@ -759,7 +759,7 @@ static int view_socket(const bContext &C,
|
||||
|
||||
if (viewer_node == nullptr) {
|
||||
for (bNode *node : btree.all_nodes()) {
|
||||
if (is_viewer_node(*node)) {
|
||||
if (is_viewer_node(*node) && node->custom1 == NODE_VIEWER_SHORTCUT_NONE) {
|
||||
viewer_node = node;
|
||||
break;
|
||||
}
|
||||
|
@ -353,6 +353,21 @@ typedef struct bNodePanelState {
|
||||
#endif
|
||||
} bNodePanelState;
|
||||
|
||||
typedef enum eViewerNodeShortcut {
|
||||
NODE_VIEWER_SHORTCUT_NONE = 0,
|
||||
/* Users can set custom keys to shortcuts,
|
||||
* but shortcuts should always be referred to as enums. */
|
||||
NODE_VIEWER_SHORCTUT_SLOT_1 = 1,
|
||||
NODE_VIEWER_SHORCTUT_SLOT_2 = 2,
|
||||
NODE_VIEWER_SHORCTUT_SLOT_3 = 3,
|
||||
NODE_VIEWER_SHORCTUT_SLOT_4 = 4,
|
||||
NODE_VIEWER_SHORCTUT_SLOT_5 = 5,
|
||||
NODE_VIEWER_SHORCTUT_SLOT_6 = 6,
|
||||
NODE_VIEWER_SHORCTUT_SLOT_7 = 7,
|
||||
NODE_VIEWER_SHORCTUT_SLOT_8 = 8,
|
||||
NODE_VIEWER_SHORCTUT_SLOT_9 = 9
|
||||
} eViewerNodeShortcut;
|
||||
|
||||
typedef enum NodeWarningPropagation {
|
||||
NODE_WARNING_PROPAGATION_ALL = 0,
|
||||
NODE_WARNING_PROPAGATION_NONE = 1,
|
||||
|
@ -1356,6 +1356,20 @@ static void rna_NodeTree_active_node_set(PointerRNA *ptr,
|
||||
}
|
||||
}
|
||||
|
||||
static void rna_Node_shortcut_node_set(PointerRNA *ptr, int value)
|
||||
{
|
||||
bNode *curr_node = static_cast<bNode *>(ptr->data);
|
||||
bNodeTree &ntree = curr_node->owner_tree();
|
||||
|
||||
/* Avoid having two nodes with the same shortcut. */
|
||||
for (bNode *node : ntree.all_nodes()) {
|
||||
if (node->is_type("CompositorNodeViewer") && node->custom1 == value) {
|
||||
node->custom1 = NODE_VIEWER_SHORTCUT_NONE;
|
||||
}
|
||||
}
|
||||
curr_node->custom1 = value;
|
||||
}
|
||||
|
||||
static bNodeLink *rna_NodeTree_link_new(bNodeTree *ntree,
|
||||
Main *bmain,
|
||||
ReportList *reports,
|
||||
@ -9125,6 +9139,14 @@ static void def_cmp_viewer(BlenderRNA * /*brna*/, StructRNA *srna)
|
||||
"Use Alpha",
|
||||
"Colors are treated alpha premultiplied, or colors output straight (alpha gets set to 1)");
|
||||
RNA_def_property_update(prop, NC_NODE | NA_EDITED, "rna_Node_update");
|
||||
|
||||
prop = RNA_def_property(srna, "ui_shortcut", PROP_INT, PROP_NONE);
|
||||
RNA_def_property_int_sdna(prop, nullptr, "custom1");
|
||||
RNA_def_property_int_funcs(prop, nullptr, "rna_Node_shortcut_node_set", nullptr);
|
||||
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
|
||||
RNA_def_property_override_flag(prop, PROPOVERRIDE_IGNORE);
|
||||
RNA_def_property_int_default(prop, NODE_VIEWER_SHORTCUT_NONE);
|
||||
RNA_def_property_update(prop, NC_NODE | ND_DISPLAY, nullptr);
|
||||
}
|
||||
|
||||
static void def_cmp_composite(BlenderRNA * /*brna*/, StructRNA *srna)
|
||||
|
@ -40,6 +40,7 @@ static void node_composit_init_viewer(bNodeTree * /*ntree*/, bNode *node)
|
||||
ImageUser *iuser = MEM_cnew<ImageUser>(__func__);
|
||||
node->storage = iuser;
|
||||
iuser->sfra = 1;
|
||||
node->custom1 = NODE_VIEWER_SHORTCUT_NONE;
|
||||
|
||||
node->id = (ID *)BKE_image_ensure_viewer(G.main, IMA_TYPE_COMPOSITE, "Viewer Node");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user