Sublime Forum

[SOLVED] How to execute a function immediately?

#1

I’m working on creating an easy to implement progress bar, but am running into an issue with updating it in realtime.

My original proof of concept accomplished its effect by using an increasing delay value @ set_timeout.

 

For actual implementation, I’d like to be able to establish various points throughout code where the progress bar will be updated.

This is a simplified example of what I’m trying to achieve:

progressValues = [ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ]
burnSomeTime   = 5000000

for value in progressValues:
	sublime.set_timeout( lambda: view.show_popup( value ), 0 )

	for x in range( 0, burnSomeTime ):
		pass

( the burnSomeTime loop is meant to represent potentially resource-intense functions that are running in between progress updates )

 
The result I’m expecting:

show_popup will display the current progressValue each time the code reaches set_timeout.

( I also tried set_timeout_async )

 
The actual result:

Only the final value of progressValues is being displayed, after the entire script has finished.

1 Like

#2

This is quite simple, actually. ST by default operates on a single thread, which means that both your code and the callback specified in set_timeout run on the same thread. Thus, they will only be executed after your code has run entirely. Since they are then all executed immediately, you only get to see the results of the last call with the last value.

The proper way to do this is to reschedule the next iteration of the progress from within your set_timeout callback and sharing or storing the current state (functools.partial, (globals,) or closures).

Also, use time.sleep if you have time to burn instead of that very long loop thing.

2 Likes

#3

@FichteFoll

 
I managed to get a semi-working model by deconstructing this solution:
 

 

import sublime, sublime_plugin
import time

class TestCommand( sublime_plugin.TextCommand ):

	def run( self, edit ):

		global view
		view = self.view

		global progress_Index, progress_MaxIndex
		progress_Index    = 0
		progress_MaxIndex = 10

		view.show_popup( str( 0 ) )
		ProgressBar.update()

class ProgressBar():

	def update():

		global view
		global progress_Index, progress_MaxIndex

		progress_Index += 1

		if progress_Index > progress_MaxIndex:
			return

		else:

			progress_String = str( progress_Index )
			view.update_popup( progress_String )

			time.sleep( 0.3 )
			sublime.set_timeout_async( ProgressBar.update )

 

In order for it to work properly, I would have to execute functions in ProgressBar.update via a callback or something. ( in the placeholder area of  time.sleep )

Is there any way to keep the progress effect while allowing updates to happen throughout a regular script?
 



 
For example:

	def run( self ):

		ProgressBar.start()
		...
		...
		# stuff happening in script
		...
		...
		ProgressBar.update()
		...
		...
		# more stuff happening in script
		...
		...
		ProgressBar.update()
		...
		...
		# etc.
		...
		...
		ProgressBar.finish()
1 Like

#4

And to add to the end of that talk: If you do end up using classes as namespaces, don’t define the functions in it as instance method. Use @classmethod or @staticmethod, depending on requirements.


Regarding the code itself,

  1. I would not declare a fixed collection of iterations that a progress can go over but instead have it accept a parameter with the value (i.e. update(0.5) or 50 if you like seeing big numbers).
  2. The progress bar’s update function should be blocking, so there is no need to do a callback from within. Whatever code is using a progress bar should just call the update method to report something to the user and then go back to doing what it was initially. Much like a print call.
2 Likes

Run process in background
#5

 
That’s what I’m trying to acheive, but I haven’t had much luck.

I’ve been following How to run part of my plugin in the second thread properly? and have attempted to implement some of the modifications you made to the OP’s script.

 
Do you see any reasons why the following script is still only showing the popup in it’s final state?
( without updating through the sequence: 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 )

import sublime, sublime_plugin
import threading
import time

# This is the primary command which executes everything else
class TestCommand( sublime_plugin.TextCommand ):
	def run( self, edit ):

		global progress_Index, progress_MaxIndex
		progress_Index    = 0
		progress_MaxIndex = 100

		self.view.show_popup( str( 0 ) )

		# this loop is representative of a single script executing
		# with various progress updates throughout its execution
		for index in range( 0, 10 ):
			time.sleep( 0.2 )
			start_thread()

def start_thread():
	thread = ProgressBarThread( view )
	thread.start()

class ProgressBarThread( threading.Thread ):
	def run( self ):
		sublime.active_window().active_view().run_command( "update_progress_bar" )

# This updates the global progress data & prints current values to the PopUp
class UpdateProgressBarCommand( sublime_plugin.TextCommand ):
	def run( self, edit ):

		global progress_Index, progress_MaxIndex

		progress_Index += 10

		if progress_Index > progress_MaxIndex:
			return
		else:
			progress_String = str( progress_Index )
			self.view.update_popup( progress_String )
0 Likes

#6

@FichteFoll

Any chance you can check out my previous post in this thread?

Sorry to bug you, but I’ve pretty much been stuck on this & you’re the only person I know of with experience in this department :grin:

0 Likes

#7

@fico

  1. Let ST resume its main an UI process by returning from a Command’s run method early.
  2. Execute the work on the working thread (or usually called “worker”)
  3. Don’t use global state when it’s not necessary. Instead, save state in class instance variables/attributes.
  4. Properly use synchronous update methods for the progress bar, i.e. make them blocking.
  5. In order to modify what the progress bar displays (for example an actual progress bar), you would modify the ProgressBar.update method.

import sublime, sublime_plugin
import threading
import time


class TestCommandCommand(sublime_plugin.TextCommand):
    def run(self, edit):
        # We start the worker thread here so that ST can resume its UI operations
        # while we do stuff in that other thread
        WorkingThread(self.view).start()


