Cancelling a long process from the GUI

Ask software engineering and SDK questions for developers working on Mac OS X, Windows or Linux.
  • Author
  • Message
Offline

philipbowser

  • Posts: 267
  • Joined: Tue Oct 14, 2014 11:53 pm

Cancelling a long process from the GUI

PostFri Dec 01, 2023 12:23 am

Hey folks, I'm building a Workflow Integration for Resolve and I'm really stumped in trying to figure out a proper way to cancel a long process from a GUI button. I have a button that executes a function which iterates over a lot of items and takes a while. I would love to include a "Cancel" button next to the progress bar that allows the user to cancel the iterations. However, once the function is ran, the GUI locks up and button clicks are not registered until the function returns.

So far I've only come up with one way to do this, but it's kind of hacky and I feel like there must be a proper way. I've looked into Lua coroutines and the mysterious UITimer that isn't documented, but I couldn't figure out how to make those work.

Here's an example of what I've previously tried that does not work. The GUI is frozen until the "some_long_process" function returns.
Code: Select all
local temp_folder = Fusion():MapPath("Temp:")
local ui = Fusion().UIManager
local disp = bmd.UIDispatcher(ui)
local cancel

local some_long_process = function ()
   print("beginning long process")
   for i = 1, 10 do
      if cancel then
         return
      end
      print("executing: ", i)
      bmd.wait(1)
   end
   print("ending long process")
   return
end

local function my_window()
   local width,height = 200, 50
      
   local win = disp:AddWindow({
      ID = "my_window",
      WindowTitle = "My Window",
      WindowFlags = {Window = true, WindowStaysOnTopHint = true,},
      Geometry = {100, 100, width, height},
      Spacing = 10,
      Margin = 20,
   
      ui:VGroup{
         ID = 'root',
         Weight = 0,
         
         ui:HGroup{
            Weight = 0,
            ui:Button{
               ID = "start",
               Text = "Start",
            },
            ui:Button{
               ID = "cancel",
               Text = "Cancel",
            },
         },
      },
   })

   win:RecalcLayout()
   
   function win.On.start.Clicked(ev)
      cancel = false
      some_long_process()
   end
   
   function win.On.my_window.Close(ev)
      disp:ExitLoop()
   end
   
   function win.On.cancel.Clicked(ev)
      cancel = true
      print("cancel clicked")
   end
   
   return win
end

local my_window = my_window()

my_window:Show()
disp:RunLoop()
my_window:Hide()

And here's a hacky way that I've been able to do this. I'm writing the "some_long_process" function to disk as a lua script, then using RunScript to run that script which doesn't block the GUI, then using Fusion custom data to pass the cancel variable to it. This can't be the right way to do this though.
Code: Select all
local temp_folder = Fusion():MapPath("Temp:")
local ui = Fusion().UIManager
local disp = bmd.UIDispatcher(ui)


local some_long_process = function ()
   print("beginning long process")
   for i = 1, 10 do
      cancel = Fusion():GetData('cancel')
      if cancel then
         print("long process cancelled")
         return
      end
      print("executing: ", i)
      bmd.wait(1)
   end
   print("ending long process")
   return
end

local function my_window()
   local width,height = 200, 50
      
   local win = disp:AddWindow({
      ID = "my_window",
      WindowTitle = "My Window",
      WindowFlags = {Window = true, WindowStaysOnTopHint = true,},
      Geometry = {100, 100, width, height},
      Spacing = 10,
      Margin = 20,
   
      ui:VGroup{
         ID = 'root',
         Weight = 0,
         
         ui:HGroup{
            Weight = 0,
            ui:Button{
               ID = "start",
               Text = "Start",
            },
            ui:Button{
               ID = "cancel",
               Text = "Cancel",
            },
         },
      },
   })

   win:RecalcLayout()
   
   function win.On.start.Clicked(ev)
      Fusion():SetData('cancel', nil)
      local serialized = string.dump(some_long_process)
      local path = temp_folder.."some_long_process.lua"
      local out = io.open(path, "wb")
      out:write(serialized)
      out:close()
      Fusion():RunScript(path)
   end
   
   function win.On.my_window.Close(ev)
      Fusion():SetData('cancel', nil)
      os.remove(temp_folder.."some_long_process.lua")
      disp:ExitLoop()
   end
   
   function win.On.cancel.Clicked(ev)
      Fusion():SetData('cancel', true)
      os.remove(temp_folder.."some_long_process.lua")
   end
   
   return win
