OverviewFilesCommitsIssuesDocsReleasesPeers

cthuloid / blender-addon / cthuloid_lights / ui.py

282 lines · python ref:
"""N-panel UI in the 3D viewport, tab "Cthuloid"."""

import bpy
from bpy.types import Panel, UIList

from . import protocol, rig
from .net import SENDER


class CTHULOID_UL_fixtures(UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_prop, index):
        row = layout.row(align=True)
        row.prop(item, "enabled", text="")
        row.label(text=item.host or "(no host)", icon='LIGHT_DATA')
        text, icon = fixture_state(context.scene, item)
        sub = row.row()
        sub.alert = icon == 'ERROR'
        sub.label(text=text, icon=icon)


def fixture_state(scene, fixture):
    """(words, icon) for one fixture, as the user should read it."""
    if not fixture.enabled:
        return "off", 'BLANK1'
    if not scene.cthuloid.live:
        return "not connected", 'RADIOBUT_OFF'
    status, note = SENDER.status_of(fixture.host, fixture.port)
    if status == "connected" and "held by" in note:
        # Shared control: someone else holds a slot ("slot 2 held by alice").
        return note, 'LOCKED'
    return {
        "connected":   ("connected", 'RADIOBUT_ON'),
        "connecting":  ("connecting...", 'SORTTIME'),
        "idle":        ("connecting...", 'SORTTIME'),
        "unreachable": ("unreachable", 'ERROR'),
        "lost":        ("unreachable", 'ERROR'),
        "duplicate":   ("same device twice", 'ERROR'),
        "error":       ("error", 'ERROR'),
    }.get(status, (status, 'QUESTION'))


class CTHULOID_MT_tools(bpy.types.Menu):
    """Occasional operations. Everything here is either a one-time migration or
    a recovery action -- normal output is automatic while connected."""
    bl_label = "Tools"
    bl_idname = "CTHULOID_MT_tools"

    def draw(self, context):
        layout = self.layout
        layout.operator("cthuloid.send_now", icon='FILE_REFRESH')
        layout.separator()
        layout.operator("cthuloid.adopt_scene", icon='IMPORT')


class CthuloidPanelBase:
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = "Cthuloid"


class CTHULOID_PT_main(CthuloidPanelBase, Panel):
    bl_label = "Fixtures"

    def draw(self, context):
        layout = self.layout
        props = context.scene.cthuloid

        # One switch decides everything. Red when on, like auto-keying, so a
        # hot rig is unmistakable; words rather than PLAY/PAUSE icons, which
        # collided with the timeline's own transport controls.
        row = layout.row(align=True)
        row.scale_y = 1.6
        row.alert = props.live
        row.prop(props, "live", toggle=True,
                 text="CONNECTED - live (click to disconnect)" if props.live else "CONNECT",
                 icon='REC' if props.live else 'RADIOBUT_OFF')
        row.operator("cthuloid.blackout", text="", icon='CANCEL')

        row = layout.row()
        row.template_list("CTHULOID_UL_fixtures", "", props, "fixtures", props, "active_index", rows=3)

        col = row.column(align=True)
        col.operator("cthuloid.add_fixture", text="", icon='ADD')
        col.operator("cthuloid.remove_fixture", text="", icon='REMOVE')
        col.separator()
        col.menu("CTHULOID_MT_tools", text="", icon='DOWNARROW_HLT')

        if props.live:
            layout.label(text="Edits and playback go straight to the lights")
        else:
            layout.label(text="Not connected: nothing is sent to the lights")


class CTHULOID_PT_fixture(CthuloidPanelBase, Panel):
    bl_label = "Fixture"
    bl_parent_id = "CTHULOID_PT_main"

    @classmethod
    def poll(cls, context):
        props = context.scene.cthuloid
        return 0 <= props.active_index < len(props.fixtures)

    def draw(self, context):
        layout = self.layout
        layout.use_property_split = True
        props = context.scene.cthuloid
        fixture = props.fixtures[props.active_index]

        col = layout.column(align=True)
        col.prop(fixture, "host")
        col.prop(fixture, "port")

        text, icon = fixture_state(context.scene, fixture)
        box = layout.box()
        box.alert = icon == 'ERROR'
        box.label(text=text, icon=icon)
        _status, err = SENDER.status_of(fixture.host, fixture.port)
        if err and props.live and fixture.enabled:
            box.label(text=err)

        layout.separator()
        col = layout.column(align=True)
        col.prop(fixture, "red_max_pwm")
        if fixture.red_max_pwm > protocol.FIRMWARE_RED_MAX_PWM:
            col.label(text="Above firmware limit; command will be rejected", icon='ERROR')

        col = layout.column(align=True)
        col.prop(fixture, "use_energy")
        sub = col.column()
        sub.enabled = fixture.use_energy
        sub.prop(fixture, "energy_ref")

        layout.separator()
        col = layout.column(align=True)
        col.prop(fixture, "fade_mode")
        sub = col.column()
        sub.enabled = fixture.fade_mode == 'FIXED'
        sub.prop(fixture, "fade_ms")


