theodox / mGui

Python module for cleaner maya GUI layout syntax
MIT License
123 stars 23 forks source link

MayaEvent() handlers don't work properly in LayoutDialogs #56

Closed theodox closed 7 years ago

theodox commented 7 years ago

By default mGui widgets are created with MayaEvent() objects as their event delegates. Ordinarily that's nice because it minimizes damage from UI reacting across threads...

but it also means that the events for a widget created using cmds.layoutDialog don't fire until after the dialog closes, since we never hit the idle state while the dialog is open.

The workaround it to replace the MayaEvent() with an Event() -- that's synchronous so it doesn't get hung up. Here's an example:

    def _layout(self):
        with forms.LayoutDialogForm('root') as self.root:
            with forms.VerticalThreePane(None):
                gui.Text( label='Please log into Perforce. This is only required once per project.')
                with forms.VerticalExpandForm(None):
                    gui.Text( label='Enter your p4 client workspace. E.G. "bryce-class4"')
                    self.p4Client = gui.TextField('P4ClientWorkspace')
                    gui.Text( label='Enter p4 user name. E.G. "Bryce"')
                    self.p4User = gui.TextField('P4UserName')
                    gui.Text( label='Enter p4 password.')
                    self.p4Password = gui.TextField('P4Password')
                    gui.Text( label='Enter p4 port.')
                    self.p4Port = gui.TextField('p4port', text='p4proxy.undeadlabs.net:1667')
                login_button = gui.Button('login', label='Login')
                login_button.command = Event()
                login_button.command += self.attempt_p4_login

where the login_button is post-edited to switch types. That's an annoying piece of trivia for users to remembr

theodox commented 7 years ago

Options I can think of:

  1. don't fix, add a note. Not ideal, it's a nasty gotcha
  2. add a flag to Nested which can change the MayaEvents to Events when a layout context manager closes. That would make it safer - but you'd still have to remember to hook events after layout time... which is the style I use most often but it may not seem natural
  3. add a cmds.layoutDialog replacement which hides the issue: maybe for the duration we replace MayaEvent with Event?
  4. add some logic to MayaEvent.fire() that knows when a layoutDialog is open and works synchronously....
bob-white commented 7 years ago

So I'm not running into any problems with the following code:

from maya import cmds

from mGui import gui, forms, lists
from mGui.bindings import bind, BindingContext, BindableObject
from mGui.observable import ObservableCollection
from mGui.events import Event

@object.__new__
class testDialog(object):
    def _layout(self):
        self._menu_items = ObservableCollection(*cmds.ls(type='transform'))
        with forms.LayoutDialogForm() as self.base:
            with BindingContext() as bc:
                with forms.FooterForm() as form:
                    with forms.FillForm() as main:
                        self._menu = gui.OptionMenu()
                        self._menu.label = 'Items: '
                        self._menu < bind() < self._menu_items
                    with forms.HorizontalStretchForm() as footer:
                        okay = gui.Button()
                        okay.label = 'Okay'
                        okay.command += self._okay
                        update = gui.Button()
                        update.label = 'Duplicate'
                        update.command += self._duplicate
                        cancel = gui.Button()
                        cancel.command += self._cancel
                        cancel.label = 'Cancel'

    def _duplicate(self, *args, **kwargs):
        cmds.duplicate(self._menu.value)
        self.base.update_bindings()

    def _cancel(self, *args, **kwargs):
        cmds.layoutDialog(dismiss='dismiss')

    def _okay(self, *args, **kwargs):
        cmds.layoutDialog(dismiss='Okay')

    def __call__(self):
        return cmds.layoutDialog(uiScript=self._layout, title='Test Dialog Events')

td = testDialog()
print(td)

Each of the buttons is working, and the duplicate command is firing properly while the dialog is active.

theodox commented 7 years ago

Does the duplicate work more than once? If you press it a few times with the dialog opne?

bob-white commented 7 years ago

Yeah, and when I was duplicating some cameras it was even spawning the little on screen boxes talking about messing with the FOV. So it seemed to only be blocking user interaction, and not actually stalling the idle thread. This is in 2016 btw, might be different in older versions.

Also updated my example to use the Observable collections, and update the bindings after a duplicate. So those events are also working properly.

theodox commented 7 years ago

I'll have to dig in a bit deeper with @mouthlessbobcat ; he and I have bumped into this in 2016 on different occasions

bob-white commented 7 years ago

The only time I've really bumped into something similar is when an uncaught exception gets raised. So at the very least the default exception handler is getting blocked. Which certainly makes debugging the dialog a pain.

I wonder if another solution might be checking which thread we're attempting to fire the event from, and if its the main thread to avoid the executeDeferred call.

theodox commented 7 years ago

So, we tried a cut and paste of your example, and it still showed the incorrect behavior for us. What version are you on?