end

local my_window = my_window()

my_window:Show()
disp:RunLoop()
my_window:Hide()

If anyone is generous enough to help me figure out the proper way to tackle this problem I would be really grateful!
Thanks!
Offline
User avatar

roger.magnusson

  • Posts: 3399
  • Joined: Wed Sep 23, 2015 4:58 pm

Re: Cancelling a long process from the GUI

PostFri Dec 01, 2023 4:28 pm

Depending on what the long process is, maybe you can use disp:StepLoop() instead of disp:RunLoop(). Then you'd have to create your own loop and call StepLoop() in it each time you want to dispatch UI events.
Offline
User avatar

Andrew Hazelden

  • Posts: 538
  • Joined: Sat Dec 06, 2014 12:10 pm
  • Location: West Dover, Nova Scotia, Canada

Re: Cancelling a long process from the GUI

PostSat Dec 02, 2023 4:57 pm

roger.magnusson wrote:Depending on what the long process is, maybe you can use disp:StepLoop() instead of disp:RunLoop(). Then you'd have to create your own loop and call StepLoop() in it each time you want to dispatch UI events.


Nice.Thanks for the UI manager tip Roger!!! I've not tried StepLoop() before. :)
Mac Studio M2 Ultra / Threadripper 3990X | Fusion Studio 18.6.4 | Kartaverse 6
Offline

philipbowser

  • Posts: 267
  • Joined: Tue Oct 14, 2014 11:53 pm

Re: Cancelling a long process from the GUI

PostSun Dec 03, 2023 5:33 pm

Thanks Roger! I didn't know about StepLoop() before. I'll look into this. I appreciate your help!
Offline

philipbowser

  • Posts: 267
  • Joined: Tue Oct 14, 2014 11:53 pm

Re: Cancelling a long process from the GUI

PostSun Dec 03, 2023 8:07 pm

For anyone else who is interested, I was able to get StepLoop() and Lua coroutines to work for my needs. I'll post some sample code below. Huge thanks to Roger for helping with this!

One thing I learned is that each time StepLoop() is called, it only registers one UI event in the queue at a time. So if you you triggered any other UI events before pressing the cancel buttons, those would have to be looped through before the cancel button event is registered. Which means there could be a delay in when the cancel button is pressed and when the function is actually cancelled. I got around this in the example by just disabling the start button once the function starts so that it can't generate any UI events, but I'm sure there must be smarter approaches for more complex UIs.

Code: Select all
local ui = Fusion().UIManager
local disp = bmd.UIDispatcher(ui)
local cancel, coro, run_loop

local function some_long_process()
   print("beginning long process")
   for i = 1, 10 do
      if cancel then
         print("long process cancelled")
         return
      end
      print("executing: ", i)
      bmd.wait(1)
      coroutine.yield()
   end
   print("ending long process")
   return
end

local function my_window()
   local width,height = 200, 50
      
   local win = disp:AddWindow({
      ID = "my_window",
      WindowTitle = "My Window",
      WindowFlags = {Window = true, WindowStaysOnTopHint = true,},
      Geometry = {100, 100, width, height},
      Spacing = 10,
      Margin = 20,
   
      ui:VGroup{
         ID = 'root',
         Weight = 0,
         
         ui:HGroup{
            Weight = 0,
            ui:Button{
               ID = "start",
               Text = "Start",
            },
            ui:Button{
               ID = "cancel",
               Text = "Cancel",
            },
         },
      },
   })

   win:RecalcLayout()
   
   function win.On.start.Clicked(ev)
      if coro == nil then
         cancel = false
         coro = coroutine.create(some_long_process)
      end
   end
   
   function win.On.my_window.Close(ev)
      cancel = true
      run_loop = false
   end
   
   function win.On.cancel.Clicked(ev)
      cancel = true
   end
   
   return win
