MPS 2024.1 Help

Progress indicators

Actions by default block the UI for the duration of their execution. For actions that do not finish instantly it is advisable to indicate to the user that the action is active and perhaps also show a progress bar to give the user some feedback on the progress and time estimates. In this section we'll see how to properly display and update progress indicators, how to allow the users to manually cancel actions as well as to send actions to the background.

Asynchronous tasks

You should aim at making all your actions quick to avoid freezing the UI. Long-lasting activities should be extracted from actions and placed into one or more asynchronously run tasks. Tasks extend the Task class from com.intellij.openapi.progress@java_stub and they can either display a modal dialog (Task.Modal class) or be sent to the background (Task.Backgroundable class). The task should be invoked on the EDT through the ApplicationManager.getApplication().invokeLater() method.

action ModalProgressAction {   mnemonic: <no mnemonic>   execute outside command: false   also available in: << ... >>   caption: BackgroundableProgressAction   description: <no description>   icon: <no icon>     construction parameters     << ... >>   action context parameters ( always visible = false )     Project project key: PROJECT required     MPSProject mpsProject key: MPS_PROJECT required   <update block>     execute(event)->void {   boolean canBeCanceled = true;   Task.Modal modalTask = new Task.Modal(ModalProgressAction.this.project, "Modal cancelable task", canBeCanceled) {     public void run(@NotNull() final ProgressIndicator indicator) {      doWorkChunk1(); if (checkForCancellation()) return; doWorkChunk2();   if (checkForCancellation()) return; ...     }      @Override      public void onCancel() {        super.onCancel();      }    };    ApplicationManager.getApplication().invokeLater({ => ProgressManager.getInstance().run(modalTask); });  } }

The task typically provides a run() method to perform the task and an onCancel() method to handle cancellation invoked by the user. The actual cancellation logic must be implemented by the task implementer - work in run() should be organised into chunks with checks for pending cancellation in between them. The onCancel() method will get fired only after run() finishes through cancellation and so serve mostly for cleaning up the partial work done by the task.

Non-cancelable modal tasks (the canBeCancelled parameter set to false) will force the user to wait until the action finishes completely and so should be used with care, perhaps for critical actions only, the cancellation of which would be difficult to handle properly.

Monitoring progress

The task's run() method obtains an instance of IntelliJ's progress indicator as a parameter. It is advisable to wrap it in ProgressMonitorAdapter coming from jetbrains.mps.progress@java_stubProgressMonitorAdapter represents the visual progress dialog and provides methods to set the actual progress bar value as well as and the labels shown to the user. It also holds the information regarding cancellation.

