Edit keyframe values from graph or dopesheet using Python For short I want to make an addon which would allow to select keyframes from the graph editor (and eventually also from the dope sheet) and change their value at once using an input popup. And more complex options like incremental inputs and other stuff, but that's for later.

I did a flow chart of what I aim for the v1, if it can help you:



I'm struggling with the core action:
Set a value to the selected keyframes, from the graph editor.

My first tries were to basically edit some keyframe values in the graph editor and work from what pops up in the info editor as I usually do:

bpy.context.object.co[1] = 1


Which doesn't work on its own and returns:
AttributeError: 'Object' object has no attribute 'co'.

I also noticed this in the Python Tooltips:


I tried at least to just get a working line like
bpy.context.selected_visible_fcurves.Keyframe.co[1] = 1
bpy.context.active_object.animation_data.action.fcurves.Keyframe.co[1] = 1

But with no success.

I also tried to see how other people did on Internet and tried stuff like these:

def execute(self, context):
    for b in bpy.context.selected_visible_fcurves:
        b.types.Keyframe.co[1] = 6
    return{'FINISHED'}


Or :

def execute(self, context):
    for b in bpy.context.selected_visible_fcurves:
        b.types.Keyframe.co[1] = 6
    return{'FINISHED'}


I also saw lots of people actually replacing their existing keyframes by new ones with new values, but to me it seems to be a lot of instructions for very few effects, and I guess it will not perform correctly on large scale operations.

I may also indicate a few needs:


this should work on any kind of curve, no matter if it's a loc/rot/scale curve, a custom curve, ... So I cannot use any transform-specific code like I've seen several times.
ideally it should work on any selected keyframe, no matter if its a different curve, a different object, ... As long as it's in the graph editor and I can select some keys, it should work.


Thanks for any help!

Edit

Thanks to brockmann's answer I now can go further in my script.

I just have a couple more wonders:

Set the pop-up value from the active keyframe's value

Right now, the default value of the popup is 0 at first start, and then it keeps the last value used everytime it pops again. I would like it so that each time I call the popup, it shows the active keyframe's value.

So as far as my python skills can go, it means to find a way with blender's api to get the xy coordinates of the active keyframe and assign the Y to my-float_y, and put this procedure at the very start of the operator, right?
I tried this but it doesn't output anything I want:

    def kval(self, context):
        k_val = 0
        for v in context.active_editable_fcurve:
            for kv in v.keyframe_points:
                if kv.select_control_point:
                    x, y = keyframe.co
                    print("y key value is", y)
                    my_float_y = y
        return {'VALUED'}


How to change the UI

Right now it looks like this:



The cons are that it takes lots of screen-space just for a value, and it requires to set the value, press enter and then click the OK button.
I can kind of make it simpler by reducing the width because the value always overlays the text, but that's not enough:
It would be more efficient if it was a more direct interaction like i.e. the samples inputs:



Ideally I would want just something small and simple like this (but with a float):
When editing the value of the keyframe you have to set both handles as well. Example on how to set the value based on our famous Operator Simple template (should work for the dope sheet too):



import bpy

class SimpleOperator(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "object.simple_operator"
    bl_label = "Simple Object Operator"
    bl_options = {'REGISTER', 'UNDO'}

    my_float_y : bpy.props.FloatProperty()

    def cpoints(self, context):
        ctrl_pts = []
        for c in context.selected_visible_fcurves:
            for k in c.keyframe_points:
                if k.select_control_point:
                    ctrl_pts.append(k)
        return ctrl_pts

    @classmethod
    def poll(cls, context):
        areas = ('DOPESHEET_EDITOR', 'GRAPH_EDITOR', 'TIMELINE')
        if context.area.type in areas:
            return True
        else:
            return False 

    def invoke(self, context, event):
        wm = context.window_manager
        return wm.invoke_props_dialog(self, width=450)

    def execute(self, context):
        sel = self.cpoints(context)

        for k in sel:
            k.co = (k.co.x, self.my_float_y)
            k.handle_left = (k.handle_left.x, self.my_float_y)
            k.handle_right = (k.handle_left.y, self.my_float_y)
        return {'FINISHED'}

    def draw(self, context):
        lay = self.layout
        lay.row().prop(self, "my_float_y")


def register():
    bpy.utils.register_class(SimpleOperator)


def unregister():
    bpy.utils.unregister_class(SimpleOperator)


if __name__ == "__main__":
    register()

    # test call
    #bpy.ops.object.simple_operator()


Alternatively you can use an invoke_props_popup:



See: How do I get invoke_props_popup to work?