How to Read Mouse / Keyboard continuously?

I noticed Blender not storing autosaves for a while. Then found through a bug report from 2016, that this is because autosave is disabled while a modal operator is running.

I have a continuous scene_update_post function running, and I have a modal operator running along side it for 2 reasons.

  1. Find the area/space the mouse is under, to only run in the view_3d
  2. Find if Alt + G/R/S are pressed, to run my own function if certain variables are true.

#1 is for performance but a stability thing was added because data from event was available, so without it, I would have to try to find a way to run without a modal.

Is there a way to read event (mouse / keyboard) or area/space data, outside an operator?

I would try to read the window like this but the function seems to only read the initial bpy.context (from install), and does not update to the current space_data, making it None

@bpy.app.handlers.persistent
def function(self):
	space = bpy.context.space_data.type
bpy.app.handlers.scene_update_post.append(function)

I use a modal operator to find the space_data by using the mouse location, because a modal also doesn’t update space_data

I’m also interested in this :slight_smile: as I also found no official way to do it - other than keeping a modal running forever.

I think I found a way.
It doesn’t block key inputs, so would have to rework the code I was already using in modal(), but I believe this method DOES get the event+info that I needed from it.

The gist of it is to call a mostly blank operator using (‘INVOKE_DEFAULT’) then get/store data from that.
Normally when you call an operator using python, it implies (‘EXEC_DEFAULT’) , and only default to invoke when you click it via UI button.

"Properties"
import	bpy

global_event	=	None
global_timer	=	0

"Operators"
class ADDITIVE_OT_Get_Event(bpy.types.Operator):
	bl_description	=	"Get Event data accessible via operators, and store as a global/local variable"
	bl_idname		=	'wm.get_event'
	bl_label		=	"Get Event info"
	bl_options		=	{'INTERNAL',}
	bl_undo_group	=	""
	
	@classmethod
	def poll(cls, context):
		return True
	
	def invoke(self, context, event):
		global global_event
		
		bpy.event = global_event = event
		
		# The bpy part gives a value you can read in console, or even other scripts
		# (Note: displays the last event logged, so there's a delay if trying to manually check for yourself)
		# To use elsewhere, use something like this:
		if (hasattr(bpy, 'event')):
			...
		
		# Or you can just use this:
		# global_event = event
		
		return {'FINISHED'}
	
	def execute(self, context):
		self.report({'INFO'}, "Error:   use bpy.ops.wm.get_event('INVOKE_DEFAULT') to function correctly")
		return {'PASS_THROUGH'}

class ADDITIVE_OT_operations:
	# I put theses defs in classes now, for sorting in Notepad++
	
	@bpy.app.handlers.persistent
	def Add_Constant(self):
		
		def update_timer(max=220):
			global global_timer
			
			# Display timer value, to measure how slow you want it's delay
			def sec(timer):
				return round(timer/max*60)
			if sec(global_timer) in range(0, max, 5):
				print('Timer: ', sec(global_timer))
			# Comment out this section to stop spamming the console
			
			global_timer += 1
			if (global_timer > max):
				global_timer	=	0
				bpy.ops.wm.get_event('INVOKE_DEFAULT')
			
			return global_timer
		
		global global_event
		event = global_event
		timer = update_timer(220)
		
		if event:
			if event.alt:
				print('Alt derp')
			...
		else:
			This will spam for a second or two, on startup
			print("No event")
			...
	
	# This def is used to load the event on startup. 
	# You can't run the op during register() because reasons
	@bpy.app.handlers.persistent
	def Add_Reload(self):
		bpy.ops.wm.get_event('INVOKE_DEFAULT')

"Registration"
def register():
	if (__name__ == "__main__" or not __package__):
		bpy.utils.register_module(__name__)
	bpy.app.handlers.scene_update_post.append(ADDITIVE_OT_operations.Add_Constant)
	bpy.app.handlers.load_post.append(ADDITIVE_OT_operations.Add_Reload)

def unregister():
	bpy.app.handlers.scene_update_post.remove(ADDITIVE_OT_operations.Add_Constant)
	bpy.app.handlers.load_post.remove(ADDITIVE_OT_operations.Add_Reload)
	if (__name__ == "__main__" or not __package__):
		bpy.utils.unregister_module(__name__)

if (__name__ == "__main__"):
	register()

To be clear, the reason for the timer is because scene update. where my main thing occurs, will try to run several times and run the operator continuously, resulting in a crash.

So, I opt to using a timer that convinces it to slow it’s roll.

Heh, this can also be tweaked a little to solve my first problem, because this can be used to get an updated context, to read the area/space for the mouse.

The right solution here would be to make auto-saving working during modal operators. But we need to make it a bit smarter to distinguish different cases, since it is useful to disable auto-save while the user is sculpting a stroke a dragging a vertex, to keep the UI more responsive.

Bumping an old thread here, but couldn’t we just have an additional bl_option that allows things like auto save to pass through while it’s running? Something like “BACKGROUND”?

1 Like