Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Eclipse RCP IPageLayout woes

I have an Eclipse RCP application with a sort of three column layout:

enter image description here

The editor area is at the extreme right. Now, when you get an IPageLayout to work with, the editor area is already added in. That's fine: we add area B to the left of the editor, and area A to the left of B, and the layout is exactly what we need.

The issue is that when you move the sash between A and B, views A and B change without resizing the editor area (good;) but when you move the other sash between B and the editor area, all three views are resized; the layout manager acts to maintain the ratio of the widths of A and B, and that's not what we want. We want the user to be able to move each sash independently, and have it influence only the two views it touches.

It seems like the root cause of this is that the editor is in place when you get your IPageView, and therefore you have to position the IFolderLayouts relative to it. If you could position the editor relative to B, instead, then resize would do the right thing.

So my questions:

  1. Is there any way to tell the IPageView to position the editor relative to a view, instead of the other way around?
  2. Barring that, is there any other way to influence the layout algorithm, like writing some kind of layout manager?
like image 499
Ernest Friedman-Hill Avatar asked Aug 09 '12 20:08

Ernest Friedman-Hill


2 Answers

I don't think, it's possible to achieve exactly what you want (so the answers to your questions would be 1. no, 2. no). But there it a 3rd alternative, which IMO behaves quite nicely.

When trying in Eclipse: Start with viewA on left and Editor on right. Then when you drag viewB to the right side of viewA, you get the (wrong) setup you describe. But then you drag it to the left part of the Editor, then you get different configuration, where dragging right sash behaves as you want. Dragging of left sash resizes viewA and Editor and MOVES viewB.

I would say that the code to achieve this would be:

IFolderLayout areaA = layout.createFolder("A", IPageLayout.LEFT, 0.33f, editorArea);
IFolderLayout areaB = layout.createFolder("B", IPageLayout.LEFT, 0.5f, editorArea);
like image 25
Michal Avatar answered Nov 03 '22 15:11

Michal


I know of no way to alter the layout tree of IPageLayout in Eclipse 3.x. In Eclipse 4.2, however, the Application Model can be changed dynamically at runtime.

So, if you would consider migrating your application to Eclipse 4, this solution could be an option. To keep the original application and UI code as untouched as possible, this solution will

  • take full advantage of the compatibility layer of Eclipse 4 to create an Application Model from the Eclipse 3 based RCP application. There is no need to create an Application Model or alter the UI code of the application.
  • rearrange the editor area's layout after the application is active. This is done by creating an addon class in a separate plugin.
  • allow easy migration to more Eclipse 4 functionality in the future: Should you decide to build an own Application Model, you can just unhook the addon plugin.

I started with the regular RCP Mail template of Eclipse 3 and altered the perspective to recreate the problem. This is the Perspective class I used in my test application:

import org.eclipse.ui.IPageLayout;
import org.eclipse.ui.IPerspectiveFactory;

public class Perspective implements IPerspectiveFactory {

  public static final String ID = "wag.perspective";

  public void createInitialLayout(IPageLayout layout) {
    String editorArea = layout.getEditorArea();
    layout.setEditorAreaVisible(true);

    layout.addStandaloneView(AView.ID, false, IPageLayout.LEFT,
                             0.25f, editorArea);
    layout.addStandaloneView(BView.ID, false, IPageLayout.LEFT,
                             0.25f, editorArea);

    layout.getViewLayout(AView.ID).setCloseable(false);
    layout.getViewLayout(BView.ID).setCloseable(false);
  }
}

It basically creates the scenario you described: a three column layout where one sash effects all three parts and the other one only two.

I then proceeded to migrate the application and alter the Application Model.

Migrate the Eclipse 3 based RCP application to Eclipse 4

There are online tutorials available for this process. I found Eclipse 4.1: Run your 3.x RCP in 4.1 and Eclipse 4 and the Compatibility Layer - Tutorial to be very helpful.

I recommend including the org.eclipse.e4.tools.emf.liveeditor and its required plug-ins in your product dependencies. With the live editor, you can take a look at the Application Model that is created by the compatibility layer.

Once the application starts, thet sashes will still behave the same way. Open the live editor on your application window and take a look at your model.

layout tree created by the compatibility layer

You can see that the PartSashContainer including the placeholder for the AView contains another PartSashContainer. Moving the sash between AView and that container will update the rest of the layout tree, while moving the sash between BView and the editor does not effect other parts of the layout.

You could now drag the placeholder for the AView to the container where the BView and the editor are located. This would instantly create the effect you desire: The sashes will only affect their direct neighbours. But these changes will only be saved in one's own runtime workspace. Something else is needed to alter the layout structure automatically.

