How to Get the Selected Keyframes in Active Timeline/FCurve Manager

How would you find these selected keyframes?
0_1536620777786_60e72919-26ca-492c-a3a9-008da5740d27-image.png

I didn't see a super clean forum post on the subject, so I figured I would write up my findings as I worked this out. Turns out what I thought was going to be a tutorial, is actually a question for the SDK Team.

Open Questions

  1. How do you determine which Timeline is currently active?
  2. How can you detect selected keys in the F-Curve manager?
    • track.GetNBit(c4d.NBIT_TL1_FCSELECT) doesn't seem to be working.
  3. How do you determine if an object/track/keyframe is visible in the Active timeline? I don't want to act on hidden objects as the user wouldn't expect that to happen.

Getting Selected Keyframes

Well, unlike Objects and Materials, we don't have any helper functions like BaseDocument.GetActiveObjects() and BaseDocument.GetActiveMaterials(). So, we have to:

  1. Iterate through every object in the scene.
def GetNextObject(op):
    """Gets the next object in the scene graph after `op`. Does not stop until all objects are returned.
    From: https://c4dprogramming.wordpress.com/2012/11/26/non-recursive-hierarchy-iteration/
    """

    if op==None:
        return None

    if op.GetDown():
        return op.GetDown()

    while not op.GetNext() and op.GetUp():
        op = op.GetUp()

    return op.GetNext()

def IterAllObjects(doc):
    """Iterates through all objects in `doc`.
    Call like:
    for obj in IterAllObjects(doc):
        print obj.GetName()
    """

    if doc is None:
        return

    op = doc.GetFirstObject()
    while op is not None:
        yield op
        op = GetNextObject(op)
  1. Iterate through each object's tracks.
def IterAllTracks(doc):
    """Iterates through all animation tracks in `doc`.
    Call like:
    for track in IterAllTracks(doc):
        print track.GetName()
    """
    
    if doc is None:
        return

    for obj in IterAllObjects(doc):
        for track in obj.GetCTracks():
            yield track
  1. Get the track's Curve.
curve = track.GetCurve()
if curve is None:
    continue
  1. Iterate through each Curve's keys.
for key_id in xrange(curve.GetKeyCount()):
    key = curve.GetKey(key_id)
  1. Test each key to see if it is selected.
    is_selected = key.GetNBit(timeline_bit)  # Typically: c4d.NBIT_TL1_SELECT
  1. Push selected keys into a list.
  2. Return the list.

Current Script

"""Name-en-US: Print Selected Keys
Description-en-US: Prints the frame and value of the selected keys to the console."""

import c4d
from c4d import gui

def GetNextObject(op):
    """Gets the next object in the scene graph after `op`. Does not stop until all objects are returned.
    From: https://c4dprogramming.wordpress.com/2012/11/26/non-recursive-hierarchy-iteration/
    """

    if op==None:
        return None

    if op.GetDown():
        return op.GetDown()

    while not op.GetNext() and op.GetUp():
        op = op.GetUp()

    return op.GetNext()

def IterAllObjects(doc):
    """Iterates through all objects in `doc`.
    Call like:
    for obj in IterAllObjects(doc):
        print obj.GetName()
    """

    if doc is None:
        return

    op = doc.GetFirstObject()
    while op is not None:
        yield op
        op = GetNextObject(op)

def IterAllTracks(doc):
    """Iterates through all animation tracks in `doc`.
    Call like:
    for track in IterAllTracks(doc):
        print track.GetName()
    """
    
    if doc is None:
        return

    for obj in IterAllObjects(doc):
        for track in obj.GetCTracks():
            yield track