end

local my_window = my_window()

my_window:Show()

run_loop = true
while run_loop == true do
   disp:StepLoop()
   if coro ~= nil then
      if coroutine.status(coro) ~= "dead" then
         my_window:Find('start').Enabled = false
         coroutine.resume(coro)
      else
         coro = nil
         my_window:Find('start').Enabled = true
      end
   end
end

my_window:Hide()
Offline

creonovo

  • Posts: 6
  • Joined: Wed Sep 18, 2013 7:26 pm
  • Real Name: Ari Brown

Re: Cancelling a long process from the GUI

PostFri Dec 22, 2023 8:29 pm

I am trying to do this exact same thing, however I have been using Python for all of my scripting and GUI.
I've been trying to use StepLoop() as well, however when calling it from Python, it errors because it seems to want a variable passed through - however, I have no idea what it is expecting. Any clues to what that should be or how it can be used with Python?
Offline
User avatar

roger.magnusson

  • Posts: 3399
  • Joined: Wed Sep 23, 2015 4:58 pm

Re: Cancelling a long process from the GUI

PostFri Dec 22, 2023 9:40 pm

I haven't tried it with Python but the variable that StepLoop accepts is a boolean. Can't remember right now what it's for.
Offline
User avatar

roger.magnusson

  • Posts: 3399
  • Joined: Wed Sep 23, 2015 4:58 pm

Re: Cancelling a long process from the GUI

PostFri Dec 22, 2023 9:52 pm

I think it determines whether or not to wait until an event is received.
Offline

creonovo

  • Posts: 6
  • Joined: Wed Sep 18, 2013 7:26 pm
  • Real Name: Ari Brown

Re: Cancelling a long process from the GUI

PostFri Dec 22, 2023 10:03 pm

Thanks Roger - If StepLoop() is empty, it says it is missing 1 required positional argument: 'wait'. I tried feeding it a bool, however that returns back an AttributeError: 'dict' object has no attribute 'done'. This leads me to believe it is looking for a dictionary object, but I'm not sure exactly what that should be.
Offline
User avatar

roger.magnusson

  • Posts: 3399
  • Joined: Wed Sep 23, 2015 4:58 pm

Re: Cancelling a long process from the GUI

PostFri Dec 22, 2023 10:28 pm

Yes, looking at it now it seems they either made a mistake when porting the Lua version of UIDispatcher to Python, or we're not using StepLoop() correctly as it assumes the internal dict attribute _data.done has a value (apologies if I'm not using the correct Python nomenclature).

As a workaround, you can try executing dispatcher.ExitCode() before starting your loop, it sets _data.done to False and I hope it doesn't automatically end execution of the script.
Offline

philipbowser

  • Posts: 267
  • Joined: Tue Oct 14, 2014 11:53 pm

Re: Cancelling a long process from the GUI

PostFri Jan 05, 2024 4:09 pm

In case anyone is interested, HitsugiYukana has posted about how to use the UITimer properly here:
https://www.steakunderwater.com/wesuckl ... php?t=6361
Offline

creonovo

  • Posts: 6
  • Joined: Wed Sep 18, 2013 7:26 pm
  • Real Name: Ari Brown

Re: Cancelling a long process from the GUI

PostFri Jan 05, 2024 5:42 pm

It looks like the Lua code all works with the StepLoop. I’ve unfortunately still been unable to get it to work with Python. I tried Roger’s suggestion of the ExitCode, but it doesn’t seem to make any difference. Perhaps the StepLoop code is just broken with the Python implementation. I’m not sure there is any way around this that would work with interrupting the loop process. If anyone has any knowledge on this it would be greatly appreciated.

Return to Software Developers

Who is online

Users browsing this forum: No registered users and 10 guests