How to show dialog at the beginning of the process and handle errors inside UI.start_timer?

I want a simple window to pop up at the beginning of a heavy process. However, the window is not loading properly because the HTML part is delayed by the Ruby script and is only rendered after the job is done. To work around this, I placed the heavy process inside UI.start_timer(0.3, false) {heavy_process} to give the HTML some time to load. It works but, the heavy process contains some raise and rescue bits that when triggered conflict with UI.start_timer. Once an error is raised and rescued, the script doesn’t stop but is instead raised and rescued repeatedly. In this case, a UI.messagebox from the rescue block pops up over and over again.

Do you have any ideas how to make the process wait for the dialog to load, or how to make raise and rescue work with UI.start_timer?

    def self.run_constructor
      PDialog.show_p_dialog

      UI.start_timer(0.3, false) {

      begin
        some process....

      rescue ComponentOutOfBoundsError
        UI.messagebox("no luck")
        PDialog.close_p_dialog
      end
      }
    end

This is a known problem, See the following code and the explanation in the link below.

def self.run_constructor
  # This must be outside of the timer Proc
  first_invocation = true
  
  UI.start_timer(0.3, false) {
    begin
      sleep(1)
      x = 1/0 # raise an error

    rescue 
      if first_invocation 
        first_invocation = false      
        UI.messagebox("no luck")
      end

    ensure 
      p "Close"  
      
    end
  }
end

run_constructor()
nil

We do this all the time without using timer blocks.

At the bottom of the dialog’s Javascript …

// Tasks to be performed after the document has loaded:
function tasksPostLoad(event) {

    // No longer need this event listener:
    document.removeEventListener("DOMContentLoaded",tasksPostLoad);

    // Tell the SketchUp Ruby-side the document has loaded:
    sketchup.dialog_ready();

    // other post load task ...

}

// Attach master document DOMContentLoaded event listener:
document.addEventListener("DOMContentLoaded",tasksPostLoad);

The Ruby-side dialog code needs to attach an action callback that reponds to the call from the JS DOMContentLoaded event handler function.

In the following snippet, (taken from an example I am writing to show font size control like in normal browsers,) … I have named it "dialog_ready", but it can be whatever name you desire, as long as the name used in the call upon the sketchup JS object and the name assigned in the Ruby #add_action_callback method call match.