def GetActiveTracks(doc, timeline_bit=c4d.NBIT_TL1_SELECT):
    """Returns the active tracks based on `selection_bit`.
    
    NBIT Reference:
    https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d/C4DAtom/GeListNode/index.html?highlight=getnbit#GeListNode.GetNBit
    
    Timeline 1: c4d.NBIT_TL1_SELECT,
    Timeline 2: c4d.NBIT_TL2_SELECT,
    Timeline 3: c4d.NBIT_TL3_SELECT,
    Timeline 4: c4d.NBIT_TL4_SELECT,
    
    F-Curve 1: c4d.NBIT_TL1_FCSELECT,
    F-Curve 2: c4d.NBIT_TL2_FCSELECT,
    F-Curve 3: c4d.NBIT_TL3_FCSELECT,
    F-Curve 4: c4d.NBIT_TL4_FCSELECT
    
    Issues:
    * No way (that I'm aware of) to detect which timeline is active.
    * No way to determine whether in Timeline or FCurve mode.
    * NBIT_TL1_FCSELECT seems to return incorrect values.
    * Not aware of whether track is visible/hidden in Timeline or not.
    """

    if doc is None:
        return
    
    active_tracks = []

    for track in IterAllTracks(doc):
        if track.GetNBit(timeline_bit):
            active_tracks.append(track)
            
    return active_tracks

def GetActiveKeys(doc, timeline_bit=c4d.NBIT_TL1_SELECT):
    """Returns all active keys.
    
    NBIT Reference:
    https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d/C4DAtom/GeListNode/index.html?highlight=getnbit#GeListNode.GetNBit
    
    Timeline 1: c4d.NBIT_TL1_SELECT,
    Timeline 2: c4d.NBIT_TL2_SELECT,
    Timeline 3: c4d.NBIT_TL3_SELECT,
    Timeline 4: c4d.NBIT_TL4_SELECT,
    
    F-Curve 1: c4d.NBIT_TL1_FCSELECT,
    F-Curve 2: c4d.NBIT_TL2_FCSELECT,
    F-Curve 3: c4d.NBIT_TL3_FCSELECT,
    F-Curve 4: c4d.NBIT_TL4_FCSELECT
    """
    
    active_keys = []

    for track in IterAllTracks(doc):
        curve = track.GetCurve()
        if curve is None:
            continue

        for key_id in xrange(curve.GetKeyCount()):
            key = curve.GetKey(key_id)
            is_selected = key.GetNBit(timeline_bit)
            if (key is not None) and is_selected:
                active_keys.append(key)

    return active_keys

def main():
    """Print the frame and value of the currently selected keys to the Python Console.
    """
    
    print " "
    print " "
    print "Active Keys"
    print "==========="

    active_keys = GetActiveKeys(doc)
    if active_keys:
        for key in GetActiveKeys(doc, c4d.NBIT_TL1_SELECT):
            print key.GetTime().GetFrame(doc.GetFps()), ": ", key.GetValue()
    else:
        print "No active keys."

if __name__=='__main__':
    main()

Thanks,

Donovan

--- Edits ---

  • Fixed issue where I was only iterating through some of the tracks.

Hello Donovan,

first of all thanks for this tutorial.

Your questions:

  1. Unfortunately you can not determine which timeline is active. I'm not even sure, there actually is only one active at a time. What would this "active state" mean in the end? I guess, you are after the timeline the user used last. But this information is not available.
  2. The code line in this question is actually checking a track, while you asked for the selection state of a key. The flags TL1_SELECT, TL2_SELECT, TL3_SELECT, TL4_SELECT represent the selection of a key or track in a dope sheet (with TL1 to TL4 being the timeline manager).
    On the other hand TL1_SELECT2, TL2_SELECT2, TL3_SELECT2, TL4_SELECT2 (though marked as private) represent the selection state of keys or tracks in F-curve timeline managers.
    TL1_FCSELECT (and TL2_ to TL4_) finally show, if the actual curve of a track (so the bit is on the CTrack) is selected (this is also true for hidden selected tracks).
  3. TL1_HIDE (and TL2_ to TL4_) provide the hidden state of tracks in both dope sheet and F-curve mode. But as said before, you can just get this information for each of the up to four timeline managers, but not for a specific active one.

One word of warning: Especially those flags marked as private in the docs should only be used for reading purposes and should not be used to change the state.

