RhinoApp.EscapeKeyPressed event, unable to cancel/consume keypress event

I’m trying to use the RhinoApp.EscapeKeyPressed event to cancel a background worker that is running inside a command. What I find, however, is that my command gets canceled after the Esc key is pressed (which also stops the background worker).

Any ideas how to cancel out of a long running background task but keep the command itself alive? It seems a possible solution would be to be able to consume the EscKeyPressed event and not let it propagate further, similar to how mouse events are handled (by setting Rhino.UI.MouseCallbackEventArgs.Cancel = true)

private BackgroundWorker _bg;
protected Result RunCommand(RhinoDoc doc, RunMode mode)
{
    int n = 10000;
    GetObject go = new GetObject();
    go.AcceptNothing(true);
    OptionInteger optN = new OptionInteger(n);
    go.AddOptionInteger("Cycles", ref optN);

    RhinoApp.EscapeKeyPressed += OnEscPressed;
                    
    while (true)
    {
        _bg = new BackgroundWorker();
        _bg.WorkerSupportsCancellation = true;
        _bg.WorkerReportsProgress = false;
        _bg.DoWork += (o, a) =>
        {
            BackgroundWorker bgw = o as BackgroundWorker;
            DoWork(bgw, a, optN.CurrentValue);
        };

        RhinoApp.WriteLine("Running background task. Press Esc to cancel...");
        RhinoApp.WriteLine();
        _bg.RunWorkerAsync();
        while (_bg.IsBusy)
        {
            RhinoApp.Wait();
        }
        RhinoApp.WriteLine(" ... background task completed");
        RhinoApp.WriteLine();

        GetResult res = go.Get();
        if (res == GetResult.Cancel)
        {
            RhinoApp.WriteLine("Cancelling command");
            return Result.Cancel;
        }
        if (res == GetResult.Nothing)
            break;

    }

    RhinoApp.EscapeKeyPressed -= OnEscPressed;
                    

    RhinoApp.WriteLine("Command successfully completed");
    return Result.Success;
            
}

private void OnEscPressed(object sender, EventArgs args)
{
    if (null != _bg)
    {
        RhinoApp.WriteLine("Cancelling worker");
        _bg.CancelAsync();
    }
}

private void DoWork(BackgroundWorker bg, DoWorkEventArgs args, int n)
{
    // let's keep the processor busy.
    int r = 0;
    for (int i = 0; !bg.CancellationPending && i < n; ++i)
    {
        r = 0;
        for (int j = 0; !bg.CancellationPending && j < n; ++j)
        {
            r++;
        }
    }
    RhinoApp.WriteLine("End of DoWork. CancellationPending = {0}", bg.CancellationPending);
}

FWIW, I coded a key listener in the past that I now used to solve this problem. The listener can cancel the event and prevent it from progressing. It would be nice if RhinoApp.EscapeKeyPressed could also do this.

Hi Menno,

RhinoApp.EscapeKeyPressed hooks the keyboard using the WH_KEYBOARD hook. This hook enables you to monitory WM_KEYDOWN and WM_KEYUP messages about to be returned by the GetMessage or PeekMessage (e.g. Rhino’s message queue). This why your command is being cancelled.

The advantage of using the WH_KEYBOARD_LL hook, as you’ve experienced, is that you are notified before the message is posted. There are some ramifications when using this, such as whether or not to call the next hook proc.

Since RhinoApp.EscapeKeyPressed is unique to RhinoCommon, we might be able to make a change, but probably not until V6. Let me discuss this is @stevebaer

I’m open to changing this, but it probably should only be done in V6 since it is a significant change.

Hi @dale , @stevebaer , do you recall if any change like this to consume the key was ever implemented? Thanks, Larry

Hi @theleibmans,

Sorry, I’m not following. What are you looking for?

Thanks,

– Dale

If you are looking for performing a task that can be cancelled with the escape key, have a look at the GetCancel class in RhinoCommon

Thanks @stevebaer , @dale ,

I think GetCancel could do the trick.

I am calling the HiddenLineDrawing.Compute overload that takes a cancellation token. Currently I start a thread on which to run the calculation and while waiting for the thread to complete listen to see if the user pressed the ESC key. If they do I call Cancel the passed in cancellation token and then return to the caller. What was happening is that even though I handled the ESC key being pressed, it looks like it was not consumed by the Rhino input stack, so the next time GetObject.GetMultiple was called it immediately returned GetResult.Cancel. So I was looking for an easy way to “consume” the ESC key on the input stack.

The GetCancel approach is much simpler and replaces some of the logic I had implemented; thanks for the tip!

2 Likes

Dear @stevebaer , @dale ,

I’m still running into the issue where the user ESC key seems not to be processed, perhaps I am not using GetCancel quite right. Below is a code snippet where I’m call the HiddenLineDrawing.Compute method which can take a while. If I press ESC the call to GetCancel.Wait returns with a result of Result.Cancel as expected. Then I call GetString and it returns immediately with a Result.Cancel. Is there another step required so that will not happen?

        // Use a GetCancel object to watch for <ESC> from the user to cancel the task
        GetCancel getCancel = new GetCancel();
        RG.HiddenLineDrawing hld = null;
        System.Threading.Tasks.Task task = System.Threading.Tasks.Task.Factory.StartNew(() => 
        {
            hld = RG.HiddenLineDrawing.Compute(hldParams, true, null, getCancel.Token);
        });

        var result = getCancel.Wait(task, rhinoDoc); 
        if (result == Rhino.Commands.Result.Cancel)
        {
            // Use a GetString to consume the input <ESC>
            string outputString = string.Empty;
            var result2 = Rhino.Input.RhinoGet.GetString("<Enter> when finished.", true, ref outputString);
            return null;
        }

Hi @theleibmans,

Maybe something more like this?

{
  HiddenLineDrawing hld = null;
  HiddenLineDrawingParameters hld_params = new HiddenLineDrawingParameters();

  // TODO...

  var gc = new GetCancel();
  gc.SetCommandPrompt("Press Esc to cancel");
  gc.ProgressReporting = true;
  var task = Task.Run(() => HiddenLineDrawing.Compute(hld_params, true, gc.Progress, gc.Token), gc.Token);
  var get_rc = gc.Wait(task, doc);

  bool rc = false;
  if (get_rc == Result.Success)
  {
    hld = task.Result;
    rc = hld != null;
    if (rc == false)
    {
      RhinoApp.WriteLine("HiddenLineDrawing.Compute failed.");
      return Result.Failure;
    }
  }

  if (null == hld || hld.Segments.Count() < 1)
  {
    RhinoApp.WriteLine("No output geometry found..");
    return Result.Failure;
  }

  // TODO...

  return Result.Success;
}

– Dale