Article Using the Selection Service

From mitk.org
Revision as of 14:56, 4 December 2017 by Kahl (talk | contribs) (→‎Implementing a Selection Listener)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

BlueBerry Workbench: Using the Selection Service

This article is based on this Eclipse Corner Article written by Marc R. Hoffmann.


Summary

The selection service provided by the BlueBerry workbench allows efficient linking of different parts within the workbench window. Knowing and using the existing selection mechanisms gives your plug-ins a clean design, smoothly integrates them into the workbench and opens them for future extensions.


Introduction

The BlueBerry workbench is a powerful UI framework for rich applications. It provides many services for a highly integrated and extensible user interface. One of the integration aspects are view parts that provide additional information for particular objects and update their content automatically whenever such objects are selected somewhere in the workbench window. For example the "Properties" view in MITK applications behaves in this way: Wherever an element is selected in the workbench this view lists the properties of that element.

insert image here

Other aspects of the workbench like the enablement of global actions may also depend on the current selection.

Instead of implementing "hard-wired communication" plug-ins can rely on the so called selection service. It decouples parts where items can be selected from others reacting on selection changes.

This article outlines the functionality and usage of the selection service.


The Big Picture

Each workbench window has its own selection service instance. The service keeps track of the selection in the currently active part and propagates selection changes to all registered listeners. Such selection events occur when the selection in the current part is changed or when a different part is activated. Both can be triggered by user interaction or programmatically.

The view where elements or text is selected does not need to know who is interested in the selection. Therefore we may create new views that depend on the selection of an existing view – without changing a single line of code in that view.

The next sections explain who provides what kind of selections to whom.


What can be selected?

From the users point of view a selection is a set of highlighted entries in a viewer like a table or tree widget. A selection can also be a piece of text in an editor.

Internally a selection is a data structure holding the model objects which corresponds to the graphical elements selected in the workbench. As pointed out before there are two fundamental different kinds of selections:

  • A list of objects
  • A piece of text

Both can be empty, i.e. a empty list or a text string with zero length. Following the typical Eclipse philosophy these data structures are properly defined by interfaces:

  • berry::ISelection
    • berry::IStructuredSelection
    • berry::ITextSelection (not available yet)

The IStructuredSelection refers to a list of objects. ITextSelection describes a piece of selected text.


Telling the Workbench Window about Selections

GUI widgets which serve as viewers for a particular set of items must provide a so called selection provider. A selection provider implements the interface ISelectionProvider.

Due to the elegant model/view concept in Qt, the workbench provides the QtSelectionProvider class for Qt viewers which must be provided with a QItemSelectionModel:

<syntaxhighlight lang="cpp"> m_SelectionProvider = new berry::QtSelectionProvider(); m_SelectionProvider->SetItemSelectionModel(qtViewer->selectionModel()); </syntaxhighlight>

If you are using MITK and the model for your Qt viewer can provide mitk::DataTreeNode instances, you should use the adapter class QmitkDataTreeNodeSelectionProvider:

<syntaxhighlight lang="cpp"> m_SelectionProvider = new QmitkDataTreeNodeSelectionProvider(); m_SelectionProvider->SetItemSelectionModel(qtViewer->selectionModel()); </syntaxhighlight>

Also custom viewers may serve as selection providers and implement the ISelectionProvider interface directly.

Any workbench part that holds a viewer should register a selection provider with the respective view site:

<syntaxhighlight lang="cpp"> GetSite()->SetSelectionProvider(m_SelectionProvider) </syntaxhighlight>

Even if you don't see a need for propagating your selection right now, this opens your plug-in for future extensions by you or by other plug-in implementers. If your view defines actions that depend on the current selection, setting a selection provider is also necessary to allow dynamic enablement of these actions.

Tracking the Current Selection

The workbench window is typically assembled in many parts, each coming with at least one viewer. The workbench keeps track about the currently selected part in the window and the selection within this part. Here starts the interesting part of the story: Plug-in implementations can access this information or register for notifications on selection changes.

Each workbench window has an ISelectionService implementation which allows tracking the current selection. A view part can obtain a reference to it through its site:

<syntaxhighlight lang="cpp"> GetSite()->GetWorkbenchWindow()->GetSelectionService() </syntaxhighlight>

The selection service knows the current selection of the active part or of a part with a particular id:

<syntaxhighlight lang="cpp"> berry::ISelection::ConstPointer GetSelection() berry::ISelection::ConstPointer GetSelection(const std::string& partId) </syntaxhighlight>

Typically views react on selection changes in the workbench window. In this case they better register an ISelectionListener on the ISelectionService to get notified when the window's current selection changes:

<syntaxhighlight lang="cpp"> void AddSelectionListener(berry::ISelectionListener::Pointer listener) void RemoveSelectionListener(berry::ISelectionListener::Pointer listener) </syntaxhighlight>

Listeners registered in this way are notified when the selection in the active part is changed or when a different part is activated. Only selections within the active part are propagated. If the application is only interested in the selection of a particular part (independently of its actual activation) one may register the listener only for the respective part id:

<syntaxhighlight lang="cpp"> void AddSelectionListener(const std::string& partId, berry::ISelectionListener::Pointer listener) void RemoveSelectionListener(const std::string& partId, berry::ISelectionListener::Pointer listener) </syntaxhighlight>