Cheers,
Andreas

You can't tell which is active, but you can see if each timeline is opened with c4d.IsCommandChecked()

It would be nice if there was a way to get the currently-selected timeline or object manager, similar to the way we can get the activebasedraw. There's unique selections in each manager, so if you want to work on the keys the user expects you need to know which timeline they're currently working in.

@a_block said in How to Get the Selected Keyframes in Active Timeline/FCurve Manager:

Your questions:

  1. Unfortunately you can not determine which timeline is active. I'm not even sure, there actually is only one active at a time. What would this "active state" mean in the end? I guess, you are after the timeline the user used last. But this information is not available.

Yeah, I'm hoping for the last-used / currently active Timeline and its mode: Timeline vs F-Curve vs NLA. Something like Rick's suggestion of a method similar to GetActiveBaseDraw(). Basically, I want to be able to write tools the behave on the selected keys in the active manager just like the Timeline commands like Move / Scale.

  1. The code line in this question is actually checking a track, while you asked for the selection state of a key.

I'm pretty sure it's not. There's an extra function in my script GetActiveTracks() but it's not being called from main(). There, I'm calling GetActiveKeys().

The flags TL1_SELECT, TL2_SELECT, TL3_SELECT, TL4_SELECT represent the selection of a key or track in a dope sheet (with TL1 to TL4 being the timeline manager).

Okay, so that's functioning as expected.

On the other hand TL1_SELECT2, TL2_SELECT2, TL3_SELECT2, TL4_SELECT2 (though marked as private) represent the selection state of keys or tracks in F-curve timeline managers.

Perhaps the documentation could explain this and list them as READ ONLY rather than Private? Perhaps with READ ONLY linking to a page explaining that read only values should never be written to?

TL1_FCSELECT (and TL2_ to TL4_) finally show, if the actual curve of a track (so the bit is on the CTrack) is selected (this is also true for hidden selected tracks).

Gotcha. That's a little ambiguous. Perhaps that documentation could be clarified?

  1. TL1_HIDE (and TL2_ to TL4_) provide the hidden state of tracks in both dope sheet and F-curve mode. But as said before, you can just get this information for each of the up to four timeline managers, but not for a specific active one.

Okay, so I can use this to filter my tracks list.

Thank you for the helpful, detailed, and speedy reply!

  1. TL1_HIDE (and TL2_ to TL4_) provide the hidden state of tracks in both dope sheet and F-curve mode. But as said before, you can just get this information for each of the up to four timeline managers, but not for a specific active one.

Okay, so I can use this to filter my tracks list.

