import obspython as obs
import ntpath
NUM_BUTTONS = 27
# Per-button configuration
button_sources = [None] * NUM_BUTTONS # Source name for each button
button_inverts = [False] * NUM_BUTTONS # Invert flag per button
hotkey_ids = [obs.OBS_INVALID_HOTKEY_ID] * NUM_BUTTONS
hotkey_names = [""] * NUM_BUTTONS
# Global state for reference counting
active_counts = {} # source_name -> int: number of pressed hotkeys affecting it
target_visibility = {} # source_name -> bool: desired visibility when count > 0
rest_visibility = {} # source_name -> bool: desired visibility when count == 0
def script_description():
return "<b>Push to Enable - 27 Buttons (with Reference Counting)</b>\n\n" \
"Each hotkey enables (or disables if inverted) its assigned source while held.\n" \
"Multiple hotkeys can control the same source — it stays active as long as " \
"at least one assigned hotkey is pressed.\n\n" \
"Settings can be changed without interrupting active hotkeys."
def script_load(settings):
print("--- " + ntpath.basename(__file__) + " loaded ---")
for i in range(NUM_BUTTONS):
hk_name = f"push_to_enable_button_{i+1}"
hk_desc = f"Push to Enable Button {i+1}"
hotkey_names[i] = hk_name
hotkey_ids[i] = obs.obs_hotkey_register_frontend(
hk_name, hk_desc,
lambda pressed, idx=i: hotkey_callback(pressed, idx)
)
saved_array = obs.obs_data_get_array(settings, hk_name)
if saved_array is not None:
obs.obs_hotkey_load(hotkey_ids[i], saved_array)
obs.obs_data_array_release(saved_array)
def script_save(settings):
for i in range(NUM_BUTTONS):
save_array = obs.obs_hotkey_save(hotkey_ids[i])
obs.obs_data_set_array(settings, hotkey_names[i], save_array)
obs.obs_data_array_release(save_array)
def script_properties():
props = obs.obs_properties_create()
for i in range(NUM_BUTTONS):
# Source dropdown
list_prop = obs.obs_properties_add_list(
props, f"source_{i}", f"Button {i+1} Source",
obs.OBS_COMBO_TYPE_LIST, obs.OBS_COMBO_FORMAT_STRING
)
obs.obs_property_list_add_string(list_prop, "(none)", "")
sources_list = obs.obs_enum_sources()
if sources_list:
for src in sources_list:
name = obs.obs_source_get_name(src)
obs.obs_property_list_add_string(list_prop, name, name)
obs.source_list_release(sources_list)
# Invert checkbox
obs.obs_properties_add_bool(props, f"invert_{i}", f"Invert Button {i+1} (hide while pressed)")
return props
def script_update(settings):
global button_sources, button_inverts
global active_counts, target_visibility, rest_visibility
# Read new configuration
new_sources = []
new_inverts = []
for i in range(NUM_BUTTONS):
src_name = obs.obs_data_get_string(settings, f"source_{i}")
src_name = None if src_name == "" or src_name == "(none)" else src_name
new_sources.append(src_name)
new_inverts.append(obs.obs_data_get_bool(settings, f"invert_{i}"))
# Track old sources for cleanup
old_sources = set(active_counts.keys())
# Update button config
button_sources = new_sources
button_inverts = new_inverts
# Rebuild visibility mappings
target_visibility.clear()
rest_visibility.clear()
for i in range(NUM_BUTTONS):
src = button_sources[i]
if src is None:
continue
invert = button_inverts[i]
target_visibility[src] = not invert # Visible when active (normal), hidden if inverted
rest_visibility[src] = invert # Hidden at rest (normal), visible if inverted
# Update active_counts for currently assigned sources
current_sources = set(target_visibility.keys())
for src in current_sources:
if src not in active_counts:
active_counts[src] = 0 # New source, start at 0
# Clean up sources no longer assigned to any button
removed_sources = old_sources - current_sources
for src in removed_sources:
# If count is already 0, force rest state (safe to hide)
if active_counts[src] == 0:
set_source_visibility(src, False)
# If count > 0, leave it alone — some hotkey is still conceptually held
del active_counts[src]
# IMPORTANT: Do NOT force visibility changes on currently assigned sources
# This preserves visibility when holding hotkeys while changing settings
def hotkey_callback(pressed, idx):
src_name = button_sources[idx]
if src_name is None:
return
# Ensure source is in our tracking dicts (in case config changed mid-use)
if src_name not in active_counts:
active_counts[src_name] = 0
# Use current button's invert setting as fallback
target_visibility[src_name] = not button_inverts[idx]
rest_visibility[src_name] = button_inverts[idx]
if pressed:
prev_count = active_counts[src_name]
active_counts[src_name] += 1
# Only change visibility if transitioning from 0 to 1
if prev_count == 0:
set_source_visibility(src_name, target_visibility.get(src_name, False))
else:
if active_counts[src_name] > 0:
active_counts[src_name] -= 1
# Only change visibility if dropping to 0
if active_counts[src_name] == 0:
set_source_visibility(src_name, rest_visibility.get(src_name, False))
def set_source_visibility(source_name, visible):
if source_name is None:
return
scene_sources = obs.obs_frontend_get_scenes()
if scene_sources is None:
return
for scn_src in scene_sources:
scene = obs.obs_scene_from_source(scn_src)
items = obs.obs_scene_enum_items(scene)
if items:
for item in items:
item_src = obs.obs_sceneitem_get_source(item)
if obs.obs_source_get_name(item_src) == source_name:
if obs.obs_sceneitem_visible(item) != visible:
obs.obs_sceneitem_set_visible(item, visible)
obs.sceneitem_list_release(items)
obs.source_list_release(scene_sources)