This works even if there is currently no part with such a id. As soon as the part is created its initial selection will be propagated to the listeners registered for it. When the part is destroyed, the listener is passed a null selection.


Implementing a Selection Listener

The ISelectionListener is a simple class with just one method. You can either subclass ISelectionListener or use the adapter template SelectionChangedAdapter<R>:

<syntaxhighlight lang="cpp">

  1. include <berryISelectionListener.h>

class MyView : public berry::ViewPart {

 public:
   MyView();
   ...
 private:
   void SelectionChanged(berry::IWorkbenchPart::Pointer sourcepart,
                    berry::ISelection::ConstPointer selection);
   berry::ISelectionListener::Pointer m_SelectionListener;
   friend struct berry::SelectionChangedAdapter<MyView>;

};

MyView::MyView()

m_SelectionListener(new berry::SelectionChangedAdapter<MyView>(this, &MyView::SelectionChanged))

{ }

void MyView::SelectionChanged(berry::IWorkbenchPart::Pointer sourcepart,

                              berry::ISelection::ConstPointer selection)

{

 if (sourcepart != this && 
     selection.Cast<const berry::IStructuredSelection>())
 {
   DoSomething(selection.Cast<const berry::IStructuredSelection>());
 }

} </syntaxhighlight>

If you are writing MITK code and are only interested in mitk::DataTreeNode items, you can try to cast the ISelection to a mitk::DataTreeNodeSelection instances.

Depending on your requirements your listener implementation probably needs to deal with the following issues as shown in the code snippet above:

  • In case we also provide selections (e.g. a view or editor) we should (line 25) exclude our own selection events from processing. This avoids unexpected results when the user selects elements within our part.
  • Check whether we can handle this kind of selection (line 26).
  • Get the selected content from the selection and process it (line 28).

Removing the Selection Listener

Don't forget to remove your selection listener when you can't handle events any more, e.g. when your view has been closed. The destructor of your view is a good place to remove your listener:

<syntaxhighlight lang="cpp"> MyClass::~MyClass() {

 berry::ISelectionService* s = GetSite()->GetWorkbenchWindow()->GetSelectionService();
 s->RemoveSelectionListener(m_SelectionListener);

} </syntaxhighlight>

More Selection Issues

Up to now we focused on the core functionality of the selection service, which covers most of the use cases. There are additional issues that might come into picture in implementation projects.


Post Selection

When navigating views the selection changes frequently - especially when the keyboard is used to scroll through long lists or the mouse is dragged over some text. This will lead to many unnecessary updates of the viewers registered as listeners to the selection service and may make your application less responsive.

So called post selection events will be send-out with a slight delay. All intermediate selections during the delay time are ignored; just the final selection is propagated. The ISelectionService has additional methods to register listeners for the delayed selection events:

  • void AddPostSelectionListener(ISelectionListener::Pointer listener)
  • void RemovePostSelectionListener(ISelectionListener::Pointer listener)
  • void AddPostSelectionListener(const std::string& partId, ISelectionListener::Pointer listener)
  • void RemovePostSelectionListener(const std::string& partId, ISelectionListener::Pointer listener)

To avoid performance issues viewers should typically register selection listeners this way.

The selection providers are responsible for sending out the delayed events and have to implement the berry::IPostSelectionProvider interface if they support it – all provided Qt selection providers do so.


Null Selections

The call-back method SelectionChanged() defined in berry::ISelectionListener gets the new selection as well as the originating part passed in as parameters:

  • void SelectionChanged(IWorkbenchPart::Pointer part, ISelection::Pointer selection)

These parameters may be null in the following cases:

  • The active part has not set a selection provider.
  • The specific part we have registered our listener for has not set a selection provider.
  • There is no active part in the workbench window, all parts are closed.
  • The specific part we have registered our listener for has been closed.


Multiple Selection Providers Within a Part

Be aware that the part's site accepts a single selection provider only, which should be registered within the CreatePartControl() method only:

<syntaxhighlight lang="cpp"> GetSite()->SetSelectionProvider(provider); </syntaxhighlight>

Replacing the selection provider during the lifetime of the part is not properly supported by the workbench. If a part contains multiple viewers providing selections, a intermediate ISelectionProvider implementation has to be provided that allows dynamically delegating to the currently active viewer within the part.


What to do With the Selected Objects?

This article claims that the selection service helps to decouple views reacting on each others selection. But the view handling a selection still needs to deal with the selected objects to provide any useful functionality. Check out the adapter pattern provided by the BlueBerry runtime core, which allows attaching new functionality to existing model objects or the other way round providing required functionality for newly contributed objects. (There will be an own article about the adapter pattern in the future)


Conclusion

The mechanisms provided by the BlueBerry workbench are simple to use and powerful. Not using this mechanisms result in plug-ins that poorly integrate with the rest of the workbench and will be hard to extend. To avoid such pitfalls simply follow these rules when selections come into picture:

  • Avoid direct hard-wired inter-view communication. If one view needs to react on the selection in another view use the ISelectionService.
  • Be cooperative and open for future extensions: Always register your viewers as selection providers with the part site.
  • Use existing selection specific views like the "Properties" view when appropriate instead of creating new ones.