Tuesday, February 23, 2010

Ghost Image Feedback - Enhancing Support for Viewports in GEF

As I had quite recently initiated a discussion about enriching the Eclipse Graphical Editing Framework (GEF) with some "advanced" editing features, I thought this to be a good opportunity to give a concrete example of what I had in mind by posting about a feature that could improve the end-user experience when working with nested viewports.

So what is the problem when working with viewports? To put it bluntly I think their biggest advantage most certainly is also their biggest disadvantage. That is, the possibility to reduce a container figure's external bounds (making it thus easier to embed it within a larger graphical context) while preserving the ability to access its internal contents in-place (even if not completely at a time) on the other hand yields the problem that navigating and editing this internal contents is often quite painful, as a lot of scrolling is usually necessary to access hidden parts, and orientation is easily lost.

To emphasize this, consider the following two versions of the same (slightly modified) 4-bit adder example, provided with GEF's sample logic editor. Despite having resized the circuit figure, both diagrams are indeed identical. And having not seen the first version before, the accessibility of the second one would most certainly be quite limited.


BTW: You might notice that by using the original GEF logic example editor you may not produce these screenshots, because connections are not clipped correctly, and because the scroll bars within the circuit figure are not transparent but opaque. The first problem can be addressed by a patch I contributed to bug #195527 (fix for clipping problems of connections in combination with viewports). The second one is something I will document herein, so keep on reading...

While those troubles mentioned before with respect to navigating and editing the contents of a viewport seem to be somehow the price to pay, the fact that its contents may never be visualized completely at a time, is something that can be circumvented by adequate feedback support. What I am referring to is a feature we implemented as part of a reasearch project during my time at the university.

The idea was born when Philip Ritzkopf (who was a student assistant in our research group at that time and has very much contributed to the conception and implementation of the feature) pointed me to a post in the GEF newsgroup about a "ghost image" figure (i.e. a semi-transparent copy of some original figure) that was discussed as an option to show feedback during a drag & drop operation. As we thought that the sketched mechanism could also be suited to show the hidden contents of a viewport, Philip and I directly started to implement a first prototype. Having made some further iterations, the following screenshot shows the final result.


Note that in case the circuit figure is not selected, everything will look like before. However, in case it is selected you get the 'Ghost Image Feedback'. What a difference!

So how is it realized? Indeed the approach is rather lightweight. It basically consists of a respective SelectionEditPolicy (namely ScrollableSelectionFeedbackEditPolicy), some interfaces (IScrollableFigure, IScrollableEditPart) to assure it can only registered to host edit parts, having a figure with a nested Viewport (or a ScrollPane containing a nested Viewport to be more precise), and an implementation of a GhostImageFigure, being used to render the feedback.

The GhostImageFigure being used is pretty much inspired by the one depicted in above mentioned use newsgroup entry. In difference to the version being posted, our's does not extend ImageFigure but constructs the original figure's image lazily within its paintFigure() method. The reason to do so is that this way, disposal of the used image can be performed directly after having painting it, so that no client has to take care (while this could be an option in case any performance problems might arise). It was furthermore enhanced by a possibility to specify a transparency color, which is needed in case the original figure is a connection (whose background should indeed be regarded as transparent). Here is the source code:

