I'm used to working with dialogs that are modal and blocking: all activity grinds to a halt until the dialog box is handled.
That makes life easy, but the problem with that approach is that Ubuntu decides that if an application is waiting in a sleeping state, it needs to pop up a dialog warning the user that their program might be broken, which is really jarring.
This isn't an issue with emulated dialogs, since the program is busy drawing and redrawing the screen, even when it looks like it's doing nothing.
But with "native" dialogs, where the program has called to the shell asking for some utility like zenity or kdialog to interact with the user, it really is doing nothing, and after a few seconds, Ubuntu gets all nervous about that.
So I've had to make "native" dialogs asynchronous. That requires a bit of gymnastics to pull off, but fortunately, it's not too difficult.
The first problem is that the dialog handling has to be done in another thread. Fortunately, that's pretty easy in Love, the version of Lua that I'm using. The thread is created by telling what code to run, and then started up:
thread = love.thread.newThread( "dialogThread.lua" ) thread:start()
The thread runs an endless loop, watching a queue. When a command comes in, it executes the command, and passes the result back to via a queue.
I've kept things simple - parameters are passed in via a simple table, and back with another table. The first value in the table is the command to execute. The queue holding messages for the thread are placed in the queue toThread, and the messages to the caller are placed in the queue fromThread:
require("tinyFileDialog") -- loop forever while true do -- watch channel local a = love.thread.getChannel( 'toThread' ):pop() if a then local cmd = a[1] if cmd == "messageBox" then local response = messageBox( a[2], a[3], a[4], a[5], a[6] ) love.thread.getChannel( 'fromThread' ):push( {response, a[7]} ) elseif cmd == "inputBox" then local response = inputBox( a[2], a[3], a[4] ) love.thread.getChannel( 'fromThread' ):push( {response, a[5]} ) elseif cmd == "saveFileDialog" then local response saveFileDialog( a[2], a[3], a[4], a[5] ) love.thread.getChannel( 'fromThread' ):push{response, a[5]} elseif cmd == "openFileDialog" then openFileDialog( a[2], a[3], a[4], a[5], a[6] ) love.thread.getChannel( 'fromThread' ):push{response, a[7]} end end end
The calls to the thread are simple - the parameters are passed in a table using the queue toThread.
There's one one complication, however - the callback datatype can't be passed in the queue. So the function storeCallback puts the callback in a table, and returns the index to the position in the table. The rest of the code is pretty trivial:
local function messageBox( title, message, dialogType, iconType, defaultButton, callback ) callback = storeCallback( callback ) love.thread.getChannel( 'toThread' ):push{ "messageBox", title, message, dialogType, iconType, defaultButton, callback } end local function inputBox( title, message, default, callback ) callback = storeCallback( callback ) love.thread.getChannel( 'toThread' ):push{ "inputBox", title, message, default, callback } end function saveFileDialog( title, defaultPathAndFile, filters, singleDescription ) callback = storeCallback( callback ) love.thread.getChannel( 'toThread' ):push( { "saveFileDialog", title, defaultPathAndFile, filters, singleDescription, callback } ) end local function openFileDialog( title, defaultPathAndFile, filters, singleDescription, allowMultipleSelects, callback ) callback = storeCallback( callback ) love.thread.getChannel( 'toThread' ):push( { "openFileDialog", title, defaultPathAndFile, filters, singleDescription, allowMultipleSelects, callback } ) end
Finally, the love callback update is called on a regular basis. It checks the fromThread queue to see if there are any results waiting to be processed. It expects a single result and a callback index (or nil if there is no callback). The function retrieveCallback takes the index, finds the previously stored callback, and returns it. It then runs the callback:
function love.update( dt ) -- Get the info channel and pop the next message from it. local a = love.thread.getChannel( 'fromThread' ):pop() if a then -- is there a callback? if a[2] then local callback = retrieveCallback( a[2] ) -- trigger the callback callback( a[1] ) end end -- Make sure no errors occurred. local error = thread:getError() assert( not error, error ) end
That's all there is to it.
As an example, here's some code that launches displays an inputBox, When the user closes the dialog, it calls the function myCallback, which displays an messageBox with the results:
local function myCallback( value ) messageBox( "My InputBox Callback", "You entered: "..(value or "nil") ) end inputBox( "My InputBox", "Enter a Value", nil, myCallback )
Unfortunately, this "use threads" doesn't seem to work under OS X - probably because OS X asks for permission to do everything, from accessing the link library to allowing the bash shell permission to open the file dialog.
Because it's running on a background thread, some of the permissions requests don't even get shown, and the routine silently fail.
So what's to be done?
For the moment, I'm going to put the code aside and just focus on synthesis.
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.