0
$\begingroup$

I would like to add behavior to the 3d viewport so that when a user double-clicks an object it puts the object in a special move mode (locked along a certain axis depending on conditions). The part I need help with is the first part - monitoring for the event. The second part would be handled by a modal.

All of the examples for modals I've found are either fired off with a test run during register(), tied to a button, or re-routed from an existing event to send in a new direction. I'm not really sure which approach to take - to try and map through Window Manager api, set up an event to monitor for clicks, or... something else.

Most help along these lines suggest re-mapping through Keymap in preferences and saving that config for the addon. However I don't see a way to map anything with a double-click (so maybe that's not even possible and would need to use a different combo).

With other UI toolkits I've set it to listen for events in event queue. Blender has events, but seems to only apply inside a modal once already called. Either way I haven't figured out how to create a double-click event.

I would prefer to tie it to some type of mouse event because a button fired operator in this situation would have the button on the side and the selected object half a screen away. The closest I've found to a workable solution is adding to the right-click menu. That could work; however I had hoped to eventually hide most of the right-click functionality. This is designed for users who know nothing about Blender, so an overlay would eventually replace everything in a slimmed down solution.

EDIT

Here is a script I found that emulates an event queue... modified with a DOUBLE_CLICK monitor. However this is an extra event queue which runs until terminated. So a good solution doesn't feel to right to spin this up in the background.

Also note the DOUBLE_CLICK option does not work. I tested out several other values and verified those events were getting picked up, so I don't believe it's the structure of the code.

import bpy

class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"

    _timer = None

    def modal(self, context, event):
        if event.type in {'LEFTMOUSE'}:
            if event.value in {'DOUBLE_CLICK'}:
                print('DOUBLE CLICK')
                return {'RUNNING_MODAL'}

        if event.type in {'RIGHTMOUSE'}:
            screen = context.screen
            x, y = event.mouse_x, event.mouse_y

            areas = [a for a in screen.areas if a.x < x < a.x + a.width
                    and a.y < y < a.y + a.height]

            if areas and areas[0].type not in {'VIEW_3D', 'TEXT_EDITOR'}:
                print("Right Click Taken Out")
                return {'RUNNING_MODAL'}

        if event.type in {'ESC'}:
            self.cancel(context)
            return {'CANCELLED'}

        if event.type == 'TIMER':
            pass

        return {'PASS_THROUGH'}

    def execute(self, context):
        wm = context.window_manager
        self._timer = wm.event_timer_add(0.1, window = context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def cancel(self, context):
        wm = context.window_manager
        wm.event_timer_remove(self._timer)

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

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

if __name__ == "__main__":
    register()
    # test call
    bpy.ops.wm.modal_timer_operator('INVOKE_DEFAULT')

ANOTHER EDIT

Here is the updated code in case anyone else needs it. Thanks to @Tareyes for pointing me in the right direction. I've opened a ticket against the DOUBLE_CLICK not being recognized, which would still be the preferable way to handle this. However this works pretty well and can be worked into an existing modal.

import bpy
import time

class ModalTimerOperator(bpy.types.Operator):
    """Operator which runs its self from a timer"""
    bl_idname = "wm.modal_timer_operator"
    bl_label = "Modal Timer Operator"

    click = False

    def modal(self, context, event):
        loop_time = time.time()
        delta = loop_time - self.last_click

        if delta > 0.3:
            self.click = False

        if event.type in {'LEFTMOUSE'}:
            if event.value in {'RELEASE'}:                
                if not self.click:
                    self.click = True
                    self.last_click = time.time()

                elif delta < 0.3 and self.click:
                    print("Double Click")
                    self.click = False


        if event.type in {'ESC'}:
            self.cancel(context)
            return {'CANCELLED'}

        if event.type in {'TIMER'}:
            pass

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        print("INVOKE")
        self.last_click = time.time()

        self.execute(context)
        return {'RUNNING_MODAL'}

    def execute(self, context):
        wm = context.window_manager
        self._timer = wm.event_timer_add(0.1, window = context.window)
        wm.modal_handler_add(self)
        return {'RUNNING_MODAL'}

    def cancel(self, context):
        wm = context.window_manager
        wm.event_timer_remove(self._timer)

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

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

if __name__ == "__main__":
    register()
    # test call
    bpy.ops.wm.modal_timer_operator('INVOKE_DEFAULT')
$\endgroup$
  • $\begingroup$ Anyone have any ideas? I can't get double click to work at all. I messed around with double click timing in the Input preferences and can't get it to recognize. $\endgroup$ – Sam Vimes Aug 27 at 17:31
  • $\begingroup$ maybe I'm missing something, buy you could something like: if event.type is left click --> start modal timer operator --> if event.type = timer, delete the timer and reset, but if you get an event of type "left click" before the timer ends, then it means you had a double click and you can run your functions $\endgroup$ – Tareyes Aug 28 at 18:31
  • $\begingroup$ That's a good idea to get the functionality emulated after the first click. The problem is the how to monitor for that first left click, without running a modal the whole time waiting for the interrupt. I'm just learning about modals, but my experience with queues and blocking threads suggests that's a potentially brittle solution. $\endgroup$ – Sam Vimes Aug 28 at 18:42
  • $\begingroup$ every time a left click is detected a timer is started (like 0.3 seconds). After those 0.3 seconds that timer can be deleted, so it's not running all the time. But in any case, in my experience, modal operator are far less power consuming or fragile than it seems, at least with simple functions. But if you don't want to use timers, every time a left click is pressed you could use the time.localtime() function to get the input time, and in the next left click check if the time elapsed is less than your tolerance $\endgroup$ – Tareyes Aug 28 at 21:54
1
$\begingroup$

You can do it in 2 ways:

Modal Timer

You can do something like:

def modal(self, context, event):
        if event.type in {'LEFTMOUSE'}:
            wm = context.window_manager
            if self._timer is not None:         
                print("Double click")       #your function goes here
                wm.event_timer_remove(self._timer)
            else:                
                self._timer = wm.event_timer_add(0.3, window = context.window)
                return {'RUNNING_MODAL'}

        if event.type in {'TIMER'}:
            wm.event_timer_remove(self._timer)  #remove the timer
            return {'RUNNING_MODAL'}

use time.time()

you can check the time of each click and check the time elapsed between them

import time

#operator declaration etc

_timer = None
t = 0

def modal(self, context, event):
        if event.type in {'LEFTMOUSE'}:
            temp = time.time()
            if (temp - self.t) < 0.3:
                    print("Double Click")       #your function goes here
            self.t = temp

Disclaimer: I haven't tested the code since this scripts are more of a explainer rather than actual code, so you'll have to check for bugs (I'm lazy ;))

$\endgroup$
  • $\begingroup$ That got me pretty close to the answer, which had to take into account an existing click and loop timer. There was also a LEFTMOUSE event credited when I ran the script, so I added to check for RELEASE to make sure it was a click. I'll update my original post with the updated code. $\endgroup$ – Sam Vimes Aug 29 at 2:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.