[addon] initialise a modal frame_change operator from an addon ?

hi again, this is the 3 bpy threads/days guy talking.

I’m writing an addon where I need a frame_change like modal operator. I’d like the modal to start at ‘addon enable’ time.

when I enable the addon in userprefs, frame_change is started but will only execute when focus is on the userprefs window not on the blender window, which is quite useless :slight_smile:

it works fine when I load the script in a block text, I guess this is context related but I found no info or examples anywhere about it. (like for the update option in the bpy.props.XxxxProperties btw :wink: )

I tried this in the invoke/execute modal functions :

bpy.data.window_managers['WinMan'].modal_handler_add(self)

but useless. the documentation lacks info about self and context usage I think… please share gurus :slight_smile:

also I call some operators to initialise my addon from the init register() function… not sure if it’s the right way do do it.
… and all the addon resides in the init file, no modules apart.

below is a silly code with an framechange operator that only work in the user preferences window.
copy/paste in 2.58/scripts/addons then enable it in user pref Add-ons > Animation
…but this will work as it should if you copy/paste it directly in a block text

thanks for any pointers or explanations about this and/or context usage.

bl_info = {
    "name": "frame change watchdog",
    "description": "dummy add-on",
    "author": "Jerome Mahieux (littleneo)",
    "version": (0, 1),
    "blender": (2, 5, 7),
    "api": 36339,
    "location": "earth",
    "warning": "it's dummy",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Animation"}

import bpy

class frameChanged(bpy.types.Operator):
    '''Implement a frame change event.'''
    bl_idname = "object.frame_change"
    bl_label = "Frame Change Event"

    last_frame = bpy.props.IntProperty()

    def __init__(self):
        print("--> frameChanged ACTIVATED")

    def __del__(self):
        print("--> frameChanged DEACTIVATED")

    def modal(self, context, event):
        if event.type in ('ESC'):
            return {'CANCELLED'}
        if self.properties.last_frame != bpy.context.scene.frame_current :
            print ("    frameChanged To:" + str(bpy.context.scene.frame_current))
            self.properties.last_frame = bpy.context.scene.frame_current 
        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        print ("invoke")
        context.window_manager.modal_handler_add(self)
        #bpy.data.window_managers['WinMan'].modal_handler_add(self)
        #self.execute(context)
        return {'RUNNING_MODAL'}

    def execute(self, context):
        context.window_manager.modal_handler_add(self)
        #bpy.data.window_managers['WinMan'].modal_handler_add(self)
        return {'RUNNING_MODAL'}


def register():
    bpy.utils.register_class(frameChanged)
    bpy.ops.object.frame_change()

def unregister():
    bpy.utils.unregister_class(frameChanged)
    
if __name__ == "__main__":
    register()

UPDATE :

if the userprefs is called inside the blender windows (from an header main button, not from the menu that spawn a new window on window at least) the addon WORK.

trying some crappy things with context.screen.name tests (userprefs creates a temp panel)

also if the addon is enabled by default, it crashes blender beacause of bpy.ops.object.frame_change()

One thing to be aware of with any implementation of frame change is that you will not have access to the context when rendering. So any code you develop to run in a frame change should avoid using the context. This is because the context is always None when rendering.

If you have not looked at my frame change AddOn feel free to click on the link in my signature. I created a couple versions. One is a driver based version and the other is a threaded approach.

To make a piece of code always run on load you need to name the text file with a .py extension in the text window and click the Register button for the script. This will make the script run on load and you can avoid those problems associated with a def not being found because the script was not run,

@Atom have you seen this new stuff…?
here i can change frame number and object position from context before render, just press F12


import bpy, random

def Pre(scene):
    print ("-> change frame")
    r = int(random.random()*250)
    bpy.context.scene.frame_set(r)

    print ("-> move object")
    r = int(random.random()*5)
    bpy.context.object.location[1] += r

print (bpy.app.handlers.render_pre)
bpy.app.handlers.render_pre.append(Pre)
print (bpy.app.handlers.render_pre)

ok, now tried with ctrl+F12 and that is a different thing…

(oh wow I did’nt know one could key the object visibility… amazing pix script liero :slight_smile: )

Atom the watchdog thread script is clever and flexible, thanks to share. and all exceptions seem adressed. I’d rather like this one than the pydriver way.

I was wondering if you could write your thread script as a common addon, with a little panel and a registered groupprops were one could configure sleep_time and type our own watchdog function that would be called from your addon. I suppose you could kill the process also, at unregister() time ?

One thing to be aware of with any implementation of frame change is that you will not have access to the context when rendering.

by the way, it’s really interesting but… my first question remains ! :slight_smile: (supposing I don’t need to run a modal or watchdog at render time).
there’s something about the context that should be done I suppose. it’s only because a new window for user prefs is spawned that it fails.

sorry I could not resist to hack your code :rolleyes: here is a draft as a thanks for the pointers you gave me.

now as a versatile addon in the System category.
it won’t start at installation, there’s a little ui in the Shelf. the interesting thing is that you can change the called function while the thread is running.

it can be run and configured from another script (below).

calls and variables:

import bpy
from bpy.props import *

import threading
import time
import logging
logging.basicConfig(level=logging.DEBUG,format='(%(threadName)-10s) %(message)s',)


bl_info = {
    "name": "Atomic Watchdog",
    "description": "",
    "author": "Atom",
    "version": (0, 1),
    "blender": (2, 5, 8),
    "api": 37702,
    "location": "View3D > Tool Shelf",
    "warning": "",
    "wiki_url": "",
    "tracker_url": "",
    "category": "System"}


class Watchdog(bpy.types.PropertyGroup) :
    '''
    '''
    idletime = bpy.props.IntProperty(default=1, min=1, max=100)
    function = bpy.props.StringProperty(default='example()')
    status   = bpy.props.BoolProperty()
    sigstop  = bpy.props.BoolProperty()

class WatchdogPanel(bpy.types.Panel):

    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_idname = 'watchdog'
    bl_label = 'Atomic Watchdog'

    def draw(self, context):

        watchdog = bpy.context.scene.watchdog
        layout = self.layout

        row = layout.row(align = True)
        if watchdog.status : status = 'running'
        else : status='idle'
        row.label('status : %s'%status)

        layout = self.layout
        row = layout.row(align = True)
        row.label('function/operator to run :')
        row = layout.row(align = True)
        row.prop(watchdog,"function" )

        row = layout.row(align = True)
        #row.label()
        row.prop(watchdog,"idletime",text='speed')

        row = layout.row(align = True)
        row.operator( "watchdog.start", text='Start', icon='PLAY')
        row.operator( "watchdog.stop", text='Stop', icon='MESH_PLANE')

class WatchdogStart(bpy.types.Operator) :
    ''''''
    bl_idname = 'watchdog.start'
    bl_label = 'start watchdog'

    def execute(self, context) :
        bpy.context.scene.watchdog.status = True
        bpy.data.scenes[0].watchdog.sigstop = False
        run_locked = False
        if run_locked:
            lock = threading.Lock()
            lock_holder = threading.Thread(target=watchdog, args=(lock,), name='Watchdog')
            lock_holder.setDaemon(True)
            lock_holder.start()
        else:
            lock = None
            t = threading.Thread(target=watchdog,args=(lock,), name='Watchdog')
            t.start()
        return {'FINISHED'}

    def invoke(self, context, event ):
        bpy.context.scene.watchdog.status = True
        bpy.data.scenes[0].watchdog.sigstop = False
        run_locked = False
        if run_locked:
            lock = threading.Lock()
            lock_holder = threading.Thread(target=watchdog, args=(lock,), name='Watchdog')
            lock_holder.setDaemon(True)
            lock_holder.start()
        else:
            lock = None
            t = threading.Thread(target=watchdog,args=(lock,), name='Watchdog')
            t.start()

        return {'FINISHED'}

class WatchdogStop(bpy.types.Operator) :
    ''''''
    bl_idname = 'watchdog.stop'
    bl_label = 'stop watchdog'

    def execute(self, context) :
        #bpy.data.scenes[0].watchdog.status = False
        bpy.context.scene.watchdog.sigstop = True
        return {'FINISHED'}

    def invoke(self, context, event ):
        #bpy.data.scenes[0].watchdog.status = False
        bpy.context.scene.watchdog.sigstop = True
        return {'FINISHED'}


def watchdog(lock):
    # When the thread is started, setup our local defaults.
    isBusy = False
    canContinue = False
    shouldLoop = True
    
 
    while shouldLoop:
        # We have entered an infinite loop that we may never exit from.
        if isBusy:
            # Skip any further processing, we are already busy.
            pass
        else:
            # This is the equivalent of the frameChange event, PUT FRAME CHANGE CODE IN THIS INDENT BLOCK.
            isBusy = True
            scene_index = 0
        
            # Try to fetch a custom value from the scene that determines how fast our timer should be running.
            try:
                # Our thread speed is passed to us as an integer percentage value.
                #cp_ts = bpy.data.scenes[0].CameraPlus_thread_speed
                cp_ts = bpy.data.scenes[0].watchdog.idletime
                # we use this as an amount in the range of 0-1 seconds. 
                # So 100% speed mean about 0.01 sleep interval and 1% speed means 0.99 sleep interval.
                new_sleep_time = 1.0-(1.0*(cp_ts/100))
            except:
                # Error accessing the scene property, just use default value for the next interval.
                new_sleep_time = 0.5
                logging.debug("thread_speed Scene property is unavailable.")

            try:
                bpytest = bpy.data.scenes[scene_index].frame_current
                canContinue = True
            except: 
                # If we end up here, BPY is in a state that is unaccessible at this time, simply let it go and we will try again on next interval.
                logging.debug("(" + str(new_sleep_time) + ") Failed to fetch frame information, last valid frame #" + str(frame_last) + ".")
                canContinue = False
    
            if canContinue:
                try:
                    exec(bpy.data.scenes[0].watchdog.function)
                except:
                    logging.debug("(" + str(new_sleep_time) + ") a problem occured with the user function.")
            else:
                # Our first attempt to access BPY resulted in an error, lets wait an interval before we try any other access.
                pass

            # check if no stop signal has been sent ( watchdog.sigstop = True )
            try :
                if bpy.data.scenes[0].watchdog.sigstop :
                    bpy.data.scenes[0].watchdog.status = False
                    shouldLoop=False
                    print("watchdog paused.")
            except :
                print("problem while reading watchdog sigstop value.")
                

            isBusy = False

        #shouldLoop = False

        time.sleep(new_sleep_time) # Feel free to alter time in seconds as needed. (smaller time for light weight scenes, higher time for heavier scenes)
    return

def example() :
    print('done.')
    print(bpy.data.scenes[0].frame_current)
    cube = bpy.data.scenes[0].objects['Cube']
    cube.scale[0] *= 1.01
    return
    
def register():
    bpy.utils.register_class(WatchdogPanel)
    bpy.utils.register_class(WatchdogStart)
    bpy.utils.register_class(WatchdogStop)
    bpy.utils.register_class(Watchdog)
    bpy.types.Scene.watchdog = bpy.props.PointerProperty(type=Watchdog)
    
def unregister():
    bpy.utils.unregister_class(WatchdogPanel)
    bpy.utils.unregister_class(WatchdogStart)
    bpy.utils.unregister_class(WatchdogStop)
    bpy.utils.unregister_class(Watchdog)

if __name__ == "__main__":
    register()  

updated with a stop feature

say one want to run this operator every x time thanks to Atom modified script above, (even when rendering, omg) :


class DogListener(bpy.types.Operator):
    ''''''
    bl_idname = "scene.doglistener"
    bl_label = "Modal Listener"

    def execute(self, context):
        print ("I heard.")
        return {'FINISHED'}

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

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

if __name__ == "__main__":
    register()

type bpy.ops.scene.doglistener() in the (too) little function box then start

or from your script/addon:

bpy.context.scene.watchdog.function = 'bpy.ops.scene.doglistener()'

then

bpy.ops.watchdog.start()
#bpy.ops.watchdog.stop()