Yeah, so this can tell me if a track has been manually hidden by the user using Timeline > View > Hide > Hide Selected Elements, but it does not show whether a given object is actually visible to the user in the timeline (unless it's been explicitly hidden). The timeline doesn't necessarily show all animated objects all the time, sometimes a user can setup their timeline so only 2-3 objects are visible, or 2-3 tracks out of dozens are visible - with none of them being explicitly "hidden".

I assume the answer is no, but is there a way to get a filtered list of objects/tracks/keys that are visible to the end-user for a given timeline?

Updated script including the ability to filter explicitly hidden objects, along with a method for detecting the active timeline.

"""Name-en-US: Print Selected Keys
Description-en-US: Prints the frame and value of the selected keys to the console."""

import c4d
from c4d import gui

def GetNextObject(op):
    """Gets the next object in the scene graph after `op`. Does not stop until all objects are returned.
    From: https://c4dprogramming.wordpress.com/2012/11/26/non-recursive-hierarchy-iteration/
    """

    if op==None:
        return None

    if op.GetDown():
        return op.GetDown()

    while not op.GetNext() and op.GetUp():
        op = op.GetUp()

    return op.GetNext()

def IterAllObjects(doc):
    """Iterates through all objects in `doc`.
    Call like:
    for obj in IterAllObjects(doc):
        print obj.GetName()
    """

    if doc is None:
        return

    op = doc.GetFirstObject()
    while op is not None:
        yield op
        op = GetNextObject(op)

def IterAllTracks(doc):
    """Iterates through all animation tracks in `doc`.
    Call like:
    for track in IterAllTracks(doc):
        print track.GetName()
    """

    if doc is None:
        return

    for obj in IterAllObjects(doc):
        for track in obj.GetCTracks():
            yield track

def GetActiveTracks(doc, timeline_bit=None, exclude_hidden=True):
    """Returns the active tracks based on `selection_bit`.

    timeline_bit NBIT Reference:
    https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d/C4DAtom/GeListNode/index.html?highlight=getnbit#GeListNode.GetNBit

    Timeline 1: c4d.NBIT_TL1_SELECT,
    Timeline 2: c4d.NBIT_TL2_SELECT,
    Timeline 3: c4d.NBIT_TL3_SELECT,
    Timeline 4: c4d.NBIT_TL4_SELECT,

    F-Curve 1: c4d.NBIT_TL1_FCSELECT,
    F-Curve 2: c4d.NBIT_TL2_FCSELECT,
    F-Curve 3: c4d.NBIT_TL3_FCSELECT,
    F-Curve 4: c4d.NBIT_TL4_FCSELECT

    Issues:
    * No way (that I'm aware of) to detect which timeline is active.
    * No way to determine whether in Timeline or FCurve mode.
    * Not aware of whether track is visible/hidden in Timeline or not.
    """

    if doc is None:
        return

    if timeline_bit is None:
        timeline_bit = GetFirstOpenTimeline()
        if timeline_bit is None:
            # Error: No open timelines.
            return

    active_tracks = []

    hidden_bit = None
    if exclude_hidden:
        hidden_bit = GetTimelineHiddenBit(timeline_bit)
        if hidden_bit is None:
            return

    for track in IterAllTracks(doc):
        if exclude_hidden and track.GetNBit(hidden_bit):
            continue
        
        # Is selected?
        if track.GetNBit(timeline_bit):
            active_tracks.append(track)

    return active_tracks

def GetFirstOpenTimeline():
    """Returns the timeline selection bit for the first (by timeline number, not opening order) open timeline.

    As of 2018/09/11 There's no way to determine if a Timeline is the most recently used/selected so this is the best we have.
    Also, there's no way to determine whether you're in Timeline or F-Curve mode, so Timeline (2) is the only one that will
    return `TL#_SELECT2` which is the selection bit for F-Curve data.

    Note: should only be called from the Main thread (e.g. no expressions, no objects)
    """

    timeline_commands_and_bits = [
        (465001510, c4d.NBIT_TL1_SELECT), # Timeline... (TL that opens with Dopesheet command)
        (465001511, c4d.NBIT_TL2_SELECT2), # Timeline... (2) (TL that opens with F-Curve command, will assume to be in F-Curve mode, this could have unexpected results if user manually switched to Dopesheet mode)
        (465001512, c4d.NBIT_TL3_SELECT), # Timeline... (3)
        (465001513, c4d.NBIT_TL4_SELECT) # Timeline... (4)
    ]

    for command_id, selection_bit in timeline_commands_and_bits:
        # Is this command shown as Checked in the UI? Odds are, the Timeline is open and in-use by the user.
        if c4d.IsCommandChecked(command_id):
            return selection_bit

def GetTimelineHiddenBit(selection_bit):
    """Gives you the appropriate visibility bit to use based on a given Timeline selection bit.
    """

    visibility_bit = None

    # Timeline 1
    if selection_bit in [c4d.NBIT_TL1_SELECT, c4d.NBIT_TL1_SELECT2, c4d.NBIT_TL1_FCSELECT]:
        visibility_bit = c4d.NBIT_TL1_HIDE
    # Timeline 2
    elif selection_bit in [c4d.NBIT_TL2_SELECT, c4d.NBIT_TL2_SELECT2, c4d.NBIT_TL2_FCSELECT]:
        visibility_bit = c4d.NBIT_TL2_HIDE
    # Timeline 3
    elif selection_bit in [c4d.NBIT_TL3_SELECT, c4d.NBIT_TL3_SELECT2, c4d.NBIT_TL3_FCSELECT]:
        visibility_bit = c4d.NBIT_TL3_HIDE
    # Timeline 4
    elif selection_bit in [c4d.NBIT_TL4_SELECT, c4d.NBIT_TL4_SELECT2, c4d.NBIT_TL4_FCSELECT]:
        visibility_bit = c4d.NBIT_TL4_HIDE

    return visibility_bit

def GetActiveKeys(doc, timeline_bit=None, exclude_hidden=True):
    """Returns all active keys.

    timeline_bit: Should be and NBIT, or will be auto-detected if set to None.
    exclude_hidden: Won't return active keys if tracks were explicitly hidden. Note: passively hidden (e.g. not dragged into TL, or filtered) will
    still return their active keys. I don't think there's a way to return the keys actually visible to the user.

    NBIT Reference:
    https://developers.maxon.net/docs/Cinema4DPythonSDK/html/modules/c4d/C4DAtom/GeListNode/index.html?highlight=getnbit#GeListNode.GetNBit

    Timeline 1: c4d.NBIT_TL1_SELECT,
    Timeline 2: c4d.NBIT_TL2_SELECT,
    Timeline 3: c4d.NBIT_TL3_SELECT,
    Timeline 4: c4d.NBIT_TL4_SELECT,

    # Note: The _SELECT2 bits are for F-Curve key selections, Don't use c4d.NBIT_TL1_FCSELECT, that's only for Tracks.
    F-Curve 1: c4d.NBIT_TL1_SELECT2,
    F-Curve 2: c4d.NBIT_TL2_SELECT2,
    F-Curve 3: c4d.NBIT_TL3_SELECT2,
    F-Curve 4: c4d.NBIT_TL4_SELECT2
    """

    if timeline_bit is None:
        timeline_bit = GetFirstOpenTimeline()
        if timeline_bit is None:
            # Error: No open timelines.
            return

    hidden_bit = None
    if exclude_hidden:
        hidden_bit = GetTimelineHiddenBit(timeline_bit)
        if hidden_bit is None:
            # Error: No way to determine if an element is hidden or not.
            return

    active_keys = []

    for track in IterAllTracks(doc):
        if exclude_hidden:
            # Track is marked as hidden, we don't care about these keys
            # even if they are selected.
            if track.GetNBit(hidden_bit):
                continue

        curve = track.GetCurve()
        if curve is None:
            continue

        for key_id in xrange(curve.GetKeyCount()):
            key = curve.GetKey(key_id)
            is_selected = key.GetNBit(timeline_bit)
            if (key is not None) and is_selected:
                active_keys.append(key)

    return active_keys

def main():
    """Print the frame and value of the currently selected keys to the Python Console.
    """

    print " "
    print " "
    print "Active Keys"
    print "==========="

    active_keys = GetActiveKeys(doc)
    if active_keys:
        for key in GetActiveKeys(doc, c4d.NBIT_TL1_SELECT):
            print key.GetTime().GetFrame(doc.GetFps()), ": ", key.GetValue()
    else:
        print "No active keys."

if __name__=='__main__':
    main()

@dskeithbuck said in How to Get the Selected Keyframes in Active Timeline/FCurve Manager:

  1. The code line in this question is actually checking a track, while you asked for the selection state of a key.

I'm pretty sure it's not. There's an extra function in my script GetActiveTracks() but it's not being called from main(). There, I'm calling GetActiveKeys().

I'm sorry, I was just confused by the line in the actual question, where it look like you get the NBit of the track:

  1. How can you detect selected keys in the F-Curve manager?
    • track.GetNBit(c4d.NBIT_TL1_FCSELECT) doesn't seem to be workin

So, with your code snippets was nothing wrong. It was me being confused by the question. No worries.

There is no way to get a filtered list of objects visible in the timeline. Sorry.