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.
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.
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.
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_stub. ProgressMonitorAdapter 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.
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.
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:
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
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:
The Additional methods section can be leveraged to extract the locking implementation code out of the action's body.