Friday, December 13, 2013

Split Editors: Anatomy of a new Feature


Finally, we're able to start moving the Eclipse IDE beyond what 3.x could do, showing some real pay-off for all the pain (for you and us). First up is the infamous Bug 8009, 11 years old and highly desired but never able to be implemented in the 3.x code base.

Lars has (again) beat me to the punch as far as the announcement goes so I'll take a different track; how did we go from the idea to the finished feature ?

Step 1: Design and Mockups


We started by making a variety of informal stabs at hacking the functionality, arriving in the end at the conclusion that in order to proceed we needed to allow a mechanism for  an MPart to contain a sub-structure (sashes and other parts...). This part is all about testing the scope of the feature and learning what we really need.

This eventually lead to the extension of the model with a new element MCompositePart, giving us a formal definition of a part that can contain other parts.

Step 2: Functional Prototype


Once the MCompositePart made it into the model it was time to hack up a prototype of the feature, making sure that it performs all the operations necessary to expose the new functionality. My favorite technique for this type of work (especially when it's eventually aimed at the IDE rather than pure e4) is to just make a new project based off the 'Hello World, Command' template. Then I work on the command's handler code until I have a 'command' that can do what I want. In this case I ended up with the following (Kodos to the Orion team; my code snippets use an Embedded Orion Editor):

public Object execute(ExecutionEvent event) throws ExecutionException {
 window = HandlerUtil.getActiveWorkbenchWindowChecked(event);
 MApplication theApp = (MApplication) window.getService(MApplication.class);
 MTrimmedWindow win1 = (MTrimmedWindow) theApp.getChildren().get(0);

 // Get services
 ms =  win1.getContext().get(EModelService.class);
 ps = win1.getContext().get(EPartService.class);
 pe = win1.getContext().get(IPresentationEngine.class);
 
 MPart part = ps.getActivePart();
 if (part == null)
  return null;
  
 MPartStack stack = getStackFor(part);
 if (stack == null)
  return null;
  
 MStackElement stackSelElement = stack.getSelectedElement();
 if (stackSelElement instanceof MCompositePart) {
  unsplitPart((MCompositePart) stackSelElement);
 } else {
  horizontal = !horizontal ;
  splitPart(stackSelElement);
 }
  
 return null;
}

This gives me a command that will act as a toggle and change orientation each time you split (to ensure that we can do both) and accomplishes two objectives:
  1. Allows me to fully implement the desired functionality, creating methods to be used in the implementation of the handler.
  2. Test / refine the rendering capabilities to allow for the new requirements. In this case very few were needed as you can see by looking at the commits on Bug 378298. A lot of this code is only necessary because we have to support the new MCompositePart itself and it not directly related to the split editor feature.
BTW, not to worry about the implementations of the 'split' and 'unsplit' methods used in the code above; we'll be getting back to that code later :-).

Now we have a working implementation of the feature but it's not correctly packaged since all the code is in the handler class (and that handler uses the IDE's mechanisms but we will want this functionality in e4 as well)...what to do ? what to do ?

Step 3: Packaging the code for re-use


OK, the first thing to consider is what this feature would look like in pure e4. The pattern used by the MinMaxAddon is also applicable to this scenario so let's go with it:
  1. Define a new SplitPartAddon that reacts to two new IPresentationEngine constants "SPLIT_HORIZONTAL" and "SPLIT_VERTICAL".
  2. Move the methods required to do this out of the original handler into the new addon, calling them appropriately based on the new tags being added / removed from an existing MPart.
Here's SplitterAddon (note that the complete functional implementation is only 150 lines of code):

/**
 * Listens for the IPresentationEngine's SPLIT_HORIZONTAL and SPLIT_VERTICAL tags being applied to
 * an MPart and takes the appropriate steps to split / unsplit the part
 * 
 * @since 1.1
 */
public class SplitterAddon {
 @Inject
 EModelService ms;

 @Inject
 EPartService ps;

 /**
  * Handles changes in tags
  * 
  * @param event
  */
 @Inject
 @Optional
 private void subscribeTopicTagsChanged(
   @UIEventTopic(UIEvents.ApplicationElement.TOPIC_TAGS) Event event) {
  Object changedObj = event.getProperty(EventTags.ELEMENT);

  if (!(changedObj instanceof MPart))
   return;

  MPart part = (MPart) changedObj;

  if (UIEvents.isADD(event)) {
   if (UIEvents.contains(event, UIEvents.EventTags.NEW_VALUE,
     IPresentationEngine.SPLIT_HORIZONTAL)) {
    splitPart(part, true);
   } else if (UIEvents.contains(event, UIEvents.EventTags.NEW_VALUE,
     IPresentationEngine.SPLIT_VERTICAL)) {
    splitPart(part, false);
   }
  } else if (UIEvents.isREMOVE(event)) {
   MCompositePart compPart = SplitterAddon.findContainingCompositePart(part);
   if (UIEvents.contains(event, UIEvents.EventTags.OLD_VALUE,
     IPresentationEngine.SPLIT_HORIZONTAL)) {
    unsplitPart(compPart);
   } else if (UIEvents.contains(event, UIEvents.EventTags.OLD_VALUE,
     IPresentationEngine.SPLIT_VERTICAL)) {
    unsplitPart(compPart);
   }
  }
 }