class CTHULOID_PT_hierarchy(CthuloidPanelBase, Panel):
    bl_label = "Hierarchy"
    bl_parent_id = "CTHULOID_PT_main"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        props = context.scene.cthuloid
        return 0 <= props.active_index < len(props.fixtures)

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        props = scene.cthuloid
        fixture = props.fixtures[props.active_index]

        lights = rig.fixture_lights(scene, fixture)
        col = layout.column(align=True)
        for slot in range(1, protocol.SLOTS_PER_FIXTURE + 1):
            obj = lights.get(slot)
            row = col.row(align=True)
            row.label(text="Slot %d" % slot)
            if obj is None:
                row.label(text="empty -> 0 0 0", icon='RADIOBUT_OFF')
            else:
                row.label(text=obj.name, icon='LIGHT_DATA')

        for slot, a, b in rig.slot_conflicts(scene, fixture):
            layout.label(text="Slot %d claimed by %s and %s" % (slot, a, b), icon='ERROR')

        layout.separator()
        col = layout.column(align=True)
        col.operator("cthuloid.rebuild_rig", icon='FILE_REFRESH')
        col.operator("cthuloid.assign_light", icon='LINKED')
        col.operator("cthuloid.sync_names", icon='SORTALPHA')


class CTHULOID_PT_firmware(CthuloidPanelBase, Panel):
    bl_label = "Firmware Settings"
    bl_parent_id = "CTHULOID_PT_main"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        props = context.scene.cthuloid
        return 0 <= props.active_index < len(props.fixtures)

    def draw(self, context):
        layout = self.layout
        layout.use_property_split = True
        props = context.scene.cthuloid
        fixture = props.fixtures[props.active_index]

        col = layout.column(align=True)
        col.prop(fixture, "gamma_b")
        col.prop(fixture, "gamma_g")
        col.prop(fixture, "gamma_r")
        layout.prop(fixture, "freq")
        layout.operator("cthuloid.push_settings", icon='EXPORT')


class CTHULOID_PT_output(CthuloidPanelBase, Panel):
    bl_label = "Output"
    bl_parent_id = "CTHULOID_PT_main"
    bl_options = {'DEFAULT_CLOSED'}

    def draw(self, context):
        layout = self.layout
        layout.use_property_split = True
        props = context.scene.cthuloid
        col = layout.column(align=True)
        col.prop(props, "send_on_frame")
        col.prop(props, "send_on_edit")
        col.prop(props, "max_hz")


class CTHULOID_PT_light(CthuloidPanelBase, Panel):
    bl_label = "Cthuloid Light"
    bl_context = "objectmode"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(cls, context):
        return context.object is not None and context.object.type == 'LIGHT'

    def draw(self, context):
        layout = self.layout
        layout.use_property_split = True
        obj = context.object
        lp = obj.cthuloid_light
        layout.prop(lp, "is_fixture_light")
        col = layout.column(align=True)
        col.enabled = lp.is_fixture_light
        col.prop(lp, "slot")

        fixture = None
        for f in context.scene.cthuloid.fixtures:
            if f.id == lp.fixture_id:
                fixture = f
                break
        col.label(text="Fixture: %s" % (fixture.host if fixture else "unassigned"))


CLASSES = (
    CTHULOID_UL_fixtures,
    CTHULOID_MT_tools,
    CTHULOID_PT_main,
    CTHULOID_PT_fixture,
    CTHULOID_PT_hierarchy,
    CTHULOID_PT_firmware,
    CTHULOID_PT_output,
    CTHULOID_PT_light,
)


# Fixture status changes on the worker threads, which cannot redraw a panel,
# so without this the words above only update when the mouse moves over them.
_REDRAW_INTERVAL = 0.5


def _redraw_tick():
    wm = bpy.context.window_manager
    if wm is not None and SENDER.has_targets():
        for window in wm.windows:
            for area in window.screen.areas:
                if area.type == 'VIEW_3D':
                    area.tag_redraw()
    return _REDRAW_INTERVAL


def register():
    for cls in CLASSES:
        bpy.utils.register_class(cls)
    bpy.app.timers.register(_redraw_tick, first_interval=_REDRAW_INTERVAL, persistent=True)


def unregister():
    if bpy.app.timers.is_registered(_redraw_tick):
        bpy.app.timers.unregister(_redraw_tick)
    for cls in reversed(CLASSES):
        bpy.utils.unregister_class(cls)