Search code examples
eclipsescalaeclipse-rcpe4

How to populate undo/redo in an Eclipse3/Eclipse4 hybrid editor (DIEditorPart)?


My application is hybrid E3/E4 because I want to use parts of the Eclipse Workbench, but also make my stuff (somewhat) ready for later pure E4 based applications.

In this application I have an editor which uses a custom editor input (not file-based, but based on data pulled from a database; when the user saves the changes, they are written back to the database). My local in-memory representation of the data is managed by EMF / Xcore and the editor uses a manually-designed GUI that works via EMF Edit Databinding, i.e., I am using an EditingDomain (that is, a basic AdapterFactoryEditingDomain with a BasicCommandStack) to track all the changes.

To plug my E4 editor into an E3 editor, I use the compatibility layer, and here especially the DIEditorPart.

While this works fine so far, I have not yet been able to get undo/redo to work.

My code looks like this (I am using Scala with Scala-IDE):

The E3 editor bridge:

final class CustomEditorPartE3Bridge extends DIEditorPart(classOf[CustomEditorPart])

And the "real" part:

final class CustomEditorPart {

  @Inject private var _ctx: IEclipseContext = _
  private var _view: Option[MyCustomEditorView] = None
  @Inject private var _dirty: MDirtyable = _
  @Inject @Optional private var _dirtyE3: IDirtyProviderService = _
  private var doPersist: () => Unit = () => {}

  @PostConstruct
  def init(input: IEditorInput): Unit = input match {
    case i: MyCustomEditorInput => initPart(i)
    case _ =>
      throw new IllegalStateException("Required a %s but got a %s".
        format(classOf[MyCustomEditorInput].getName, input.getClass.getName))
  }

  private def initPart(input: MyCustomEditorInput): Unit = {
    val cc = _ctx.createChild()
    // Now we need an adapter factory and a respective editing domain
    // to enable Undo and Redo
    val adapterFactory = new ModelAdapterFactory // generated with Xcore
    val cs = new BasicCommandStack
    val domain = new AdapterFactoryEditingDomain(adapterFactory, cs)
    // We need the editing domain in the control for Databinding
    cc.set(classOf[EditingDomain], domain)
    // Now we setup the view
    _view = Some(ContextInjectionFactory.make(classOf[MyCustomEditorView], cc))
    // And we handle dirtying of our part
    object csl extends CommandStackListener {
      def commandStackChanged(eo: EventObject): Unit = {
        val dirty = cs.isSaveNeeded()
        if (_dirtyE3 == null) {
          _dirty.setDirty(dirty)
        } else {
          Display.getDefault.asyncExec(() => _dirtyE3.setDirtyState(dirty))
        }
      }
    }
   cs.addCommandStackListener(csl)
    // Finally, we setup our saving routine.
    doPersist = () => { /* not relevant here */ }
  }

  @Focus
  def setFocus(): Unit = _view.foreach(_.setFocus)

  @PersistState
  def persistState(): Unit = {}

  @Persist
  def commit(): Unit = doPersist()
}

So, how do I contribute undo/redo so that E3's undo/redo mechanism kicks in? Do I somehow have to propagate my EditingDomain back to my E3 bridge and set up some action bar contributor or can I inject something that sets up the undo/redo for me?


Solution

  • I am posting this as an answer so I can mark it as the solution.

    After fiddling around and searching various examples of EHandlerService usages I came up with this:

    When I am using DIEditorPart I can inject an IWorkbenchPart. Since this might not exist if the part is used in a pure E4 based application later, I inject it as @Optional and test for null later.

    So in addition to the E4 way, I have the following:

      @Inject @Optional private var _workbenchPart: IWorkbenchPart = _
    
      // In my method
      val cc: IEclipseContext = ...
      val domain: EditingDomain = ...
      cc.set(classOf[EditingDomain], domain)
        if (_workbenchPart != null) {
          val undoAction = ContextInjectionFactory.make(classOf[UndoAction], cc)
          val redoAction = ContextInjectionFactory.make(classOf[RedoAction], cc)
          val site = _workbenchPart.getSite.asInstanceOf[{
            def getActionBars(): org.eclipse.ui.IActionBars
          }]
          val targetActionBars = site.getActionBars
          if (targetActionBars != null) {
            targetActionBars.setGlobalActionHandler(ActionFactory.UNDO.getId, undoAction)
            targetActionBars.setGlobalActionHandler(ActionFactory.REDO.getId, redoAction)
          }
        }
    

    My UndoAction and RedoAction look like this:

    abstract class UndoRedoAction(canExecute: CommandStack => Boolean,
                                  execute: CommandStack => Unit) extends Action {
    
      @Inject private var _domain: EditingDomain = _
    
      @PostConstruct
      private def init(): Unit = {
        setEnabled(canExecute(_domain.getCommandStack))
        object csl extends CommandStackListener {
          def commandStackChanged(eo: EventObject): Unit = setEnabled(canExecute(_domain.getCommandStack))
        }
        _domain.getCommandStack.addCommandStackListener(csl)
      }
    
      override final def run(): Unit = execute(_domain.getCommandStack)
    }
    
    final class UndoAction extends UndoRedoAction(_.canUndo, _.undo)
    final class RedoAction extends UndoRedoAction(_.canRedo, _.redo)
    

    This is a bit of a hack, but it works.