 /**
  * Finds the CompositePart containing the given part (if any)
  * 
  * @param part
  * @return The MCompositePart or 'null' if none is found
  */
 public static MCompositePart findContainingCompositePart(MPart part) {
  if (part == null)
   return null;

  MUIElement curParent = part.getParent();
  while (curParent != null && !(curParent instanceof MCompositePart))
   curParent = curParent.getParent();

  return (MCompositePart) curParent;
 }

 private void unsplitPart(MCompositePart compositePart) {
  if (compositePart == null)
   return;

  List innerElements = ms.findElements(compositePart, null, MPart.class, null);
  if (innerElements.size() < 2)
   return;

  MPart originalEditor = innerElements.get(1); // '0' is the composite part

  MElementContainer compParent = compositePart.getParent();
  int index = compParent.getChildren().indexOf(compositePart);
  compParent.getChildren().remove(compositePart);
  originalEditor.getParent().getChildren().remove(originalEditor);
  compParent.getChildren().add(index, originalEditor);

  if (ps.getActivePart() == originalEditor)
   ps.activate(null);
  ps.activate(originalEditor);
 }

 private MCompositePart createCompositePart(MPart originalPart) {
  MCompositePart compPart = ms.createModelElement(MCompositePart.class);
  compPart.setElementId("Split Host(" + originalPart.getLabel() + ")"); //$NON-NLS-1$ //$NON-NLS-2$
  compPart.setLabel(originalPart.getLabel());
  compPart.setTooltip(originalPart.getTooltip());
  compPart.setIconURI(originalPart.getIconURI());
  compPart.setCloseable(true);
  compPart.setContributionURI("bundleclass://org.eclipse.e4.ui.workbench.addons.swt/org.eclipse.e4.ui.workbench.addons.splitteraddon.SplitHost"); //$NON-NLS-1$

  // Always remove the composite part from the model
  compPart.getTags().add(EPartService.REMOVE_ON_HIDE_TAG);

  return compPart;
 }

 void splitPart(MPart partToSplit, boolean horizontal) {
  MElementContainer parent = partToSplit.getParent();
  int index = parent.getChildren().indexOf(partToSplit);

  MPart editorClone = (MPart) ms.cloneElement(partToSplit, null);

  MCompositePart compPart = createCompositePart(partToSplit);

  // Add the new composite part to the model
  compPart.getChildren().add(editorClone);
  compPart.setSelectedElement(editorClone);
  parent.getChildren().add(index, compPart);
  parent.setSelectedElement(compPart);

  // Now, add the original part into the composite
  int orientation = horizontal ? EModelService.ABOVE : EModelService.LEFT_OF;
  ms.insert(partToSplit, editorClone, orientation, 0.5f);

  ps.activate(partToSplit);
 }
}

Now we can take our original IDE command handler and replace the code that used to call the methods directly with code that adds or removes the appropriate tags on the MPart for the active editor. Here's the final code for the IDE handler:

public class SplitHandler extends AbstractHandler {
 private EModelService modelService;
 private IWorkbenchWindow window;

 /**
  * The constructor.
  */
 public SplitHandler() {
 }

 public Object execute(ExecutionEvent event) throws ExecutionException {
  // Only works for the active editor
  IEditorPart activeEditor = HandlerUtil.getActiveEditor(event);
  if (activeEditor == null)
   return null;
  
  MPart editorPart = (MPart) activeEditor.getSite().getService(MPart.class);
  if (editorPart == null)
   return null;
  
  window = HandlerUtil.getActiveWorkbenchWindowChecked(event);

  // Get services
  modelService =  editorPart.getContext().get(EModelService.class);
  
  MPartStack stack = getStackFor(editorPart);
  if (stack == null)
   return null;

  window.getShell().setRedraw(false);
  try {
   MStackElement stackSelElement = stack.getSelectedElement();
   if (stackSelElement instanceof MCompositePart) {
    List innerElements = modelService.findElements(stackSelElement, null, MPart.class, null);
    MPart originalEditor = innerElements.get(1); // '0' is the composite part
    
    originalEditor.getTags().remove(IPresentationEngine.SPLIT_HORIZONTAL);
    originalEditor.getTags().remove(IPresentationEngine.SPLIT_VERTICAL);
   } else {
    if ("false".equals(event.getParameter("Splitter.isHorizontal"))) { //$NON-NLS-1$ //$NON-NLS-2$
     editorPart.getTags().add(IPresentationEngine.SPLIT_VERTICAL);
    } else {
     editorPart.getTags().add(IPresentationEngine.SPLIT_HORIZONTAL);
    }
   }
  } finally {
   window.getShell().setRedraw(true);
  }
  
  return null;
 }
 
