Tools/com.renoise.ExampleToolSlicedProcess.xrnx/process_slicer.lua
--[[============================================================================
process_slicer.lua
============================================================================]]--

--[[

ProcessSlicer allows you to slice up a function which takes a lot of 
processing time into multiple calls via Lua coroutines. 

* Example usage:

local slicer = ProcessSlicer(my_process_func, argument1, argumentX)

-- This starts calling 'my_process_func' in idle, passing all arguments 
-- you've specified in the ProcessSlicer constructor.
slicer:start() 

-- To abort a running sliced process, you can call "stop" at any time
-- from within your processing function of outside of it in the main thread. 
-- As soon as your process function returns, the slicer is automatically 
-- stopped.
slicer:stop() 

-- To give processing time back to Renoise, call 'coroutine.yield()' 
-- anywhere in your process function to temporarily yield back to Renoise:
function my_process_func()
  for j=1,100 do
    -- do something that needs a lot of time, and periodically call 
    -- "coroutine.yield()" to give processing time back to Renoise. Renoise 
    -- will switch back to this point of the function as soon as has done 
    -- "its" job:
    coroutine.yield()
  end  
end


* Drawbacks:

By slicing your processing function, you will also slice any changes that are 
done to the Renoise song into multiple undo actions (one action per slice/yield).

Modal dialogs will block the slicer, cause on_idle notifications are not fired then. 
It will even block your own process GUI when trying to show it modal.

]]

class "ProcessSlicer"

function ProcessSlicer:__init(process_func, ...)
  assert(type(process_func) == "function", 
    "expected a function as first argument")

  self.__process_func = process_func
  self.__process_func_args = arg
  self.__process_thread = nil
end


--------------------------------------------------------------------------------
-- returns true when the current process currently is running

function ProcessSlicer:running()
  return (self.__process_thread ~= nil)
end


--------------------------------------------------------------------------------
-- start a process

function ProcessSlicer:start()
  assert(not self:running(), "process already running")
  
  self.__process_thread = coroutine.create(self.__process_func)
  
  renoise.tool().app_idle_observable:add_notifier(
    ProcessSlicer.__on_idle, self)
end


--------------------------------------------------------------------------------
-- stop a running process

function ProcessSlicer:stop()
  assert(self:running(), "process not running")

  renoise.tool().app_idle_observable:remove_notifier(
    ProcessSlicer.__on_idle, self)

  self.__process_thread = nil
end


--------------------------------------------------------------------------------

-- function that gets called from Renoise to do idle stuff. switches back 
-- into the processing function or detaches the thread

function ProcessSlicer:__on_idle()
  assert(self.__process_thread ~= nil, "ProcessSlicer internal error: "..
    "expected no idle call with no thread running")
  
  -- continue or start the process while its still active
  if (coroutine.status(self.__process_thread) == 'suspended') then
    local succeeded, error_message = coroutine.resume(
      self.__process_thread, unpack(self.__process_func_args))
    
    if (not succeeded) then
      -- stop the process on errors
      self:stop()
      -- and forward the error to the main thread
      error(error_message) 
    end
    
  -- stop when the process function completed
  elseif (coroutine.status(self.__process_thread) == 'dead') then
    self:stop()
  end
end