class WorkingThread(threading.Thread):
    def __init__(self, view, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.view = view

    def run(self):
        progress = ProgressBar(self.view, 0)
        progress.show()

        # this loop is representative of a single script executing
        # with various progress updates throughout its execution
        for index in range(1, 11):
            time.sleep(0.2)
            progress.update(index * 10)


class ProgressBar(object):
    def __init__(self, view, value=0):
        self.view = view
        self.visible = False

        if value is not None:
            self.update(value)
        else:
            raise ValueError("must provide initial value")

    def update(self, value):
        self._text = str(value)
        if self.visible:
            self.show()

    def show(self):
        if self.visible:
            self.view.update_popup(self._text)
        else:
            self.visible = True
            self.view.show_popup(self._text, on_hide=self._on_hide)

    def _on_hide(self):
        self.visible = False


Edit: Here’s an example for a different progress bar rendering:


class ProgressBar(object):
    progress_char = "="
    nonprogress_char = " "

    def __init__(self, view, value=0, max_value=100, length=20):
        self.view = view
        self.max_value = max_value
        self.length = length

        self.visible = False

        if value is not None:
            self.update(value)
        else:
            raise ValueError("must provide initial value")

    def update(self, value):
        progress = int(value / self.max_value * self.length)
        self._text = "[{}{}]".format(self.progress_char * progress,
                                     self.nonprogress_char * (self.length - progress))
        if self.visible:
            self.show()

    def show(self):
        if self.visible:
            self.view.update_popup(self._text)
        else:
            self.visible = True
            self.view.show_popup(self._text, on_hide=self._on_hide)

    def _on_hide(self):
        self.visible = False

Another modification:


    def update(self, value):
        progress = value / self.max_value
        progress_length = int(progress * self.length)
        self._text = "[{}{}] {:4.0%}".format(self.progress_char * progress_length,
                                             self.nonprogress_char * (self.length - progress_length),
                                             progress)
        if self.visible:
            self.show()
1 Like

#8

@FichteFoll

Thank’s for the thorough response!!! :grin:

I was just hoping for a push in the right direction but it was super helpful to receive a working example.  The example code & info points you gave really helped to clarify the proper way to use Thread.
 



 
I ended up re-integrating my ProgressBar POC as an object like you demonstrated in your example.

Currently, it allows simulation of dynamic progress display via the following parameters:

update( percentRange, increment, sleepTime, message )

 

 
Still have to clean up the implementation a bit, but I’ll be releasing this as part of my framework when it’s ready.

Thanks again for your help!

1 Like

#9

For reference, here is a template example I’ve put together for implementing sequential functions.

The commented lines @ start denote the intended order of execution.

( Thanks @FichteFoll for pointing me in the direction of classmethods :grin: )
 

import sublime, sublime_plugin

class test_sequential_function( sublime_plugin.TextCommand ):
	def run( self, edit ):

		SequentialFunction.start()

class SequentialFunction:

	@classmethod
	def start( CLASS ):

		CLASS.get_InputAnswer()
		# » CLASS.get_DialogAnswer()
		#   » CLASS.show_Answers()

	@classmethod
	def get_InputAnswer( CLASS ):

		sublime.active_window().show_input_panel( "Enter Text: ", "", CLASS.get_DialogAnswer, None, None )

	@classmethod
	def get_DialogAnswer( CLASS, inputAnswer ):

		CLASS.inputAnswer = inputAnswer

		dialogInteger = sublime.yes_no_cancel_dialog( "Select An Option" )

		if   dialogInteger == 0: CLASS.dialogAnswer = "Cancel"
		elif dialogInteger == 1: CLASS.dialogAnswer = "Yes"
		elif dialogInteger == 2: CLASS.dialogAnswer = "No"

		CLASS.show_Answers()

	@classmethod
	def show_Answers( CLASS ):

		answers = \
				"Input Answer:   " + CLASS.inputAnswer  + "\n" \
			+	"Dialog Answer:  " + CLASS.dialogAnswer

		answerPanel = sublime.active_window().create_output_panel( "answerPanel" )
		sublime.active_window().run_command( "show_panel", { "panel": "output.answerPanel" } )
		answerPanel.run_command( "insert", { "characters": answers } )
1 Like

#10

I will just not say anything about this …

0 Likes

#11

@FichteFoll

Is there a better way to go about it?

If you tried to execute the 3 command groups in a single method, it wouldn’t work since show_input_panel doesn’t block the thread like yes_no_cancel_dialog does.  The code I posted works & makes it easy to sequentially execute multiple commands, while waiting for each previous command to finish.

0 Likes

#12

You’re confusing things. Mainly, you are having problems with synchronous and asynchronous function call paradigmas. And you’re abusing classes. And your “boilerplate” is pretty application-specific and not helpful for general similar problems.

Asynchronous function calls work in a way that you provide a callback to the call, which will be called (potentially with arguments) once the function is “done”. This is done for the show_input_panel API because it is unknown how long executing it will take and the UI thread must not be locked in the meantime, i.e. the command should return and continue asynchronously (by providing a callback to show_input_panel).

And by the way, run_command is a synchronous function.

Here is how I would write the exact same code:

class test_sequential_function(sublime_plugin.WindowCommand):
    def run(self):
        self.window().show_input_panel("Enter Text: ", "", self.on_input_done, None, None)
        
    def on_input_done(self, text):
        yes_no_result = sublime.yes_no_cancel_dialog("Select An Option")
        yes_no_text_result = ("Cancel", "Yes", "No")[yes_no_result]

        answer_text = (
            "Input Answer:   {}\n"
            "Dialog Answer:  {}"
            .format(text, yes_no_text_result)
        )

        panel = self.window.create_output_panel("answerPanel")
        self.window.run_command("show_panel", {"panel": "output.answerPanel"})
        panel.run_command( "insert", {"characters": answer_text})

I hope that can clarify a bit.

4 Likes