Sequencer feature revival: show all clips' start/end frames during grab

One of my favorite Blender 2.x features is gone in 3.x, so I am attempting to revive it. :^)

In the sequencer, we used to see start/end frames of all clips when grabbing one or more others. (Ignore the “ripple” happening here, I’m just after the frame numbers.)

This made it very easy to line up text or adjustment layers, compared to the current tedium of adjusting, zooming, adjusting further, zooming again, etc.

I’ve made a start, but have run into two roadblocks:

  • how do I determine the on-screen position/width/height of a sequence strip?
  • how do i determine whether anything is actively being grabbed?

The current code draws start:end frames of one selected strip, whether or not it’s been grabbed. They show at x,y coordinates which are influenced by the strip’s on-screen position, but clearly not in a correct/robust way.

Any advice would be most welcome. Thanks in advance!

import bpy
import blf

class DrawingClass:
    def __init__(self, context):
        self.handle = bpy.types.SpaceSequenceEditor.draw_handler_add(
                   self.draw_text_callback,(context,),
                   'WINDOW', 'POST_PIXEL')

    def draw_text_callback(self, context):
        # TODO: how to tell if we're moving a strip(s)?
        # we only want these numbers if something's grabbed
        
        font_id = 0  # XXX, need to find out how best to get this.
        blf.size(font_id, 12, 72)

        # FIXME: how to get strip (sequence) positions?
        # terrible position hack, to get numbers moving at all
        sel = bpy.context.selected_sequences[0]
        blf.position(font_id, sel.frame_start*2, sel.channel*50, 0)
        blf.draw(font_id, "%d : %d" % (sel.frame_final_start, sel.frame_final_end))

        # TODO: draw for other sequences too
        #bpy.context.scene.sequence_editor.sequences.items()

    def remove_handle(self):
         bpy.types.SpaceSequenceEditor.draw_handler_remove(self.handle, 'WINDOW')

# bind it up
dns = bpy.app.driver_namespace
dns["dc"] = DrawingClass(bpy.context)

# in console, to clean up:
#bpy.app.driver_namespace.get("dc").remove_handle()
2 Likes

Strips, unlike ui widgets, are immediate mode rendered - there are no rectangles or retained data to grab coordinates from. You would have to compute the view2d positions yourself.

You can’t, unless you are fine with using a hack where you shadow the transform operator directly interface with the C api. See example.

This adds frame offsets to unselected frames during move operator. Tested on Blender 3.2.1 3.5.0 beta.

import bpy
import blf

# The strip start/end frame draw callback
def draw():
    if not detect_strip_move_modal():
        return

    region = bpy.context.region
    v2d = region.view2d

    system = bpy.context.preferences.system
    margin_x = 9.6 * system.pixel_size

    # Get the view2d rectangle.
    x1, y1 = v2d.region_to_view(0, 0)
    x2, y2 = v2d.region_to_view(region.width, region.height)

    # View-to-screen transform factors
    ay = region.height / (y2 - y1)
    ax = region.width / (x2 - x1)

    for s in bpy.context.sequences:
        # Skip selection as they draw their own frame numbers.
        if s.select:
            continue

        frame_start = s.frame_final_start
        frame_end = s.frame_final_end
        rtext_width = blf.dimensions(0, str(frame_end))[0]

        # Not pixel perfect aligned, but "good enough"
        y = (s.channel + 0.05 + 0.09 - y1) * ay
        x_left = int((frame_start - x1) * ax + margin_x)
        x_right = int((frame_end - x1) * ax - margin_x - rtext_width)

        blf.color(0, 1.0, 1.0, 1.0, 1.0)

        blf.position(0, x_left, y, 0)
        blf.draw(0, str(frame_start))

        blf.position(0, x_right, y, 0)
        blf.draw(0, str(frame_end))



# -------- Detect modal boilerplate start -------

from ctypes import *

# Handler type enum. Operator is 3
WM_HANDLER_TYPE_OP = 3

version = bpy.app.version
functype = type(lambda: None)
as_ptr = bpy.types.bpy_struct.as_pointer


class ListBase(Structure):
    _cache = {}
    _fields_ = (("first", c_void_p), ("last",  c_void_p))
    def __new__(cls, c_type=None):
        if c_type in cls._cache: return cls._cache[c_type]
        elif c_type is None: ListBase_ = cls
        else:
            class ListBase_(Structure):
                __name__ = __qualname__ = f"ListBase{cls.__qualname__}"
                _fields_ = (("first", POINTER(c_type)),
                            ("last",  POINTER(c_type)))
                __iter__ = cls.__iter__
                __bool__ = cls.__bool__
        return cls._cache.setdefault(c_type, ListBase_)

    def __iter__(self):
        links_p = []
        elem_n = self.first or self.last
        elem_p = elem_n and elem_n.contents.prev
        if elem_p:
            while elem_p:
                links_p.append(elem_p.contents)
                elem_p = elem_p.contents.prev
            yield from reversed(links_p)
        while elem_n:
            yield elem_n.contents
            elem_n = elem_n.contents.next

    def __bool__(self):
        return bool(self.first or self.last)


class StructBase(Structure):
    _structs = []
    __annotations__ = {}

    def __init_subclass__(cls):
        cls._structs.append(cls)

    def __new__(cls, srna=None):
        if srna is None:
            return super().__new__(cls)
        try:
            return cls.from_address(as_ptr(srna))
        except AttributeError:
            raise Exception("Not a StructRNA instance")

    # Required
    def __init__(self, *_):
        pass