 private MPartStack getStackFor(MPart part) {
  MUIElement presentationElement = part.getCurSharedRef() == null ? part : part.getCurSharedRef();
  MUIElement parent = presentationElement.getParent();
  while (parent != null && !(parent instanceof MPartStack))
   parent = parent.getParent();
  
  return (MPartStack) parent;
 }
}

We also add a couple of key bindings for easy access...in this case Ctrl + '_' gives a horizontal split (one above the other) and Ctrl + '{' gives a side-by-side split.

Step 4: Release and Polish

 

Finally, we blog about it so that folks will know that it's available...;-).

Then we take the feedback and polish the feature (adding CSS to define the splitter's color...). The earlier we can release a new feature the more time we'll have to get the feedback and act on it before the actual release. If there's one thing as certain as death and taxes is that you won't  get it right the first time. I've learned that it's more productive to 'get it working, get it out',  then polish it rather than putting a ton of work into the polish up front just to trash it later based on feedback.

P.S. While the current implementation is specific to the Eclipse IDE and editors in particular we do expect to extend this feature to allow the proper splitting of any MPart. This will, however, require some more design work on allowing the MCompositePart to somehow 'host' the Toolbar and Menus from the currently active 'child' MPart so they appear in the stack and work correctly.

Alrighty then, on to the next one...different presentations based on how many monitors you have.


Sunday, April 7, 2013

Another Great EclipseCon

Well it's already been a week since I got back from Boston and I'd better get this out before the memories fade into the background. First off it was really nice for me to finally be able to meet Lars, Wim and \Sopot who've been so great at extending both e4's capabilities and exposure.After our excellent conversation on Sunday I'm sure that there will be more shared beers to come...;-). Unfortunately (for me) Brian De Alwis couldn't be there due to his wife being...well...due (congrats Brian !).

OK, on to the conference...this year I had more time to attend talks because I was only giving one myself. For those that have been asking the slides are here. All the ones I attended were interesting and quite informative. Wim and Lars'"Shake that FUD" talk on mixing e4 and IDE was great, demonstrating the advantages of e4's architecture and ability but presented in a hilarious Laurel and Hardy style. There was also an excellent talk on the real-world issues encountered while porting a large application to e4. This was a great indication that there is true enterprise adoption of Eclipse 4.

Tom Schindl's talk "Modern UIs with JavaFX" showing off his JavaFX work with cool animations was made even more interesting by his demonstration showing the adoption of parts of the Eclipse JDK, allowing his simple JavaFX IDE to provide code assisst...  This was a great example of the direction we want to go; moving code away from being tightly bound to the IDE in order to allow it to be re-used much more freely.

For me though the absolute tops was the talk from l33tlabs , "Bling: The GPU powered Game IDE" demo'd a re-write of SWT using OpenGL. These guys come from a gaming background so everything about the presentation was unique (like their business card). Slides ? Who needs 'em ? Their presentation was a room around which a camera roamed, zooming in on each 'slide' (a note on a wall...). To them using the static (dare I say 'uncool') Eclipise UI just wouldn't cut it artistically so they decided to do something about it...;-). The results were stunning, seeing stacks in a running Eclipse IDE jiggle, fade, zoom as 'simple' effects has given me hope that we can make the Eclipse UI palatable to today's tablet using crowd. Early days yet but stay tuned...

Finally, away from the presentation rooms I've got to give a shout out to Marcel Bruch from CodeTrails, the folks who've given us Code Recommders. They had a really fun and effective way to demonstrate what they're all about. The idea was a 'Family Feud' game where you'd have to come up with the most common uses for a new object of some type (such as the SaveFileDialog, System...). I finally understand what they are all about ( think), it's a case of 'big data' meets code assist. Marcel and I had a great talk after my (winning!) run at the game (sort of unfair since I've written a lot of code using 'vi'...;-). We're trying to see what can be done to provide some form of code assist for injection annotations; something that would really help mitigate a real issue for folks wanting to adopt DI (as everyone should...;-).

OK, back to getting Kepler together. It's always a pleasure for me to attend EclipseCon and see the energy in the community. Hope to see you all again soon.

Stay tuned !! I'll be blogging here much more regularly with examples on using the newly available API from within the Eclipse IDE.