So, this snippet defines three action callbacks:


    def add_callbacks
      #
      unless @dialog
        puts 'Dialog object not instantiated.'
        return
      end
      #
      # Do initial tasks after dialog has loaded, like setting font size.
      # Also, tells the dialog what platform SketchUp is running upon.
      @dialog.add_action_callback('dialog_ready') {
        puts 'In callback: "dialog_ready"' if @debug
        # Restore the font size from the previous use as set by user:
        font_size = format('%.3f',@font_size)
        @dialog.execute_script(%[fontRestore(#{font_size});])
        # Set the dialog's JS var `WINDOWS` as appropriate for platform:
        val = (Sketchup.platform == :platform_win rescue RUBY_PLATFORM !~ /darwin/)
        puts "  Platform WINDOWS is: #{val}" if @debug
        @dialog.execute_script(%[WINDOWS = #{val};])
        # Let the dialog know the current sort_by key:
        @dialog.execute_script(%[sort_by = "#{@sort_by.to_s}";])
      }
      #
      # Callback to save the font size as it is changed by the user:
      @dialog.add_action_callback('save_font_size') { |_action_context, fontsize|
        save_font_size(fontsize)
      }
      # Callback to resort the shortcuts table data when the user clicks
      # upon one of the column headings in the dialog document:
      @dialog.add_action_callback('resort_shortcuts') { |_action_context, sort_by|
        puts 'In callback: "resort_shortcuts"' if @debug
        # Resort the shortcuts listing and send back to the dialog:
        resort_shortcuts(sort_by) if sort_by
      }
      #
    end

The Ruby-side UI::HtmlDialog callbacks should usually be wrapped up in a method so they can be reattached in case the user has closed the window. When the window is closed the callbacks are detached but the Ruby dialog object is still valid.
If your code is going to reshow the dialog window (say if a toolbar button is clicked,) then the callbacks need to be reattached. This is most easily done by calling the method that wraps up the callback definitions.

Thank you very much for the answer. I have made a changes based on your suggestion but the problem persists. That is, the dialog appears but the text inside the <p> tag is not rendered until the run_constructor function is finished. Do you have any idea why that might be?

    def self.show_progress_dialog
      dialog_width = 200
      dialog_height = 150

      viewport = Sketchup.active_model.active_view.vpwidth, Sketchup.active_model.active_view.vpheight
      left = (viewport[0] - dialog_width) / 2
      top = (viewport[1] - dialog_height) / 2

      @progress_dialog = UI::HtmlDialog.new(
        {
          :dialog_title => "ElipseGrade",
          :preferences_key => "com.sample.plusdsadagin",
          :scrollable => false,
          :resizable => false,
          :width => dialog_width,
          :height => dialog_height,
          :left => left,
          :top => top,
          :style => UI::HtmlDialog::STYLE_DIALOG
        })

      @progress_dialog.set_html('
        <!DOCTYPE html>
        <html>
        <head>
          <title>Processing</title>
          <style>
            body {
              background-color: #F3F3F3;
              font-family: Arial, sans-serif;
            }
            p {
              text-align: center;
              padding-top: 10px;
              font-size: 0.9em;
            }
          </style>
        </head>
        <body>
          <p>Processing...<br><br>Please wait</p>
        </body>
        <script>
        function dialog_ready() {
          document.removeEventListener("DOMContentLoaded", dialog_ready);
          sketchup.progress_dialog_ready();
        }
        document.addEventListener("DOMContentLoaded", dialog_ready);
        </script>
        </html>
      ')

      @progress_dialog.add_action_callback("progress_dialog_ready") do |action_context|
        puts "Dialog ready"
        Constructor.run_constructor
      end
      @progress_dialog.show
    end

image

The callback code needs a timer for your task.

See attached, if you uncomment line 38 and comment out line 39, it repros your issue.

Wrote the code for Ruby 2.2 (and later) compatibility.

Greg

00-dialog.rb (2.0 KB)

Hi Greg, Tank you very much for the answer. This is the solution I have tried initially but the problem is that the error handling inside the UI.start_timer is not working (description in the initial post) and this is the functionality that I need.

Nevermind this reply ... I solved it. ... See next post.

I have no idea what is in the run_constructor method.

Are you saying that the text: "Processing... Please wait" … is not rendering until after Constructor.run_constructor method returns ?

If so, then you might change from listening for DOMContentLoaded to the load event on the window object. This event is supposed to not fire until the page is totally loaded.

UPDATE: No change using window onload event

REF: Window: load event - Web APIs | MDN (mozilla.org)

Ex:

        <script>
        function dialog_ready() {
          window.removeEventListener("load", dialog_ready);
          sketchup.progress_dialog_ready();
        }
        window.addEventListener("load", dialog_ready);
        </script>

SOLVED it without a timer !

Further testing shows there still are issues with the HTML dialog approach to displaying progress.


The key / trick is to use a dummy JS callback object and then
the sketchup.progress_dialog_ready callback does not block.

ie … I just used a statement writing a console message:

Notice also that I sent an unused empty string parameter to the Ruby callback. In some older versions the callbacks would not fire unless at least one parameter was sent to Ruby.

  const dialog_ready = () => sketchup.progress_dialog_ready("",
    {
      onCompleted: function() {
        console.log('Ruby callback "progress_dialog_ready" has returned.');
      }
    }
  );
  window.addEventListener("load", dialog_ready);

I used Greg’s example to test, and added in a percent_done() method to draw a graphic progress indicator.


ProgressBar_dialog_fixed.rb (3.7 KB)

Hey @DanRathbun ,

I am coming back you your reply after some time. Thanks for your answer, I have run your file and the issue is still there. Do you have any idea what might cause the difference in the behavior?

Since I posted that, I continued testing. I also ran into strange variations where it would fail either with or without the Ruby Console open, but more so in one of the scenarios.

So, sorry, I “jumped the gun” in saying it was solved. It continues to be a problem.
I intended to update what I had previously posted, but then I got distracted off of that onto other projects.

Basically, the HTML Dialogs are still problematic when opened from asynchronous callback blocks.


But … it would still be nice for extension coders to have a nifty way to display progress to users.

I have an idea that might suffice but I need to code it up and test it out.

Okay so I found the test file I was working on. Reran it.

I had a note in the file that it would only run correctly when the Ruby Console was open and the task was sending output text to $stdout (which is SketchUp’s Ruby console object.)

Often the progress bar stops about halfway through the progress (at ~5 secs and sometimes the dialog goes “Not Responding”) until the progress completes then the progress bar fills up in one step.
This most often happens when I hide the Ruby Console whilst the progress dialog is showing.

I have never gotten a blank dialog like you show. I always get the webpage rendered completely in SketchUp 2021.1, 2023.1 (using CEF 88) and 2024.0 (the latter which uses Chromium 112.)

If you are still using SketchUp 2021, then it uses Chromium v64 in SU 21.0 and Chromium v88 for SU 21.1. So yes, there can be differences as the CEF fixes bugs.


Here is another test file that lets you try the different combinations using Boolean flags:

  • silent : the tasks does not output text to $stdout
  • hide_console : whether the Ruby Console is open or hides during the operation
  • timer : whether the operation is wrapped within a timer block or not.

Ex:

ProgressDialog.show(hide_console: true, silent: true, timer: false)

ProgressBar_dialog_test_2.rb (4.7 KB)


One of the other things I tried to get SketchUp to update the UI, was to force the focus to SketchUp’s application window and then back to the progress dialog. But it was too annoying with the caption bars changing color back and forth throughout the task’s progress.

There is no way to get a progress bar or anything visual during a long task if you do not give back control to the UI of Sketchup explicitely.

So, the best is to cut the long task into small chunks (steps), which also means that you need a re-entrant calculation.

Say you have @nb_steps = 20. Then, the following method will serve to display the status in the UI (here via the status bar):

def process_step(istep)
  time_start = Time.now

  for i in istep..@nb_steps-1
    #Update the status bar
    Sketchup.set_status_text '|' * istep
    Sketchup.set_status_text "Processing Step", SB_VCB_LABEL
    Sketchup.set_status_text istep.to_s, SB_VCB_VALUE

    #Do the processing for the step
    sleep(0.1)

    #Check if the delay for processing has exceeded 1 second
    if Time.now - time_start > 1
      UI.start_timer(0) { process_step(istep + 1) }
      return
    end

  end

  #Processing done
  ....

end

You launch the processing by calling process_step(0).

For the timer, you can use the Sketchup one, or if you are in the context of an HtmlDialog, the Javascript setTimeout.

The discussion shifted away from the error handling toward a topic of a progress bars.

Anyway,
I have found a faulty piece in the code by fiddling with all the provided solutions (thank you).

The issue that caused the error handling inside the UI.start_timer to go bananas was UI.messagebox(). I have changed it to HtmlDialog and it works ok.

The effective way to report that the dialog was fully loaded is yet to be discovered. For now we can just wait (start_timer). :stuck_out_tongue:

Thanks for all your answers @sWilliams @DanRathbun @MSP_Greg @Fredo6.
Cheers
t.

PS: It would be great to be able to have a spinner like this :Spinners | Trimble Modus Bootstrap Developer Guide in the first dialog that pops before the big process starts and actually keeps spinning until the end.

Oh, wow. We have known like forever that modal dialogs opened from within a timer block are problematic.

Especially messageboxes, a zillion of them can open up before you can use Task Manager to shut SketchUp down.

The doc warns …

Note that there is a bug that if you open a modal window in a non-repeating timer the timer will repeat until the window is closed.


I have been more successful regarding a progress indicator using an overlay, but they are 2023 and higher.

You are still on SU 2021 ?

:man_facepalming: It was there

yes