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?
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.