Is use of "startWorker" to make a non-GUI-blocking file dialog possible ?

Hi,

I’m using this code to call wxPython Phoenix from within Blender. I need to call up a file open dialog but keep Blender responsive. The entire Blender script is at the end of the message. The actual file load function is triggered by a key press from Blender Game Engine (full project here: https://blenderartists.org/forum/showthread.php?404099-DJ-turntable-platter-physics-simulation-Link-sound-pitch-to-platter-rotation-speed ).

···

class MainWindow(wx.Frame):
def init(self, parent):
wx.Timer.init(self)
startWorker(self.openFileConsumer, self.openFileWorker)

def openFileWorker (self):
    print ("openFileWorker started")
    openFileDialog = wx.FileDialog(None, "Open XYZ file", "", "", "XYZ files (*.xyz)|*.xyz", wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
    if openFileDialog.ShowModal() == wx.ID_CANCEL:
        return     # the user changed idea...
    # proceed loading the file chosen by the user
    # this can be done with e.g. wxPython input streams:
    input_stream = wx.FileInputStream(openFileDialog.GetPath())
    if not input_stream.IsOk():
        wx.LogError("Cannot open file '%s'."%openFileDialog.GetPath())
    return

def openFileConsumer (self):
    print ("openFilecomsumerStarted")
    print (path)

def openFile (cont):
print (“File load triggerred”)
app = wx.App(False)
win = MainWindow(None)
app.MainLoop()


Both “openFileWorker” and “openFile” are entered because the print message shows in my console. But after some standard GTK errors on my machine like “(blender:6455): Gtk-WARNING **: Error loading theme icon ‘edit-find’ for stock: Fatal error reading PNG image file: Invalid IHDR data” (I get that when things are working normally) there’s a Segmentation Fault.


Blender 2.77 (sub 3), Commit date: 2016-08-27 22:07, Hash a5261e0

bpy.ops.transform.translate(value=(0, 0, 0), constraint_axis=(False, False, False), constraint_orientation=‘GLOBAL’, mirror=False, proportional=‘ENABLED’, proportional_edit_falloff=‘SMOOTH’, proportional_size=1, release_confirm=True) # Operator

backtrace

./blender(BLI_system_backtrace+0x1d) [0x1984c7d]
./blender() [0x1030939]
/lib/x86_64-linux-gnu/libc.so.6(+0x354a0) [0x7fe6f30374a0]
/home/michaelzfreeman/Downloads/blender-2.77-a5261e0-linux-glibc219-x86_64/2.77/python/lib/python3.5/site-packages/wx/libwx_gtk2u_core-3.0.so.0(_ZNK12wxDialogBase23GetParentForModalDialogEP8wxWindowl+0x57) [0x7fe6cf3f81e7]
/home/michaelzfreeman/Downloads/blender-2.77-a5261e0-linux-glibc219-x86_64/2.77/python/lib/python3.5/site-packages/wx/libwx_gtk2u_core-3.0.so.0(_ZN8wxDialog9ShowModalEv+0x9b) [0x7fe6cf35d2ab]
/home/michaelzfreeman/Downloads/blender-2.77-a5261e0-linux-glibc219-x86_64/2.77/python/lib/python3.5/site-packages/wx/libwx_gtk2u_core-3.0.so.0(_ZN12wxFileDialog9ShowModalEv+0x30) [0x7fe6cf3628d0]
/home/michaelzfreeman/Downloads/blender-2.77-a5261e0-linux-glibc219-x86_64/2.77/python/lib/python3.5/site-packages/wx/_core.cpython-35m-x86_64-linux-gnu.so(+0x48d119) [0x7fe6cfd85119]
./blender(PyCFunction_Call+0xb9) [0x296e189]
./blender(PyEval_EvalFrameEx+0x8766) [0x29e7536]
./blender() [0x29e860e]
./blender(PyEval_EvalCodeEx+0x23) [0x29e86e3]
./blender() [0x294a856]
./blender(PyObject_Call+0x5c) [0x291ffac]
./blender(PyEval_EvalFrameEx+0x1010) [0x29dfde0]
./blender() [0x29e860e]
./blender(PyEval_EvalCodeEx+0x23) [0x29e86e3]
./blender() [0x294a856]
./blender(PyObject_Call+0x5c) [0x291ffac]
./blender(PyEval_EvalFrameEx+0x1010) [0x29dfde0]
./blender(PyEval_EvalFrameEx+0x754c) [0x29e631c]
./blender(PyEval_EvalFrameEx+0x754c) [0x29e631c]
./blender() [0x29e860e]
./blender(PyEval_EvalCodeEx+0x23) [0x29e86e3]
./blender() [0x294a784]
./blender(PyObject_Call+0x5c) [0x291ffac]
./blender() [0x2939e84]
./blender(PyObject_Call+0x5c) [0x291ffac]
./blender(PyEval_CallObjectWithKeywords+0x47) [0x29dece7]
./blender() [0x2a350e2]
/lib/x86_64-linux-gnu/libpthread.so.0(+0x76fa) [0x7fe6f46136fa]
/lib/x86_64-linux-gnu/libc.so.6(clone+0x6d) [0x7fe6f3108b5d]


I must admit I’m a bit new to wxPython and Python in general, but at least as far as I can see, it should work. However I have studied https://wiki.wxpython.org/LongRunningTasks and https://wiki.wxpython.org/Non-Blocking%20Gui as well as http://stackoverflow.com/questions/7666368/how-to-implement-a-thread-in-a-wxpython-gui-application

… and it suggests that I can’t create any wx GUI item in another thread only in the main thread, hence the crash ?


DJ Turntable Physics Simulation, Michael Z Freeman, 2016

See CHANGELOG.TXT, TODO.TXT & README.TXT in text data blog for changelog.

Better name “Blender Game Engine DJ Turntable” ?

This is the main Python file for the project. I decided not to pile everything into a single monolithic Python script without logic blocks. I found the logic blocks helped me think through the problems and keep things fairly simple. I also think the logic blocks make it easier for new comers to understand what is going on. So the script is arranged into “modules” that are referenced in the logic blocks. There is only one at the moment but that will probably change when, for example, file loading is added. Controls are only through the keyboard at the moment and will probably change:

#MOUSE CLICK LEFT/RIGHT: nudge/spin platter left/right.

SPACE: start/stop.

ALT: Deck on/off

from bge import logic
import bge
import aud
import bpy
import wave
import contextlib
import wx
from wx.lib.delayedresult import startWorker

backwards_position = None
forwards_position = None
trap = 0
track_path1 = “/home/michaelzfreeman/DJ_Barney_Docs/Docs/Forge/Music/Beat-Research-Forge/DJ Turntable Platter Phyics Simulation/SEEDS_MASTER_3.wav”
track_path2 = “/home/michaelzfreeman/DJ_Barney_Docs/Docs/Forge/Music/Beat-Research-Forge/DJ Turntable Platter Phyics Simulation/Party For Your Right To Fight – Public Enemy – It Takes a Nation of Millions to Hold us Back.wav”
device = aud.device()
track = aud.Factory.file(track_path2)

Find length of track

with contextlib.closing(wave.open(track_path2,‘r’)) as f:
frames = f.getnframes()
rate = f.getframerate()
track_duration = frames / float(rate)
print("Duration: ")
print(track_duration)

Setup handle to playing track.

MUST be buffered for reverse. Where is this in documentation ?

track_buffered_reverse = track.buffer()
track_buffered_forward = track.buffer()
track_reverse = track_buffered_reverse.reverse()
track_forward = track_buffered_forward
#track_handle = device.play(track_reverse)
#track_handle.pitch = 1

make sure its stopped because we have not moved the platter yet.

#track_handle.stop()

#def playTrack (cont):
#global track
#global track_handle
#global track_buffered
#global track_reverse
#global track_handle_forwards
#global track_handle_backwards
track_handle_forwards = device.play(track_forward)
track_handle_forwards.keep = True
track_handle_forwards.pause()
track_handle_forwards.loop_count = 0
track_handle_backwards = device.play(track_reverse)
track_handle_backwards.keep = True
track_handle_backwards.pause()
#reverse_handle.reverse
#track_handle.pitch = 1
#speed = setPitch
#print("Pitch: ")
#print(speed)
#track = track.pitch(speed)

def setPitch (cont):
global track
global track_handle
global track_buffered
global track_reverse
global track_forward
global duration
global trap

#global backwards_position
#global forwards_position
#forwards_position = track_handle_forwards.position   
#backwards_position = track_handle_backwards.position
# Get object
platter = bge.logic.getCurrentScene().objects['Platter']
rot_speed = platter.worldAngularVelocity.z
#print("Rotation speed: ")
#print(rot_speed);
speed = rot_speed * -1
#print("Pitch: ")
#print(speed)
if speed >= 0:
    track_handle_backwards.pause()
    forwards_position = track_handle_forwards.position
    backwards_position = track_handle_backwards.position
    #backwards_position = track_handle_backwards.position
    #track_handle_forwards.position = forwards_position
    while (trap == 1):
        print("Forward trap triggered")
        track_handle_forwards.position = track_duration - backwards_position
        trap = 0    
    track_handle_forwards.resume()
    # playing/paused/stop status only ever shows as true ?
    #print("track_handle_backwards.status: ")
    #print(track_handle_backwards.status)
    track_handle_forwards.pitch = speed
    #print("track_handle_forwards.pitch: ")
    #print(track_handle_forwards.pitch)
    print("track_handle_forwards.position: ")
    print(track_handle_forwards.position)
else:
    track_handle_forwards.pause()
    forwards_position = track_handle_forwards.position
    backwards_position = track_handle_backwards.position
    # Only set once until direction changes
    while (trap == 0):
        print("Backwards trap triggered")
        #print("Reverse Set: ")
        #print(backwards_position - track_duration + forwards_position)
        track_handle_backwards.position = track_duration - forwards_position
        trap = 1
    #forwards_position = track_handle_forwards.position
    #track_handle_backwards.position = backwards_position
    track_handle_backwards.resume()
    track_handle_backwards.pitch = speed * -1
    #print("track_handle_backwards.pitch: ")
    #print(track_handle_backwards.pitch)
    print("track_handle_backwards.position: ")
    print(track_handle_backwards.position)  
#track = track.pitch(speed)
# rot_speed * -1
#track = track.pitch(rot_speed)   
# Old code for Actuator
#sound.pitch = rot_speed * -1
#print("Pitch: ")
#print(track.pitch)
#return rot_speed;

class MainWindow(wx.Frame):
def init(self, parent):
wx.Timer.init(self)
startWorker(self.openFileConsumer, self.openFileWorker)

def openFileWorker (self):
    print ("openFileWorker started")
    openFileDialog = wx.FileDialog(None, "Open XYZ file", "", "", "XYZ files (*.xyz)|*.xyz", wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
    #if openFileDialog.ShowModal() == wx.ID_CANCEL:
    #    return     # the user changed idea...
    # proceed loading the file chosen by the user
    # this can be done with e.g. wxPython input streams:
    #input_stream = wx.FileInputStream(openFileDialog.GetPath())
    #if not input_stream.IsOk():
    #    wx.LogError("Cannot open file '%s'."%openFileDialog.GetPath())
    #return

def openFileConsumer (self):
    print ("openFilecomsumerStarted")
    print (path)

def openFile (cont):
print (“File load triggerred”)
app = wx.App(False)
win = MainWindow(None)
app.MainLoop()

Apologies, did not see message about attaching code. Attached.

startWorker_message.txt (10.7 KB)

Michael Freeman wrote:

Apologies, did not see message about attaching code. Attached.

This is at least part of the problem:

class
MainWindow(wx.Frame):

  def __init__(self, parent):

      wx.Timer.__init__(self)

      startWorker(self.openFileConsumer, self.openFileWorker)   

You are not creating a frame, you are creating a timer in place of a class that thinks it is a frame. In the backtrace it shows that GetParentForModalDialog is on the stack so it is looking for a top-level
window to use as a parent for the dialog. The rest of the problem is probably due to trying to create and use the dialog from the thread started by startWorker as you’ve guessed. The thread that creates the wx.App is the main UI thread[1] and everything UI-related except for a
very small set of exceptions must be done in the context of that thread.

[1] It doesn’t affect your case, but for the record there are some implementation details on OSX that require that the main UI thread also the the process’s main thread too, so you can’t do tricky things like make a thread and have it be the one that creates the wx.App.

···


Robin Dunn

Software Craftsman

http://wxPython.org