01 /**
02  *
03  *
04  * Copyright (C) 2005, 2008 Research Group Software Construction,
05  *                          RWTH Aachen University, Germany.
06  *
07  * All rights reserved. This program and the accompanying materials
08  * are made available under the terms of the Eclipse Public License
09  * version 1.0, which accompanies this distribution, and is available
10  * at http://www.eclipse.org/legal/epl-v10.html.
11  *
12  * Contributors:
13  *   Research Group Software Construction - Initial API and implementation
14  *
15  *
16  */
17 package org.eclipse.draw2d;
18 
19 import org.eclipse.swt.graphics.GC;
20 import org.eclipse.swt.graphics.Image;
21 import org.eclipse.swt.graphics.ImageData;
22 import org.eclipse.swt.graphics.RGB;
23 import org.eclipse.swt.widgets.Display;
24 
25 import org.eclipse.draw2d.geometry.PrecisionRectangle;
26 import org.eclipse.draw2d.geometry.Rectangle;
27 
28 /**
29  * A figure used to render a partly transparent copy of an original source
30  * figure.
31  
32  * This class is pretty much based on a sample, posted within the GEF newsgroup
33  * (http://dev.eclipse.org/newslists/news.eclipse.tools.gef/msg15158.html),
34  * although we decided to not cache the ghost image itself (but only its image
35  * data), so the figure does not have to be disposed (and may thus directly
36  * extend {@link Figure} rather than {@link ImageFigure}).
37  
38  @author Philip Ritzkopf
39  @author Alexander Nyssen
40  */
41 public class GhostImageFigure extends Figure {
42 
43   private int alpha = -1;
44   private ImageData ghostImageData;
45 
46   /**
47    * The single constructor.
48    
49    @param source
50    *            The original figure that will be used to render the ghost
51    *            image.
52    @param alpha
53    *            The desired transparency value, to be forwarded to
54    *            {@link Graphics#setAlpha(int)}.
55    @param transparency
56    *            The RBG value of the color that is to be regarded as
57    *            transparent. May be <code>null</code>.
58    */
59   public GhostImageFigure(final IFigure source, int alpha, RGB transparency) {
60     this.alpha = alpha;
61
62     Rectangle sourceFigureRelativePrecisionBounds = new PrecisionRectangle(
63         source.getBounds().getCopy());
64 
65     Image offscreenImage = new Image(Display.getCurrent(),
66         sourceFigureRelativePrecisionBounds.width,
67         sourceFigureRelativePrecisionBounds.height);
68 
69     GC gc = new GC(offscreenImage);
70     SWTGraphics swtGraphics = new SWTGraphics(gc);
71     swtGraphics.translate(-sourceFigureRelativePrecisionBounds.x,
72         -sourceFigureRelativePrecisionBounds.y);
73     source.paint(swtGraphics);
74 
75     ghostImageData = offscreenImage.getImageData();
76     if (transparency != null) {
77       ghostImageData.transparentPixel = ghostImageData.palette
78           .getPixel(transparency);
79     }
80 
81     offscreenImage.dispose();
82     swtGraphics.dispose();
83     gc.dispose();
84   }
85 
86   /**
87    @see Figure#paintFigure(Graphics)
88    */
89   protected void paintFigure(Graphics graphics) {
90     Image feedbackImage = new Image(Display.getCurrent(), ghostImageData);
91     graphics.setAlpha(alpha);
92     graphics.setClip(getBounds().getCopy());
93     graphics.drawImage(feedbackImage, 00, ghostImageData.width,
94         ghostImageData.height, getBounds().x, getBounds().y,
95         getBounds().width, getBounds().height);
96     feedbackImage.dispose();
97   }
98 }

The second central piece of the solution of course is the ScrollableSelectionFeedbackEditPolicy. Upon primary selection of its host IScrollableEditPart, it creates GhostImageFigures for all nodes nested within the host figure's Viewport and all connection figures related to them. To keep up with changes to the host figure and its nested viewport respectively, a couple of listeners are required, which are registered and unregistered in the activate() and deactivate() methods respectively. Note that the layout constraints for the feedback figures are determined by translating the original figure's bounds into absolute coordinates first, then into coordinates relative to the feedback layer (which is chosen to be the SCALABLE_FEEDBACK_LAYER), so feedback is correctly displayed during zooming as well.