Task.Modal modalTask = new Task.Modal(ModalProgressAction.this.project, "Modal cancelable task", canBeCanceled) {  public void run(@NotNull() final ProgressIndicator indicator) {    final ProgressMonitorAdapter adapter = new ProgressMonitorAdapter(indicator);        adapter.start("Progress in progress...", 8);    int stepValue = 1;      adapter.step("Do work 1 ...");    do work chunk 1   adapter.advance(stepValue);    if (adapter.isCanceled()) { return; }        adapter.step("Do work 2 ...");    do work chunk 2   adapter.advance(stepValue);    if (adapter.isCanceled()) { return; }      ...           adapter.done();  }

A few notes:

  • The constructor of the Task includes a text that will be used as the title of the progress dialog

  • The start() method provides a text to show above the progress bar and a number of steps/points to complete the task

  • The step() method changes the text label displayed below the progress bar in the progress dialog

  • The advance() method moves the progress bar to a new value by the specified number of steps/points

  • Make the progress steps as small as possible to improve the user experience. Smaller steps provide smoother experience to the user

Running in the background

The Task.Backgroundable class should be used for tasks that can be processed in the background.

action BackgroundableProgressAction {   mnemonic: <no mnemonic>   execute outside command: false   also available in: << ... >>   caption: BackgroundableProgressAction   description: <no description>   icon: <no icon>     construction parameters     << ... >>   action context parameters ( always visible = false )     Project project key: PROJECT required     MPSProject mpsProject key: MPS_PROJECT required   <update block>     execute(event)->void {   boolean canBeCanceled = true;   PerformInBackgroundOption showProgress = PerformInBackgroundOption.DEAF;   Task.Backgroundable backgroundable = new Task.Backgroundable(BackgroundableProgressAction.this.project, "Backgroundable cancelable task", canBeCanceled, showProgress) {     public void run(@NotNull() final ProgressIndicator indicator) {  ...     }      @Override      public void onCancel() {        super.onCancel();      }    };    ApplicationManager.getApplication().invokeLater({ => ProgressManager.getInstance().run(backgroundable); });  }   additional methods   private void doWork() {      ...   } }

A few notes:

  • The backgroundable tasks may or may not allow cancellation

  • The PerformInBackgroundOption interface allows you to create tasks that start in the foreground as well as in the background

  • The user can move backgroundable tasks to the foreground as well as to the background

  • The predefined constants for the PerformInBackgroundOption interface are DEAF (start in the foreground) and ALWAYS_BACKGROUND (start in the background, useful for non-critical actions that the user does not need to pay attention to, since no dialog would show up distracting the user, the UI remains fully usable all the time_)._

Proper locking when accessing resources

It is ok to obtain read and write locks to the MPS repository inside tasks as well as executing commands:

// ReadAction in step is ok for display state ModalProgressAction.this.mpsProject.getRepository().getModelAccess().runReadAction(new Runnable() {    public void run() {      adapter.step("Do some work with Read Lock...");      ModalProgressAction.this.doWork();    }  }); adapter.advance(stepValue);  if (adapter.isCanceled()) { return; }    // WriteAction in step is ok for display state  ModalProgressAction.this.mpsProject.getRepository().getModelAccess().runWriteAction(new Runnable() {    public void run() {      adapter.step("Do some work with Write Lock...");      ModalProgressAction.this.doWork();    }  });  adapter.advance(stepValue);  if (adapter.isCanceled()) { return; }    // Command in step is ok for display state  ModalProgressAction.this.mpsProject.getRepository().getModelAccess().executeCommand(new Runnable() {    public void run() {      adapter.step("Do some work in command...");      ModalProgressAction.this.doWork();    }  });  adapter.advance(stepValue);  if (adapter.isCanceled()) { return; }

A few notes:

  • When you need locking inside an action, prefer grouping of all modification into a single locking block

  • Release the locks as soon as you do not need them to avoid blocking other potential user actions

  • Do not use R/W actions or Commands in the EDT thread - this would lead to unpredictable updates of the progress and may even cause the UI to freeze

Undoable actions

Changes to the models require undoable actions, which can be executed through the executeCommandInEDT() method. However, you must not call the ProgressIndicator's methods from within the command, since it itself is running in an EDT and all requests for the progress bar changes would be delayed until the command finishes. The recommended approach is to instruct the progress bar from the main action's thread before invoking the command with executeCommandInEDT() and then block the main action's thread until the command finishes, perhaps with a CyclicBarrier or other synchronisation primitive:

adapter.step("Do some work in command ...");  final CyclicBarrier barrier = new CyclicBarrier(2);    ModalProgressAction.this.mpsProject.getRepository().getModelAccess().executeCommandInEDT(new Runnable() {    public void run() {     try {         model m = model/myModel/;        m.new root node(MyRootConceptDeclaration);      } finally {        try {          barrier.await();        } catch (BrokenBarrierException e) {          <no statements>        } catch (InterruptedException e) {          <no statements>        }      }    }  });  try {    barrier.await();  } catch (InterruptedException e) {    <no statements>  } catch (BrokenBarrierException e) {    <no statements>  }  adapter.advance(stepValue);  if (adapter.isCanceled()) { return; }

The Additional methods section can be leveraged to extract the locking implementation code out of the action's body.

private void block(CyclicBarrier barrier) {    try {      barrier.await();    } catch (BrokenBarrierException e) {      <no statements>    } catch (InterruptedException e) {      <no statements>    }  }
Last modified: 11 February 2024