Altering the Application Model at runtime

Since I didn't want to touch the original code if possible, I created another plugin to make a contribution to the Application Model.

Create a Plug-In Project without an Activator without using a template.

Add an Addon class: select New->Other->Eclipse 4->Classes->New Addon Class

Add a Model Fragment: select New->Other-Eclipse 4->Model->New Model Fragment. Open the created fragment.e4xmi file and add a Model Fragment. For the Element Id, put org.eclipse.e4.legacy.ide.application (this is the standard id of legacy applications) and for the Featurename addons. Add an Addon to the Model Fragment. Enter an ID and set the Class URI to your addon class.

Now add your fragment.e4xmi to your org.eclipse.e4.workbench.model extension point:

<extension
  id="id1"
  point="org.eclipse.e4.workbench.model">
  <fragment
    uri="fragment.e4xmi">
  </fragment>
</extension>

Add your contribution plugin to the dependencies of your application product. When you start your application and look at the model with the live editor, you should see your Addon listed in the model.

Now we can implement the Addon. This is the code of my Addon class:

package wag.contribution.addons;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;

import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.e4.ui.model.application.MApplication;
import org.eclipse.e4.ui.model.application.ui.MElementContainer;
import org.eclipse.e4.ui.model.application.ui.MUIElement;
import org.eclipse.e4.ui.model.application.ui.advanced.MPlaceholder;
import org.eclipse.e4.ui.workbench.modeling.EModelService;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;

public class LayoutSorter {

  @Inject private IEventBroker broker;

  private EventHandler handler;

  // The part IDs we are interested in, sorted in the sequence they should be
  // shown
  private static List<String> PART_IDS = Arrays.asList(new String[] {
              "wag.aView", "wag.bView", "org.eclipse.ui.editorss" });

  // Listen to the e4 core service's event broker to find the magical time
  // when the application is created and try to sort the layout.
  @PostConstruct
  void hookListeners(final MApplication application,
                     final EModelService service) {

    if (handler == null) {
      handler = new EventHandler() {
        // Try to sort the layout. Unsubscribe from event broker if
        // successful.
        @Override
        public void handleEvent(Event event) {
          try {
            sort(application, service);
            // sort did finish: stop listening to the broker.
            broker.unsubscribe(handler);
          } catch (Exception e) {
            // Something went wrong, the application model was not ready yet.
            // Keep on listening.
          }
        }
      };

      // Subscribe "ServiceEvent.MODIFIED" to grab the application.STARTED
      // event. Does anybody know how to do this in a better way?
      broker.subscribe("org/osgi/framework/ServiceEvent/MODIFIED",
                       handler);
    }
  }

  private void sort(MApplication application, EModelService service) {

    // find all placeholders
    List<MPlaceholder> placeholders = service.findElements(application,
              null, MPlaceholder.class, null);

    // only keep the ones we are interested in
    for (int i = placeholders.size() - 1; i > -1; i--) {
      if (!PART_IDS.contains(placeholders.get(i).getElementId())) {
        placeholders.remove(i);
      }
    }

    // find the parents of the placeholders
    List<MElementContainer<MUIElement>> parents = new ArrayList<>(
             placeholders.size());
    for (MPlaceholder placeholder : placeholders) {
      parents.add(placeholder.getParent());
    }

    // find the parent that is "deepest down" in the tree
    MElementContainer<MUIElement> targetParent = null;
    for (MElementContainer<MUIElement> parent : parents) {
      for (MUIElement child : parent.getChildren()) {
        if (parents.contains(child)) {
          continue;
        }
        targetParent = parent;
      }
    }

    // move all parts to the target parent
    if (targetParent != null) {
      for (int i = 0; i < placeholders.size(); i++) {
        if (targetParent != placeholders.get(i).getParent()) {
          service.move(placeholders.get(i), targetParent, i);
        }
      }
    }
  }

  @PreDestroy
  void unhookListeners() {
    if (handler != null) {
      // in case it wasn't unhooked earlier
      broker.unsubscribe(handler);
    }
  }
}

(Please note that the code above is a bit of a hack because it is only really suited for this specific problem.)

After a restart, the application should now behave in the desired way. Take a look at the Application Model to see your changes.

One thing to be aware of is that local changes are saved in the runtime workspace in the file .metadata\.plugins\org.eclipse.e4.workbench\workbench.xmi if saving is switched on, so for recreating the unaltered model for testing this file has to be deleted.

like image 128
Modus Tollens Avatar answered Nov 03 '22 16:11

Modus Tollens