# source\blender\windowmanager\wm_event_system.h
class wmEventHandler(StructBase):
    next:   lambda: POINTER(wmEventHandler)
    prev:   lambda: POINTER(wmEventHandler)
    type:   c_int
    flag:   c_char
    poll:   c_void_p


# source\blender\makesdna\DNA_windowmanager_types.h
class wmWindow(StructBase):
    next:                   lambda: POINTER(wmWindow)
    prev:                   lambda: POINTER(wmWindow)
    ghostwin:               c_void_p
    gpuctx:                 c_void_p
    parent:                 lambda: POINTER(wmWindow)
    scene:                  c_void_p
    new_scene:              c_void_p
    view_layer_name:        c_char * 64

    if version >= (3, 3):
        unpinned_scene:     c_void_p        # Scene

    workspace_hook:         c_void_p
    global_areas:           ListBase * 3    # ScrAreaMap
    screen:                 c_void_p        # bScreen (deprecated)

    if version >= (2, 93):
        winid:              c_int

    pos:                    c_short * 2
    size:                   c_short * 2
    windowstate:            c_char
    active:                 c_char

    if version <= (2, 93):
        _pad0:              c_char * 4

    cursor:                 c_short
    lastcursor:             c_short
    modalcursor:            c_short
    grabcursor:             c_short
    addmousemove:           c_char
    tag_cursor_refresh:     c_char

    if version > (2, 93):
        event_queue_check_click:        c_char
        event_queue_check_drag:         c_char
        event_queue_check_drag_handled: c_char
        _pad0:                          c_char * 1

    else:
        winid:              c_int

    pie_event_type_lock:    c_short
    pie_event_type_last:    c_short
    eventstate:             c_void_p  # wmEvent

    if version >= (3, 2):
        event_last_handled: c_void_p  # wmEvent

    else:
        tweak:              c_void_p # wmGesture

    ime_data:               c_void_p
    event_queue:            ListBase
    handlers:               ListBase(wmEventHandler)
    modalhandlers:          ListBase(wmEventHandler)
    gesture:                ListBase
    stereo3d_format:        c_void_p
    drawcalls:              ListBase
    cursor_keymap_status:   c_void_p


class wmOperator(StructBase):
    next:               lambda: POINTER(wmOperator)
    prev:               lambda: POINTER(wmOperator)
    idname:             c_char * 64


# source\blender\windowmanager\wm_event_system.h
class wmEventHandler_Op(StructBase):

    class context(StructBase):  # Anonymous
        win:            lambda: POINTER(wmWindow)
        area:           c_void_p        # ScrArea ptr
        region:         c_void_p        # ARegion ptr
        region_type:    c_short

    head:               wmEventHandler
    op:                 lambda: POINTER(wmOperator)        # wmOperator
    is_file_select:     c_bool
    context:            context

    del context


def init_structs():
    for struct in StructBase._structs:
        fields = []
        anons = []
        for key, value in struct.__annotations__.items():
            if isinstance(value, functype):
                value = value()
            elif isinstance(value, Union):
                anons.append(key)
            fields.append((key, value))

        if anons:
            struct._anonynous_ = anons

        # Base classes might not have _fields_. Don't set anything.
        if fields:
            struct._fields_ = fields
        struct.__annotations__.clear()

    StructBase._structs.clear()
    ListBase._cache.clear()

# ------- Detect modal boilerplate end -------


def detect_strip_move_modal():
    win = wmWindow(bpy.context.window)

    for handler in win.modalhandlers:
        if handler.type == WM_HANDLER_TYPE_OP:
            wmh_op = wmEventHandler_Op.from_address(addressof(handler))
            idname = wmh_op.op.contents.idname.decode()
            if idname == "TRANSFORM_OT_seq_slide":
                return True
    return False

if __name__ == "__main__":
    init_structs()
    
    # The draw handler is persistent for the duration of session.
    bpy.types.SpaceSequenceEditor.draw_handler_add(
        draw, (), 'WINDOW', 'POST_PIXEL')

4 Likes

Works great, good enough for me :slight_smile: I’m going to add it to my start-up script in my default file

Oh, actually, this code doesn’t work on anything past 3.3 LTS:

Python: Traceback (most recent call last):
File “\Text”, line 125, in
File “\Text”, line 111, in register
RuntimeError: Error: Registering operator class: ‘TRANSFORM_OT_seq_slide’, bl_idname ‘transform.seq_slide’ could not be unregistered

I pasted in a new text document in the Text Editor and clicked Run. Using Blender 3.5a or 3.4.1, same error. It does work on 3.3 LTS. Any idea why this might be?

Thanks for the heads-up. I’ll download a more recent build and test.

1 Like

I was afraid of this. Ok, so the recent changes to Blender made it so internal operators no longer can be shadowed. It just means more boilerplate code.

I wrote a script a while back that detects running modals, but it didn’t have the ability to find a specific modal operator. I’ve updated the code in the above post with a version of the script that does.

As far as versioning goes, the script works up to 3.5.0 beta. Updates to this due to C API breakage can easily be handled using if-statements.

Let me know if you run into any issues.

2 Likes

Incredible! Thank you so very much, @iceythe !
I’m using 3.4.1 and with the tiniest of tweaks, your script does everything I desired!

The only changes I made were to comment out lines 25 and 26 (blender’s own numbers on the selected strip don’t show for me since I have most of the overlays turned off, for maximum size waveforms/curves) and to change line 29 to:
frame_end = s.frame_final_end - 1
so that the “last frame” represents the final frame for which the strip is still visible, rather than the first frame it’s not. I had that wrong in my original experiment too, but didn’t notice it until seeing strips pushed against each other sharing the same number for end/start frames :^)

Thanks again @iceythe , you made my day!

1 Like