001 /**
002  
003  *
004  * Copyright (C) 2005, 2008 Research Group Software Construction,
005  *                          RWTH Aachen University, Germany.
006  *
007  * All rights reserved. This program and the accompanying materials
008  * are made available under the terms of the Eclipse Public License
009  * version 1.0, which accompanies this distribution, and is available
010  * at http://www.eclipse.org/legal/epl-v10.html.
011  *
012  * Contributors:
013  *   Research Group Software Construction - Initial API and implementation
014  *
015  
016  */
017 package sc.viper.gef.scrolling;
018 
019 import java.beans.PropertyChangeEvent;
020 import java.beans.PropertyChangeListener;
021 import java.util.ArrayList;
022 import java.util.HashSet;
023 import java.util.Iterator;
024 import java.util.List;
025 
026 import org.eclipse.core.runtime.Assert;
027 import org.eclipse.draw2d.FigureListener;
028 import org.eclipse.draw2d.Graphics;
029 import org.eclipse.draw2d.IFigure;
030 import org.eclipse.draw2d.LayoutListener;
031 import org.eclipse.draw2d.ScrollPane;
032 import org.eclipse.draw2d.Viewport;
033 import org.eclipse.draw2d.ViewportUtilities;
034 import org.eclipse.draw2d.geometry.Insets;
035 import org.eclipse.draw2d.geometry.Rectangle;
036 import org.eclipse.gef.ConnectionEditPart;
037 import org.eclipse.gef.EditPart;
038 import org.eclipse.gef.GraphicalEditPart;
039 import org.eclipse.gef.LayerConstants;
040 import org.eclipse.gef.editpolicies.SelectionEditPolicy;
041 
042 import sc.viper.gef.EditPartUtilities;
043 
044 /**
045  * A {@link SelectionEditPolicy}, which may be registered to an
046  {@link IScrollableEditPart} to provide primary selection feedback by
047  * rendering the hidden contents of the host figure's {@link ScrollPane}'s
048  * nested {@link Viewport} by means of {@link GhostImageFigure}s.
049  
050  @author Philip Ritzkopf
051  @author Alexander Nyssen
052  @version $Revision: 1.37 $
053  */
054 public class ScrollableSelectionFeedbackEditPolicy extends SelectionEditPolicy {
055 
056   private int feedbackAlpha = 100;
057 
058   private final List feedbackFigures = new ArrayList();
059 
060   private final FigureListener figureListener = new FigureListener() {
061 
062     public void figureMoved(IFigure source) {
063       // react on host figure move
064       if (getHost().getSelected() == EditPart.SELECTED_PRIMARY) {
065         updateFeedback();
066       }
067     }
068   };
069 
070   private final LayoutListener layoutListener = new LayoutListener.Stub() {
071 
072     public void invalidate(IFigure container) {
073       // react on host figure resize
074       if (getHost().getSelected() == EditPart.SELECTED_PRIMARY) {
075         updateFeedback();
076       }
077     }
078   };
079 
080   private final PropertyChangeListener viewportViewLocationChangeListener = new PropertyChangeListener() {
081 
082     public void propertyChange(PropertyChangeEvent event) {
083       // Make sure the host edit part is always selected as primary
084       // selection, when it fires a property change event from its
085       // viewport
086       if (event.getSource() == ((IScrollableFiguregetHostFigure())
087           .getScrollPane().getViewport()
088           && getHost().getSelected() != EditPart.SELECTED_PRIMARY) {
089         getHost().getViewer().deselectAll();
090         getHost().getViewer().select(getHost());
091       }
092       // update feedback in case the viewport's view location changed
093       if (event.getPropertyName().equals(Viewport.PROPERTY_VIEW_LOCATION)) {
094         updateFeedback();
095       }
096     }
097   };
098 
099   /**
100    @see org.eclipse.gef.editpolicies.SelectionEditPolicy#activate()
101    */
102   public void activate() {
103     super.activate();
104     // register all necessary listeners
105     for (Iterator iterator = ViewportUtilities.getViewportsPath(
106         getHostFigureViewport(),
107         ViewportUtilities.getRootViewport(getHostFigure())).iterator(); iterator
108         .hasNext();) {
109       Viewport viewport = (Viewportiterator.next();
110       viewport
111           .addPropertyChangeListener(viewportViewLocationChangeListener);
112 
113     }
114     getHostFigure().addLayoutListener(layoutListener);
115     getHostFigure().addFigureListener(figureListener);
116   }
117 
118   /**
119    * Adds a given feedback figure to the feedback layer (using the provided
120    * bounds to layout it) and registers it in the local
121    {@link #feedbackFigures} list.
122    
123    @param feedbackFigure
124    *            the feedback figure to add to the feedback layer
125    @param feedbackFigureAbsoluteBounds
126    *            the absolute bounds used to layout the feedback figure
127    */
128   protected void addFeedbackFigure(IFigure feedbackFigure,
129       Rectangle feedbackFigureAbsoluteBounds) {
130     getFeedbackLayer().translateToRelative(feedbackFigureAbsoluteBounds);
131     getFeedbackLayer().translateFromParent(feedbackFigureAbsoluteBounds);
132     feedbackFigure.setBounds(feedbackFigureAbsoluteBounds);
133     addFeedback(feedbackFigure);
134     feedbackFigures.add(feedbackFigure);
135   }
136 
137   /**
138    * Creates a ghost image feedback figure for the given
139    {@link ConnectionEditPart}'s figure and adds it to the feedback layer.
140    
141    @param connectionEditPart
142    */
143   protected void createConnectionFeedbackFigure(
144       ConnectionEditPart connectionEditPart) {
145     addFeedbackFigure(new GhostImageFigure(connectionEditPart.getFigure(),
146         getAlpha(), getLayer(LayerConstants.CONNECTION_LAYER)
147             .getBackgroundColor().getRGB()),
148         getAbsoluteBounds(connectionEditPart.getFigure()));
149   }
150 
151   /**
152    * Creates the connection layer feedback figures.
153    */
154   protected void createConnectionFeedbackFigures() {
155     HashSet transitiveNestedConnections = EditPartUtilities
156         .getAllNestedConnectionEditParts((GraphicalEditPartgetHost());
157 
158     for (Iterator iterator = transitiveNestedConnections.iterator(); iterator
159         .hasNext();) {
160       Object connection = iterator.next();
161       if (connection instanceof ConnectionEditPart) {
162         createConnectionFeedbackFigure((ConnectionEditPartconnection);
163       }
164 
165     }
166   }
167 
168   /**
169    * Creates a ghost image feedback figure for the given
170    {@link GraphicalEditPart}'s figure and adds it to the feedback layer.
171    
172    @param childEditPart
173    */
174   protected void createNodeFeedbackFigure(GraphicalEditPart childEditPart) {
175     addFeedbackFigure(new GhostImageFigure(childEditPart.getFigure(),
176         getAlpha()null), getAbsoluteBounds(childEditPart.getFigure()));
177   }
178 
179   /**
180    * Creates the primary layer feedback figures.
181    */
182   protected void createNodeFeedbackFigures() {
183 
184     // create ghost feedback for node children
185     for (Iterator iterator = getHost().getChildren().iterator(); iterator
186         .hasNext();) {
187       Object child = iterator.next();
188       if (child instanceof GraphicalEditPart) {
189         createNodeFeedbackFigure((GraphicalEditPartchild);
190       }
191     }
192   }
193 
194   /**
195    @see org.eclipse.gef.editpolicies.SelectionEditPolicy#deactivate()
196    */
197   public void deactivate() {
198     // remove all registered listeners
199     getHostFigure().removeFigureListener(figureListener);
200     getHostFigure().removeLayoutListener(layoutListener);
201     for (Iterator iterator = ViewportUtilities.getViewportsPath(
202         getHostFigureViewport(),
203         ViewportUtilities.getRootViewport(getHostFigure())).iterator(); iterator
204         .hasNext();) {
205       Viewport viewport = (Viewportiterator.next();
206       viewport
207           .removePropertyChangeListener(viewportViewLocationChangeListener);
208 
209     }
210     super.deactivate();
211   }
212 
213   /**
214    * Used to obtain the alpha value used for all feedback figures. The valid
215    * range is the one documented for {@link Graphics#setAlpha(int)}.
216    
217    @return the alpha
218    */
219   protected int getAlpha() {
220     return feedbackAlpha;
221   }
222 
223   /**
224    @see org.eclipse.gef.editpolicies.SelectionEditPolicy#getFeedbackLayer()
225    */
226   protected IFigure getFeedbackLayer() {
227     return getLayer(LayerConstants.SCALED_FEEDBACK_LAYER);
228   }
229 
230   /**
231    * Provides access to the host figure's {@link Viewport}.
232    
233    @return
234    */
235   protected Viewport getHostFigureViewport() {
236     return ((IScrollableFiguregetHostFigure()).getScrollPane()
237         .getViewport();
238   }
239 
240   /**
241    * Removes all feedback figures from the feedback layer as well as from the
242    {@link #feedbackFigures} list.
243    */
244   protected void hideFeedback() {
245     for (Iterator iterator = feedbackFigures.iterator(); iterator.hasNext();) {
246       removeFeedback((IFigureiterator.next());
247     }
248     feedbackFigures.clear();
249   }
250 
251   /**
252    @see org.eclipse.gef.editpolicies.SelectionEditPolicy#hideSelection()
253    */
254   protected void hideSelection() {
255     hideFeedback();
256   }
257 
258   /**
259    * Used to specify the alpha value used for all feedback figures. The valid
260    * range is the one documented for {@link Graphics#setAlpha(int)}.
261    
262    @param alpha
263    */
264   public void setAlpha(int alpha) {
265     this.feedbackAlpha = alpha;
266   }
267 
268   /**
269    @see org.eclipse.gef.editpolicies.AbstractEditPolicy#setHost(org.eclipse.gef
270    *      .EditPart)
271    */
272   public void setHost(EditPart host) {
273     Assert.isLegal(host instanceof IScrollableEditPart);
274     super.setHost(host);
275   }
276 
277   /**
278    * Creates feedback figures for all node figures nested within the host
279    * figure's viewport, as well as for all incoming and outgoing connections
280    * of these nodes. Feedback figures are only created in case there are
281    * children or connections, which are not fully visible.
282    */
283   protected void showFeedback() {
284     // ensure primary and connection layer are revalidated,
285     // so the bounds of all their child figures, which are
286     // used to calculate the feedback figure constraints,
287     // are valid
288     getLayer(LayerConstants.CONNECTION_LAYER).validate();
289     getLayer(LayerConstants.PRIMARY_LAYER).validate();
290 
291     // check if there is a node child exceeding the client are
292     Rectangle clientArea = getAbsoluteClientArea(getHostFigure());
293     boolean primaryLayerChildExceedsViewport = !clientArea
294         .equals(getAbsoluteViewportArea(((IScrollableFiguregetHostFigure())
295             .getScrollPane().getViewport()));
296     // check if there is a connection exceeding the client area
297     boolean connectionLayerChildExceedsClientArea = false;
298     List connectionLayerChildren = getLayer(LayerConstants.CONNECTION_LAYER)
299         .getChildren();
300     for (Iterator iterator = connectionLayerChildren.iterator(); iterator
301         .hasNext()
302         && !connectionLayerChildExceedsClientArea;) {
303       IFigure connectionLayerChild = (IFigureiterator.next();
304       connectionLayerChildExceedsClientArea = (ViewportUtilities
305           .getNearestEnclosingViewport(connectionLayerChild== ((IScrollableFiguregetHostFigure())
306           .getScrollPane().getViewport() && !clientArea.getExpanded(
307           new Insets(1111)).contains(
308           getAbsoluteBounds(connectionLayerChild)));
309     }
310 
311     // Only show feedback if there is a child or connection figure whose
312     // bounds exceed the client area
313     if (primaryLayerChildExceedsViewport
314         || connectionLayerChildExceedsClientArea) {
315       createNodeFeedbackFigures();
316       createConnectionFeedbackFigures();
317     }
318   }
319 
320   /**
321    @see org.eclipse.gef.editpolicies.SelectionEditPolicy#showSelection()
322    */
323   protected void showSelection() {
324     // force ViewportExposeHelper to perform auto scrolling before
325     // showing the feedback.
326     getHost().getViewer().reveal(getHost());
327     updateFeedback();
328   }
329 
330   /**
331    * Removes any existing feedback figures by delegating to
332    {@link #hideFeedback()}. In case the host edit part is the primary
333    * selection, recreates feedback figures via {@link #showFeedback()}.
334    */
335   protected void updateFeedback() {
336     hideFeedback();
337     if (getHost().getSelected() == EditPart.SELECTED_PRIMARY) {
338       showFeedback();
339     }
340   }
341 
342   private static Rectangle getAbsoluteBounds(IFigure figure) {
343     Rectangle bounds = figure.getBounds().getCopy();
344     figure.translateToAbsolute(bounds);
345     return bounds;
346   }
347 
348   private static Rectangle getAbsoluteClientArea(IFigure figure) {
349     Rectangle clientArea = figure.getClientArea().getCopy();
350     figure.translateToParent(clientArea);
351     figure.translateToAbsolute(clientArea);
352     return clientArea;
353   }
354 
355   private static Rectangle getAbsoluteViewportArea(Viewport viewport) {
356     Rectangle viewportParentBounds = viewport.getParent().getBounds()
357         .getCopy();
358 
359     int widthMax = viewport.getHorizontalRangeModel().getMaximum();
360     int widthMin = viewport.getHorizontalRangeModel().getMinimum();
361     int heightMax = viewport.getVerticalRangeModel().getMaximum();
362     int heightMin = viewport.getVerticalRangeModel().getMinimum();
363 
364     viewportParentBounds
365         .setSize(widthMax - widthMin, heightMax - heightMin);
366     viewportParentBounds.translate(widthMin, heightMin);
367     viewportParentBounds.translate(viewport.getViewLocation().getNegated());
368     viewport.getParent().translateToAbsolute(viewportParentBounds);
369     return viewportParentBounds;
370   }
371 }

Those interfaces created to allow type-safe access to the host edit part's figure, namely  IScrollableFigure and IScrollableEditPart are rather unexciting, thus I will skip them here, as well as the utility classes (EditPartSupport and ViewportUtilities) we introduced to support some of the calculations within the edit policy.

What may be of interest however is how the circuit figure of the logic example can be beautified with respect to its scroll bars (as mentioned above, the screenshots were produced after having performed some beautifications on the GEF logic example): 



It is indeed done by exchanging the ScrollPane being used within the CircuitFigure of the logic example with a custom implementation we have chosen to name PuristicScrollPane. It uses special PuristicScrollBars, which do not show a 'thumb' and use non-opaque navigation buttons. Here is the code:

001 /**
002  
003  *
004  * Copyright (C) 2005, 2008 Research Group Software Construction,
005  *                          RWTH Aachen University, Germany.
006  *
007  * All rights reserved. This program and the accompanying materials
008  * are made available under the terms of the Eclipse Public License
009  * version 1.0, which accompanies this distribution, and is available
010  * at http://www.eclipse.org/legal/epl-v10.html.
011  *
012  * Contributors:
013  *   Research Group Software Construction - Initial API and implementation
014  *
015  
016  */
017 package org.eclipse.draw2d;
018 
019 import java.beans.PropertyChangeEvent;
020 import java.beans.PropertyChangeListener;
021 
022 import org.eclipse.draw2d.geometry.Rectangle;
023 
024 public class PuristicScrollPane extends ScrollPane {
025 
026   /**
027    * A {@link ScrollBar} with no thumb and non-opaque buttons.
028    
029    @author Alexander Nyssen
030    @author Philip Ritzkopf
031    */
032   public class PuristicScrollBar extends ScrollBar {
033 
034     /**
035      * Instantiates a new transparent scroll bar.
036      
037      @param isHorizontal
038      *            whether this scroll bar is used as a horizontal one.
039      */
040     public PuristicScrollBar(boolean isHorizontal) {
041       super();
042       setHorizontal(isHorizontal);
043     }
044 
045     /**
046      @see org.eclipse.draw2d.ScrollBar#createDefaultDownButton()
047      */
048     protected Clickable createDefaultDownButton() {
049       Clickable buttonDown = super.createDefaultDownButton();
050       buttonDown.setBorder(null);
051       buttonDown.setOpaque(false);
052       return buttonDown;
053     }
054 
055     /**
056      @see org.eclipse.draw2d.ScrollBar#createDefaultThumb()
057      */
058     protected IFigure createDefaultThumb() {
059       return null;
060     }
061 
062     /**
063      @see org.eclipse.draw2d.ScrollBar#createDefaultUpButton()
064      */
065     protected Clickable createDefaultUpButton() {
066       Clickable buttonUp = super.createDefaultUpButton();
067       buttonUp.setBorder(null);
068       buttonUp.setOpaque(false);
069       return buttonUp;
070     }
071 
072     /**
073      @see org.eclipse.draw2d.ScrollBar#createPageDown()
074      */
075     protected Clickable createPageDown() {
076       return null;
077     }
078 
079     /**
080      @see org.eclipse.draw2d.ScrollBar#createPageUp()
081      */
082     protected Clickable createPageUp() {
083       return null;
084     }
085 
086     /**
087      @see PropertyChangeListener#propertyChange(java.beans.
088      *      PropertyChangeEvent )
089      */
090     public void propertyChange(PropertyChangeEvent event) {
091       if (event.getSource() instanceof RangeModel) {
092         getButtonDown().setVisible(
093             getValue() != getMaximum() - getExtent());
094         getButtonUp().setVisible(getValue() != getMinimum());
095       }
096       super.propertyChange(event);
097     }
098   }
099 
100   /**
101    @see org.eclipse.draw2d.ScrollPane#createVerticalScrollBar()
102    */
103   protected void createVerticalScrollBar() {
104     PuristicScrollBar verticalScrollBar = new PuristicScrollBar(false);
105     setVerticalScrollBar(verticalScrollBar);
106   }
107 
108   /**
109    @see org.eclipse.draw2d.ScrollPane#createHorizontalScrollBar()
110    */
111   protected void createHorizontalScrollBar() {
112     PuristicScrollBar horizontalScrollBar = new PuristicScrollBar(true);
113     setHorizontalScrollBar(horizontalScrollBar);
114   }
115 
116   /**
117    @see org.eclipse.draw2d.Figure#paintChildren(org.eclipse.draw2d.Graphics)
118    */
119   protected void paintChildren(Graphics graphics) {
120     IFigure child;
121     // don't clip scroll bar area (as there is no thumb)
122     Rectangle clip = Rectangle.SINGLETON;
123     for (int i = 0; i < getChildren().size(); i++) {
124       child = (IFiguregetChildren().get(i);
125       if (child.isVisible() && child.intersects(graphics.getClip(clip))) {
126         graphics.clipRect(getBounds());
127         child.paint(graphics);
128         graphics.restoreState();
129       }
130     }
131   }
132 
133   /**
134    @see org.eclipse.draw2d.Figure#invalidate()
135    */
136   public void invalidate() {
137     // ensure scroll bar area is marked dirty as well.
138     getUpdateManager().addDirtyRegion(this, this.getBounds());
139     super.invalidate();
140   }
141 }

Note that in order to achieve that children are also painted below the now transparent scroll bars, the paintChildren() and invalidate() methods within the PuristicScrollPane have to be refined accordingly. To achieve that the navigation buttons are only shown in case they are needed, the PuristicScrollBar implementation reacts to changes to its underlying range model by means of the PropertyChangeListener mechanism. 

If with some of the aforementioned I could raise your interest, I propose you simply try the feature out yourself. All source code related to it (including those utility classes not explicitly shown here) is included in a patch I added to bug #303557 (which I have created to contribute the feature to GEF) as well as another patch (which contains a ViewportUtilities helper class that is used by the contribution) I had created to address aforementioned clipping problems as part of bug #195527. You may thus simply download both patches and apply them to the current HEAD version of GEF (and the GEF logic example).

The patch added to bug #303557 may also serve as a good starting point in case you not only want to trial the logic example but apply the feature to your own code, because you can infer all that is required from those few changes, we have made to CircuitFigure and CircuitEditPart respectively.