theodox commented 7 years ago

More mystery:

import inspect
import maya.cmds as cmds
import maya.utils

def ll(*x):
    def close(*_):
        cmds.layoutDialog(dismiss = "close")

    def ok (*_):
        cmds.layoutDialog(dismiss = "OK")

    def do(*_):
        def anon():
            print "DONE"
        maya.utils.executeDeferred(anon)

    c = cmds.columnLayout()

    d = cmds.button("do", c=do)
    b1 = cmds.button("cancel", c = close)
    b2 = cmds.button("ok", c = ok)

print cmds.layoutDialog(ui=ll)

This... works. so maybe MayaEvent is not exactly the culprit...

bob-white commented 7 years ago

Maya 2016 sp6(no extensions) and Maya 2017, are the primary versions I've been working with.

So when it does go haywire, the dialog is still responsive, we just don't see anything useful until after it closes correct? We're not completely locking up Maya?

Also, if a print(threading.current_thread()) statement is added to MayaEvent._fire does that trigger immediately? Does it return MainThread? Because that was the result I was getting when testing my example from above.

bob-white commented 7 years ago

Now I'm extra confused.

import inspect
import maya.cmds as cmds
import maya.utils

def ll(*x):
    def close(*_):
        cmds.layoutDialog(dismiss = "close")

    def ok (*_):
        cmds.layoutDialog(dismiss = "OK")

    def do(*_):
        def anon():
            cmds.polySphere()
            print "DONE"
        maya.utils.executeDeferred(anon)

    c = cmds.columnLayout()

    d = cmds.button("do", c=do)
    b1 = cmds.button("cancel", c = close)
    b2 = cmds.button("ok", c = ok)

print cmds.layoutDialog(ui=ll)

Won't create any of the spheres until the dialog closes. (Maya 2017) And just tested again, and it worked properly... Maya is weird.

Mouthlessbobcat commented 7 years ago

That's more in line with the behavior we're seeing. It's along the lines of:

  1. Show LayoutDialog.
  2. Click buttons that fire events from their command.
  3. Nothing happens.
  4. Close dialog.
  5. All the events have been queued up and fire now.
theodox commented 7 years ago

Ah, so maybe this is in part a testing artifact. The prints in my example didn't wait for the closure of the dialog -- but scene activities do?

On Thu, Jan 5, 2017 at 8:21 AM, Mouthlessbobcat notifications@github.com wrote:

That's more in line with the behavior we're seeing. It's along the lines of:

  1. Show LayoutDialog.
  2. Click buttons that fire events from their command.
  3. Nothing happens.
  4. Close dialog.
  5. All the events have been queued up and fire now.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/theodox/mGui/issues/56#issuecomment-270685658, or mute the thread https://github.com/notifications/unsubscribe-auth/AD3mGOgjDD4881P5RLOpmllXZIBgrWCCks5rPRiUgaJpZM4LUhQ8 .

bob-white commented 7 years ago

Doesn't seem to be consistent though, I tried testing again with the scene activity, and it triggers properly.

bob-white commented 7 years ago

Came up with a potential idea, instead of relying on cmds.layoutDialog we could get at the underling QWidget on a Window and just enable Modality directly.

No idea if this actually sidesteps the issue or now, given how random this seems to be, it could just be a dice roll that I haven't run into it again.

But either way, it feels nicer to just define a ModalWindow in the same manner as our other windows, and not have to jump through extra hoops with the LayoutDialogForm, etc...

bob-white commented 7 years ago

I wonder if this is a part of the problem? http://around-the-corner.typepad.com/adn/2016/07/the-idle-queue-isnt-100-a-queue.html

Also, there seem to be some weird edge cases on the Qt side when using exec to block execution with a dialog, which I think is how cmds.layoutDialog actually works. http://blog.qt.io/blog/2010/02/23/unpredictable-exec/

bob-white commented 7 years ago

So, my fun ModalWindow patch won't actually fix this problem.

I think option 2 from the above list might be the better starting point. But I'd like to keep the ability to hook callbacks while inside the layout's context manager.

1 - Add a flag to Nested during LayoutDialogForm.__enter__. 2 - During Layout.add_current check to see if that flag is active, if active, add an _is_modal flag to each control. 3 - When hooking a callback, check the control for the _is_modal flag, and use that to decide between MayaEvent vs Event 4 - Remove flag from Nested during LayoutDialogForm.__exit__

At this point it shouldn't matter where we hook any callbacks, as each control will already know whether it was defined inside a modal dialog or not.

theodox commented 7 years ago

that's in line with stuff that eric and I have discussed, I think it's the least complex of all the solutions we've considered .

theodox commented 7 years ago

@bob-white that link from ADSK is almost certainly why my print test was a false indicator...

theodox commented 7 years ago

Fixed