import bpy
import nodeitems_builtins
from enum import Enum
from mathutils import Vector
from ... utility import method_handler
from ... ui_framework import form_ui as form
from ... utility.base_modal_controls import Base_Modal_Controls
from ... ui_framework.master import Master
from ... ui_framework.utils.dialogue import Dialogue
def node_categories(context):
groups = {}
if context.area.ui_type == 'ShaderNodeTree':
for cat in nodeitems_builtins.shader_node_categories:
groups[cat.name] = []
append = groups[cat.name].append
for item in cat.items(context):
if hasattr(item, 'nodetype'):
append(item.nodetype)
elif context.area.ui_type == 'GeometryNodeTree':
for cat in nodeitems_builtins.geometry_node_categories:
groups[cat.name] = []
append = groups[cat.name].append
for item in cat.items(context):
if hasattr(item, 'nodetype'):
append(item.nodetype)
return groups
class Modes(Enum):
RELATED = 0
CATEGORIES = 1
APPEND = 2
ALL = 3
class Node_Data:
def __init__(self, node):
self.type = node.type
self.bl_idname = node.bl_idname
self.inputs = [n.type for n in node.inputs]
self.outputs = [o.type for o in node.outputs]
class Graph:
def __init__(self, context):
self.tree = context.area.spaces.active.edit_tree
self.active_node = self.tree.nodes.active
self.location = Vector((0,0))
self.dimensions = Vector((0, 0))
# Insert Location
x, y = 0, 0
if self.active_node:
x, y = self.active_node.location
self.location = Vector((x, y))
# Validate
if self.active_node == None: return
self.dimensions = self.active_node.dimensions.copy()
# Node Data for scrolls
self.node_data = {} # Key : Category, Val = [Node_Data]
nodes = []
category_data = node_categories(context)
for category, types in category_data.items():
node_datas = []
for node_type in types:
if not hasattr(bpy.types, node_type): continue
node = self.tree.nodes.new(node_type)
node_datas.append(Node_Data(node))
nodes.append(node)
self.node_data[category] = node_datas[:]
for node in nodes:
self.tree.nodes.remove(node)
del nodes
# Capture Node Connections
self.output_links = {} # KEY = Socket : VAL = list ( to socket )
self.input_links = {} # KEY = Socket : VAL = list ( to socket )
for output in self.active_node.outputs:
connections = []
for link in output.links:
connections.append(link.to_socket)
self.output_links[output] = connections
for n_input in self.active_node.inputs:
connections = []
for link in n_input.links:
connections.append(link.from_socket)
self.input_links[n_input] = connections
def find_node_data(self, bl_idname):
for category, datas in self.node_data.items():
for node_data in datas:
if node_data.bl_idname == bl_idname:
return node_data
return None
def hide_active(self, hide=True):
if self.active_node == None: return
self.active_node.hide = hide
def restore_connections(self):
if self.active_node == None: return
for output in self.active_node.outputs:
for link in output.links:
self.tree.links.remove(link)
for n_input in self.active_node.inputs:
for link in n_input.links:
self.tree.links.remove(link)
# Connect Outputs
for socket, connections in self.output_links.items():
for to_scoket in connections:
self.tree.links.new(socket, to_scoket)
# Connect Inputs
for socket, connections in self.input_links.items():
for to_scoket in connections:
self.tree.links.new(socket, to_scoket)
class Related_Nodes:
def __init__(self, context, graph):
self.index = 0
self.types = []
self.valid = True
self.category_title = ""
self.current_node = None
active_node = graph.active_node
if not active_node:
self.valid = False
return
# Save nodes related nodes
for category, datas in graph.node_data.items():
found = False
for node_data in datas:
if node_data.bl_idname == active_node.bl_idname:
found = True
break
if found:
self.types = [n.bl_idname for n in datas]
self.category_title = category
break
if not self.types:
self.valid = False
def scroll(self, context, scroll, graph):
if graph.active_node == None: return
graph.hide_active(hide=False)
self.index += scroll
if self.index > len(self.types) - 1:
self.index = 0
elif self.index < 0:
self.index = len(self.types) - 1
self.create(graph)
def create(self, graph):
self.remove(graph)
node_type = self.types[self.index]
self.current_node = graph.tree.nodes.new(node_type)
location = graph.location
dimensions = graph.dimensions
self.current_node.location.x = location.x
self.current_node.location.y = location.y
self.current_node.location.y -= dimensions.y + 10
def remove(self, graph):
if not self.current_node: return
graph.tree.nodes.remove(self.current_node)
self.current_node = None
class Category_Nodes:
def __init__(self, context, graph):
self.categories = node_categories(context)
self.keys = []
self.index = 0
self.nodes = []
self.current_category_name = ""
self.run_late_update = False
if 'Layout' in self.categories:
del self.categories['Layout']
if 'Group' in self.categories:
del self.categories['Group']
self.keys = list(self.categories.keys())
self.current_category_name = self.keys[self.index]
def scroll(self, context, scroll, graph):
graph.hide_active(hide=True)
self.index += scroll
if self.index > len(self.categories) - 1:
self.index = 0
elif self.index < 0:
self.index = len(self.categories) - 1
self.create(graph)
def create(self, graph):
self.remove(graph)
category_name = self.keys[self.index]
self.current_category_name = category_name
node_types = self.categories[category_name]
offset_y = 150
shift_x = -1 * 55 * len(node_types)
padding_x = 10
# Place Nodes
frame_children = []
offset_x = 0
for node_type in node_types:
if not hasattr(bpy.types, node_type): continue
node = graph.tree.nodes.new(node_type)
node.location.x = graph.location.x + offset_x + shift_x
node.location.y = graph.location.y - offset_y
offset_x += node.width + padding_x
frame_children.append(node)
self.nodes.append(node)
width = sum([node.width + padding_x for node in frame_children])
# Frame
frame = graph.tree.nodes.new('NodeFrame')
frame.label = category_name
frame.location.x = graph.location.x + shift_x
frame.location.y = graph.location.y - offset_y
frame.width = width
frame.height = 400
self.nodes.append(frame)
for node in frame_children:
node.parent = frame
# Update parenting draw
self.run_late_update = True
def remove(self, graph):
if not self.nodes: return
for node in self.nodes:
graph.tree.nodes.remove(node)
self.nodes = []
def late_update(self):
if self.run_late_update:
self.run_late_update = False
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
class Append_Nodes:
def __init__(self, context, graph):
self.flip = False
self.index = 0
self.types = []
self.active_socket_index = 0
self.target_sockect_index = 0
self.valid = False
self.current_node = None
self.category_index = 0
if not graph.active_node: return
if len(graph.active_node.outputs) < 1: return
self.valid = True
self.build_types(graph)
# Default Active : Starts with output sockets
for index, output in enumerate(graph.active_node.outputs):
if output.enabled:
self.active_socket_index = index
break
def filter_types(self, graph, string):
active_socket = None
if self.flip:
active_socket = graph.active_node.inputs[self.active_socket_index].type
else:
active_socket = graph.active_node.outputs[self.active_socket_index].type
if active_socket == None: return
temp_types = []
for category, datas in graph.node_data.items():
for node_data in datas:
if string.lower() in node_data.bl_idname.lower():
# Match Active Inputs to Target Outputs
if self.flip:
if active_socket in node_data.outputs:
temp_types.append(node_data.bl_idname)
# Match Active Outputs to Target Inputs
else:
if active_socket in node_data.inputs:
temp_types.append(node_data.bl_idname)
if temp_types:
self.index = 0
self.types = temp_types[:]
bpy.ops.hops.display_notification(info=F'Found : {len(self.types)} Nodes')
else:
bpy.ops.hops.display_notification(info='Nothing Found')
def set_flip(self, graph):
active = graph.active_node
if len(active.inputs) <= 0:
bpy.ops.hops.display_notification(info=F'Node Doesnt Support Inputs')
return
self.flip = not self.flip
self.build_types(graph)
def scroll(self, context, event, scroll, graph):
if graph.active_node == None: return
graph.hide_active(hide=False)
self.__ensure_indexes(graph)
# Output increment
if event.shift:
self.__scroll_outputs(context, event, scroll, graph)
# Categories
if event.ctrl:
self.__scroll_categories(scroll, graph)
# Current Node
else:
self.__scroll_nodes(context, event, scroll, graph)
# Validate
if len(self.types) == 0:
self.build_types(graph)
elif self.index > len(self.types) - 1:
self.index = 0
elif self.index < 0:
self.index = 0
if len(self.types) == 0:
return
bl_idname = self.types[self.index]
node_data = graph.find_node_data(bl_idname)
if not node_data: return
if self.flip:
in_type = graph.active_node.inputs[self.active_socket_index].type
for index, out_type in enumerate(node_data.outputs):
if in_type == out_type:
self.target_sockect_index = index
break
else:
out_type = graph.active_node.outputs[self.active_socket_index].type
for index, in_type in enumerate(node_data.inputs):
if out_type == in_type:
self.target_sockect_index = index
break
if out_type in {'VECTOR', 'RGBA'}:
if in_type in {'VECTOR', 'RGBA'}:
self.target_sockect_index = index
break
self.create(graph)
def __scroll_outputs(self, context, event, scroll, graph):
# Get next
moved = False
if self.flip:
for index, n_input in enumerate(graph.active_node.inputs):
if n_input.enabled:
if index > self.active_socket_index:
self.active_socket_index = index
moved = True
break
# End of list : Get first
if not moved:
for index, n_input in enumerate(graph.active_node.inputs):
if n_input.enabled:
self.active_socket_index = index
break
else:
for index, output in enumerate(graph.active_node.outputs):
if output.enabled:
if index > self.active_socket_index:
self.active_socket_index = index
moved = True
break
# End of list : Get first
if not moved:
for index, output in enumerate(graph.active_node.outputs):
if output.enabled:
self.active_socket_index = index
break
# Recalc Types
self.build_types(graph)
# Set back the previous type
if self.current_node:
bl_idname = self.current_node.bl_idname
if bl_idname in self.types:
self.index = self.types.index(bl_idname)
def __scroll_categories(self, scroll, graph):
active_socket = None
if self.flip:
active_socket = graph.active_node.inputs[self.active_socket_index].type
else:
active_socket = graph.active_node.outputs[self.active_socket_index].type
if active_socket == None: return
# Get all possible matching nodes
possible = [] # -> [ (category, [nodes]), (category, [nodes]), ]
for category, datas in graph.node_data.items():
temp_types = []
for node_data in datas:
# Match Active Inputs to Target Outputs
if self.flip:
if active_socket in node_data.outputs:
temp_types.append(node_data.bl_idname)
# Match Active Outputs to Target Inputs
else:
if active_socket in node_data.inputs:
temp_types.append(node_data.bl_idname)
possible.append( (category, temp_types) )
# Basic clamping
self.category_index += scroll
if self.category_index < 0:
self.category_index = len(possible) - 1
elif self.category_index > len(possible) - 1:
self.category_index = 0
category, nodes = possible[self.category_index]
if len(nodes) > 0:
self.index = 0
self.types = nodes[:]
bpy.ops.hops.display_notification(info=F'{category} : {len(self.types)} Nodes')
else:
bpy.ops.hops.display_notification(info=F'No Matches in : {category}')
def __scroll_nodes(self, context, event, scroll, graph):
self.index += scroll
if self.index > len(self.types) - 1:
self.index = 0
elif self.index < 0:
self.index = len(self.types) - 1
def build_types(self, graph, display_count=False):
active = graph.active_node
self.__ensure_indexes(graph)
if self.flip:
ins = [i for i in active.inputs]
if not ins: return
active_in = ins[self.active_socket_index].type
self.types = []
self.index = 0
for category, datas in graph.node_data.items():
for node_data in datas:
if active_in in node_data.outputs:
self.types.append(node_data.bl_idname)
else:
outs = [o for o in active.outputs]
if not outs: return
active_out = outs[self.active_socket_index].type
self.types = []
self.index = 0
for category, datas in graph.node_data.items():
for node_data in datas:
if active_out in node_data.inputs:
self.types.append(node_data.bl_idname)
elif active_out in {'VECTOR', 'RGBA'}:
if 'VECTOR' in node_data.inputs or 'RGBA' in node_data.inputs:
self.types.append(node_data.bl_idname)
if display_count:
bpy.ops.hops.display_notification(info=F'Reset : {len(self.types)} Nodes')
def __ensure_indexes(self, graph):
if self.active_socket_index < 0:
self.active_socket_index = 0
if self.target_sockect_index < 0:
self.target_sockect_index = 0
# --- ACTIVE NODE --- #
active = graph.active_node
valid_sockets = []
# Valid Input Sockets
if self.flip:
for index, n_input in enumerate(active.inputs):
if n_input.enabled:
valid_sockets.append(index)
# Valid Output Sockets
else:
for index, outputs in enumerate(active.outputs):
if outputs.enabled:
valid_sockets.append(index)
# Set to valid socket
if self.active_socket_index not in valid_sockets:
if valid_sockets:
self.active_socket_index = valid_sockets[0]
else:
self.active_socket_index = 0
# --- TARGET NODE --- #
if not self.current_node: return
valid_sockets = []
if self.flip:
for index, n_input in enumerate(self.current_node.inputs):
if n_input.enabled:
valid_sockets.append(index)
else:
for index, outputs in enumerate(self.current_node.outputs):
if outputs.enabled:
valid_sockets.append(index)
# Set to valid socket
if self.target_sockect_index not in valid_sockets:
if valid_sockets:
self.target_sockect_index = valid_sockets[0]
else:
self.target_sockect_index = 0
def create(self, graph):
self.remove(graph)
if self.index > len(self.types) - 1: return
if self.index < 0: return
if len(self.types) == 0: return
node_type = self.types[self.index]
self.current_node = graph.tree.nodes.new(node_type)
location = graph.location
dimensions = graph.dimensions
self.current_node.location.x = location.x
self.current_node.location.y = location.y
graph.restore_connections()
if self.flip:
# Ensure
valid = True
if self.target_sockect_index > len(self.current_node.outputs) - 1:
valid = False
if self.active_socket_index > len(graph.active_node.inputs) - 1:
valid = False
if not valid:
self.__ensure_indexes(graph)
self.remove(graph)
return
self.current_node.location.x -= dimensions.x + 60
output_slot = self.current_node.outputs[self.target_sockect_index]
input_slot = graph.active_node.inputs[self.active_socket_index]
graph.tree.links.new(output_slot, input_slot)
else:
# Ensure
valid = True
if self.active_socket_index > len(graph.active_node.outputs) - 1:
valid = False
if self.target_sockect_index > len(self.current_node.inputs) - 1:
valid = False
if not valid:
self.__ensure_indexes(graph)
self.remove(graph)
return
self.current_node.location.x += dimensions.x + 60
output_slot = graph.active_node.outputs[self.active_socket_index]
input_slot = self.current_node.inputs[self.target_sockect_index]
graph.tree.links.new(output_slot, input_slot)
def remove(self, graph):
if not self.current_node: return
graph.tree.nodes.remove(self.current_node)
self.current_node = None
class IO_Filter:
def __init__(self):
self.type = ''
self.inputs = []
self.outputs = []
class All_Nodes:
def __init__(self, context, graph):
self.index = 0
self.all_types = []
self.types = []
self.current_node = None
self.cat_group = {}
# Ctrl Scroll
self.ctrl_scroll_index = 0
# Filters
self.__filter_str = ""
self.cat_key = None
self.input_type = 'ANY'
self.output_type = 'ANY'
self.io_data = []
self.all_input_types = ['ANY']
self.all_output_types = ['ANY']
# Node Types
category_data = node_categories(context)
for key, val in category_data.items():
self.cat_group[key] = []
for node_type in val:
if not hasattr(bpy.types, node_type): continue
self.all_types.append(node_type)
self.cat_group[key].append(node_type)
self.types = self.all_types[:]
# IO Filter
for node_type in self.all_types:
node = graph.tree.nodes.new(node_type)
io_filter = IO_Filter()
io_filter.type = node_type
io_filter.inputs = [i.type for i in node.inputs]
io_filter.outputs = [o.type for o in node.outputs]
self.io_data.append(io_filter)
graph.tree.nodes.remove(node)
for io_filter in self.io_data:
for i in io_filter.inputs:
if i not in self.all_input_types:
self.all_input_types.append(i)
for o in io_filter.outputs:
if o not in self.all_output_types:
self.all_output_types.append(o)
@property
def filter_str(self):
return self.__filter_str
@filter_str.setter
def filter_str(self, val):
self.__filter_str = val
self.index = 0
self.types = []
self.cat_key = None
for t in self.all_types:
t_name = getattr(bpy.types, t).bl_rna.name
if self.__filter_str.lower() in t_name.lower():
self.types.append(t)
# Nothing Found : RESET
if not self.types:
self.__filter_str = ""
self.types = self.all_types[:]
bpy.ops.hops.display_notification(info='Nothing Found')
else:
bpy.ops.hops.display_notification(info=F'Found : {len(self.types)} Nodes')
def scroll(self, context, event, scroll, graph):
if graph.active_node != None:
graph.hide_active(hide=False)
# Scroll Categories
if event.ctrl:
for index, key in enumerate(self.cat_group.keys()):
if self.ctrl_scroll_index == index:
self.set_cat_key(key)
self.index = 0
self.ctrl_scroll_index += scroll
if self.ctrl_scroll_index > len(self.cat_group) - 1:
self.ctrl_scroll_index = 0
elif self.ctrl_scroll_index < 0:
self.ctrl_scroll_index = len(self.cat_group) - 1
# Scroll Types
else:
self.index += scroll
if self.index > len(self.types) - 1:
self.index = 0
elif self.index < 0:
self.index = len(self.types) - 1
self.create(graph)
def create(self, graph):
self.remove(graph)
node_type = self.types[self.index]
self.current_node = graph.tree.nodes.new(node_type)
location = graph.location
dimensions = graph.dimensions
self.current_node.location.x = location.x
self.current_node.location.y = location.y
self.current_node.location.y -= dimensions.y + 10
def remove(self, graph):
if not self.current_node: return
graph.tree.nodes.remove(self.current_node)
self.current_node = None
# --- FORM --- #
def cat_highlight(self, val):
return self.cat_key == val
def set_cat_key(self, val):
if val not in self.cat_group: return
self.__filter_str = ""
self.cat_key = val
self.types = self.cat_group[self.cat_key][:]
bpy.ops.hops.display_notification(info=f'Scrolling {val} : {len(self.types)}')
def turn_off_cat_key(self):
self.cat_key = None
self.types = self.all_types[:]
self.index = 0
def key_active(self):
return self.cat_key != None
def set_io_filter_types(self):
self.index = 0
self.__filter_str = ""
self.cat_key = None
self.types = []
for io_filter in self.io_data:
# Compare Inputs
in_valid = False
if self.input_type != 'ANY':
if self.input_type in io_filter.inputs:
in_valid = True
else: in_valid = True
# Compare Outputs
out_valid = False
if self.output_type != 'ANY':
if self.output_type in io_filter.outputs:
out_valid = True
else: out_valid = True
if in_valid and out_valid:
if io_filter.type not in self.types:
self.types.append(io_filter.type)
if not self.types:
self.types = self.all_types[:]
bpy.ops.hops.display_notification(info='Nothing Found')
else:
bpy.ops.hops.display_notification(info=f'Found : {len(self.types)}')
def reset_to_all(self):
self.index = 0
self.types = self.all_types[:]
bpy.ops.hops.display_notification(info='Scroll All')
DESC = """Scroll Geo Nodes
Shift : Append Mode
Press H for help"""
class HOPS_OT_Cycle_Geo_Nodes(bpy.types.Operator):
bl_idname = "hops.cycle_geo_nodes"
bl_label = "Cycle Geo Nodes"
bl_description = DESC
bl_options = {"REGISTER", "UNDO", "BLOCKING"}
@classmethod
def poll(cls, context):
if context.area:
if context.area.ui_type in {'ShaderNodeTree'} or (bpy.app.version < (3, 4) and context.area.ui_type =='GeometryNodeTree'):
return True
return False
def invoke(self, context, event):
# Validate
if not validate_context(context):
return {'CANCELLED'}
# Entry
mode = Modes.RELATED
if event.shift:
mode = Modes.APPEND
# Tool Shelf
self.show_region = bpy.context.space_data.show_region_ui
bpy.context.space_data.show_region_ui = False
# Modes
self.mode = mode
self.graph = Graph(context)
self.related_nodes = Related_Nodes(context, self.graph)
self.category_nodes = Category_Nodes(context, self.graph)
self.append_nodes = Append_Nodes(context, self.graph)
self.all_nodes = All_Nodes(context, self.graph)
if self.graph.active_node == None or self.graph.active_node.select == False:
self.mode = Modes.ALL
# Form
self.form_exit = False
self.form = None
self.setup_form(context, event)
self.draw_handle_2D = bpy.types.SpaceNodeEditor.draw_handler_add(self.safe_draw_2D, (context,), 'WINDOW', 'POST_PIXEL')
# Input
self.dialogue = Dialogue(context, self.set_dialogue, help_text="Filter Input")
# Base Systems
self.master = Master(context)
self.master.only_use_fast_ui = True
self.base_controls = Base_Modal_Controls(context, event)
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
def modal(self, context, event):
self.base_controls.update(context, event)
if self.mode == Modes.ALL:
self.form.update(context, event)
skip_m_h = self.form.active()
self.master.receive_event(event, skip_m_h)
unblocked = True
if self.dialogue.active or self.form.active():
unblocked = False
if self.base_controls.pass_through:
if unblocked:
return {'PASS_THROUGH'}
elif self.base_controls.confirm:
if unblocked:
return self.confirm_exit(context)
elif self.base_controls.cancel:
if unblocked:
return self.cancel_exit(context)
if event.type == 'TAB' and event.value == 'PRESS':
if not self.dialogue.active:
if self.form.is_dot_open():
self.form.close_dot()
else:
self.mode = Modes.ALL
self.alter_form_layout()
self.form.open_dot()
self.remove_all_created()
bpy.ops.hops.display_notification(info=F'Mode : {self.mode.name}')
# Dialogue Menu : Update
if self.dialogue.active:
self.dialogue.update(event)
# Actions
if unblocked:
self.actions(context, event)
self.interface(context)
context.area.tag_redraw()
# Update parenting draw
if self.mode == Modes.CATEGORIES:
self.category_nodes.late_update()
return {"RUNNING_MODAL"}
def actions(self, context, event):
# Tool Shelf
if event.type == 'N' and event.value == 'PRESS':
return {'PASS_THROUGH'}
# All Mode
elif event.type == 'E' and event.value == 'PRESS':
self.remove_all_created()
self.mode = Modes.ALL
self.alter_form_layout()
# Cycle Nodes
elif self.base_controls.scroll:
scroll = self.base_controls.scroll
if self.mode == Modes.RELATED:
self.related_nodes.scroll(context, scroll, self.graph)
elif self.mode == Modes.CATEGORIES:
self.category_nodes.scroll(context, scroll, self.graph)
elif self.mode == Modes.APPEND:
self.append_nodes.scroll(context, event, scroll, self.graph)
elif self.mode == Modes.ALL:
self.all_nodes.scroll(context, event, scroll, self.graph)
# Append Modes
elif event.type == 'A' and event.value == 'PRESS':
if self.append_nodes.valid:
if self.graph.active_node != None:
self.remove_all_created()
self.mode = Modes.APPEND
bpy.ops.hops.display_notification(info=F'Mode : {self.mode.name}')
else:
bpy.ops.hops.display_notification(info="Invalid Active Node")
self.alter_form_layout()
# Cycle Modes
elif event.type == 'C' and event.value == 'PRESS':
self.set_modes()
self.alter_form_layout()
# Dialogue Menu : Spawn
if self.mode in {Modes.ALL, Modes.APPEND}:
if event.type == 'S' and event.value == 'PRESS':
self.dialogue.start()
# Append Mode
if self.mode == Modes.APPEND:
# Clear Filters
if event.type == 'R' and event.value == 'PRESS':
self.append_nodes.build_types(self.graph, display_count=True)
# Flip side
if event.type == 'F' and event.value == 'PRESS':
self.append_nodes.set_flip(self.graph)
def interface(self, context):
self.master.setup()
if not self.master.should_build_fast_ui(): return
# Main
win_list = []
w_append = win_list.append
# Help
help_items = {"GLOBAL" : [], "STANDARD" : []}
help_items["GLOBAL"] = [
("H", "Toggle help"),
("N", "Toggle Side Panel"),
("~", "Toggle UI Display Type"),]
h_append = help_items["STANDARD"].append
h_append(('TAB' , 'Dot UI'))
if self.related_nodes.valid:
h_append(('C', 'Cycle Modes'))
if self.mode == Modes.RELATED:
w_append('Related Mode')
if self.related_nodes.current_node:
w_append(self.related_nodes.current_node.name)
h_append(('Scroll', 'Cycle Relative'))
elif self.mode == Modes.CATEGORIES:
w_append('Category Mode')
w_append(self.category_nodes.current_category_name)
h_append(('Scroll', 'Cycle Categories'))
elif self.mode == Modes.APPEND:
w_append('Append Mode')
if self.append_nodes.current_node:
w_append(self.append_nodes.current_node.name)
h_append(('Scroll', 'Cycle All Nodes'))
h_append(('Shift', 'Cycle Ouput Socket'))
h_append(('Ctrl', 'Cycle Categories'))
h_append(('S', 'Filter Dialogue'))
h_append(('R', 'Reset Filter'))
elif self.mode == Modes.ALL:
w_append('All Mode')
if self.all_nodes.current_node:
w_append(self.all_nodes.current_node.name)
h_append(('Scroll', 'Cycle All Nodes'))
h_append(('Ctrl Scroll', 'Category Filter'))
h_append(('S', 'Filter Dialogue'))
if self.mode != Modes.APPEND:
if self.append_nodes.valid:
h_append(('A', 'Append Mode'))
h_append(('E' , 'Every Node'))
h_append(('C' , 'Cycle Modes'))
help_items["STANDARD"].reverse()
nodes_list = []
active_node = ""
if not self.form.is_dot_open():
if self.mode == Modes.ALL:
current_node = None if self.all_nodes.current_node == None else self.all_nodes.current_node.bl_rna.name
for t in self.all_nodes.types:
t_name = getattr(bpy.types, t).bl_rna.name
nodes_list.append([t_name, ""])
if current_node == t_name:
active_node = t_name
elif self.mode == Modes.APPEND:
current_node = None if self.append_nodes.current_node == None else self.append_nodes.current_node.bl_rna.name
for t in self.append_nodes.types:
t_name = getattr(bpy.types, t).bl_rna.name
nodes_list.append([t_name, ""])
if current_node == t_name:
active_node = t_name
elif self.mode == Modes.CATEGORIES:
current_node = self.category_nodes.current_category_name
for t in self.category_nodes.keys:
nodes_list.append([t, ""])
if current_node == t:
active_node = t
elif self.mode == Modes.RELATED:
current_node = None if self.related_nodes.current_node == None else self.related_nodes.current_node.bl_rna.name
for t in self.related_nodes.types:
t_name = getattr(bpy.types, t).bl_rna.name
nodes_list.append([t_name, ""])
if current_node == t_name:
active_node = t_name
self.master.receive_fast_ui(win_list=win_list, help_list=help_items, mods_list=nodes_list, active_mod_name=active_node, image="Array")
self.master.finished()
# --- FORM --- #
def setup_form(self, context, event):
self.form = form.Form(context, event, dot_open=False)
def spacer(height=10, label='', active=True):
row = self.form.row()
row.add_element(form.Spacer(height=height))
self.form.row_insert(row, label, active)
# --- All --- #
row = self.form.row()
tips = ["Turn off Categories"]
row.add_element(form.Button(
scroll_enabled=False, text='X', highlight_text='✓', tips=tips,
width=25, height=20, use_padding=False,
callback=self.all_nodes.turn_off_cat_key,
highlight_hook=self.all_nodes.key_active))
tips = ["Filter Nodes"]
row.add_element(form.Text_Input(obj=self.all_nodes, attr="filter_str", tips=tips, width=115, height=20))
self.form.row_insert(row, label='ALL', active=self.mode == Modes.ALL)
spacer(height=5, label='ALL', active=self.mode == Modes.ALL)
row = self.form.row()
group = self.categories_group(context)
self.cat_box = form.Scroll_Box(width=140, height=60, scroll_group=group, view_scroll_enabled=True)
row.add_element(self.cat_box)
self.form.row_insert(row, label="ALL", active=self.mode == Modes.ALL)
spacer(height=5, label='ALL', active=self.mode == Modes.ALL)
row = self.form.row()
row.add_element(form.Dropdown(
width=100, tips=["Inputs"],
options=self.all_nodes.all_input_types,
obj=self.all_nodes, attr='input_type'))
row.add_element(form.Button(
scroll_enabled=False, text='F', tips=["Filter"],
width=25, height=20, use_padding=False,
callback=self.all_nodes.set_io_filter_types))
self.form.row_insert(row, label='ALL', active=self.mode == Modes.ALL)
row = self.form.row()
row.add_element(form.Dropdown(
width=100, tips=["Outputs"],
options=self.all_nodes.all_output_types,
obj=self.all_nodes, attr='output_type'))
row.add_element(form.Button(
scroll_enabled=False, text='R', tips=["Reset"],
width=25, height=20, use_padding=False,
callback=self.all_nodes.reset_to_all))
self.form.row_insert(row, label='ALL', active=self.mode == Modes.ALL)
self.form.build()
def categories_group(self, context):
group = form.Scroll_Group()
index = 1
for cat, types in self.all_nodes.cat_group.items():
row = group.row()
row.add_element(form.Label(text=str(index), width=25, height=20))
tip = []
text = form.shortened_text(cat, width=70, font_size=12)
tip = tip if text == cat else [cat]
row.add_element(form.Button(
scroll_enabled=False, text=text, tips=tip,
width=95, height=20, use_padding=False,
callback=self.all_nodes.set_cat_key, pos_args=(cat,), neg_args=(cat,),
highlight_hook=self.all_nodes.cat_highlight, highlight_hook_args=(cat,)))
group.row_insert(row)
index += 1
return group
def alter_form_layout(self):
if self.mode == Modes.RELATED:
self.form.row_activation(label='RELATED', active=True)
self.form.row_activation(label='CATEGORIES', active=False)
self.form.row_activation(label='APPEND', active=False)
self.form.row_activation(label='ALL', active=False)
elif self.mode == Modes.CATEGORIES:
self.form.row_activation(label='RELATED', active=False)
self.form.row_activation(label='CATEGORIES', active=True)
self.form.row_activation(label='APPEND', active=False)
self.form.row_activation(label='ALL', active=False)
elif self.mode == Modes.APPEND:
self.form.row_activation(label='RELATED', active=False)
self.form.row_activation(label='CATEGORIES', active=False)
self.form.row_activation(label='APPEND', active=True)
self.form.row_activation(label='ALL', active=False)
elif self.mode == Modes.ALL:
self.form.row_activation(label='RELATED', active=False)
self.form.row_activation(label='CATEGORIES', active=False)
self.form.row_activation(label='APPEND', active=False)
self.form.row_activation(label='ALL', active=True)
self.form.build(preserve_top_left=True)
if self.mode != Modes.ALL:
if self.form.is_dot_open():
self.form.close_dot()
# --- UTILS --- #
def remove_all_created(self):
self.related_nodes.remove(self.graph)
self.category_nodes.remove(self.graph)
self.append_nodes.remove(self.graph)
self.all_nodes.remove(self.graph)
def set_modes(self):
self.remove_all_created()
# No Active node
if self.graph.active_node == None:
if self.mode == Modes.CATEGORIES:
self.mode = Modes.ALL
else:
self.mode = Modes.CATEGORIES
return
if self.mode == Modes.RELATED:
self.mode = Modes.CATEGORIES
elif self.mode == Modes.CATEGORIES:
self.mode = Modes.APPEND
elif self.mode == Modes.APPEND:
self.mode = Modes.ALL
elif self.mode == Modes.ALL:
self.mode = Modes.RELATED
if self.mode == Modes.APPEND:
if not self.append_nodes.valid:
self.mode = Modes.CATEGORIES
bpy.ops.hops.display_notification(info=F'Mode : {self.mode.name}')
def set_dialogue(self, string):
if self.mode == Modes.APPEND:
self.append_nodes.filter_types(self.graph, string)
elif self.mode == Modes.ALL:
self.all_nodes.filter_str = string
# --- EXITS --- #
def common_exit(self, context):
self.remove_shaders()
self.form.shut_down(context)
self.master.run_fade()
bpy.context.space_data.show_region_ui = self.show_region
context.area.tag_redraw()
def confirm_exit(self, context):
self.graph.hide_active(hide=False)
self.common_exit(context)
if self.mode == Modes.ALL:
if self.all_nodes.current_node:
bpy.ops.node.select_all(action='DESELECT')
self.all_nodes.current_node.select = True
self.graph.tree.nodes.active = self.all_nodes.current_node
elif self.mode == Modes.APPEND:
if self.append_nodes.current_node:
bpy.ops.node.select_all(action='DESELECT')
self.append_nodes.current_node.select = True
self.graph.tree.nodes.active = self.append_nodes.current_node
elif self.mode == Modes.RELATED:
if self.related_nodes.current_node:
bpy.ops.node.select_all(action='DESELECT')
self.related_nodes.current_node.select = True
self.graph.tree.nodes.active = self.related_nodes.current_node
return {'FINISHED'}
def cancel_exit(self, context):
self.remove_all_created()
self.graph.hide_active(hide=False)
self.common_exit(context)
return {'CANCELLED'}
# --- SHADERS --- #
def remove_shaders(self):
if self.draw_handle_2D:
self.draw_handle_2D = bpy.types.SpaceNodeEditor.draw_handler_remove(self.draw_handle_2D, "WINDOW")
def safe_draw_2D(self, context):
method_handler(self.draw_shader_2D,
arguments = (context,),
identifier = 'Modal Shader 2D',
exit_method = self.remove_shaders)
def draw_shader_2D(self, context):
if self.mode == Modes.ALL:
self.form.draw()
if self.mode in {Modes.ALL, Modes.APPEND}:
self.dialogue.draw()
# --- UTILS --- #
def validate_context(context):
valid_context = False
if hasattr(context, 'area'):
if hasattr(context.area, 'spaces'):
if hasattr(context.area.spaces, 'active'):
if hasattr(context.area.spaces.active, 'edit_tree'):
valid_context = True
return valid_context