From dcaf54f426bfd2c93b79d636b0e36df79fdfcdb4 Mon Sep 17 00:00:00 2001 From: Simon Barner <barner@fortiss.org> Date: Wed, 3 Sep 2014 08:38:36 +0000 Subject: [PATCH] - Enable filtering of rows (component names) and columns (annotation names) refs 1841 --- .../view/CreateAnnotationInstanceColumn.java | 5 +- .../view/GenericAnnotationView.java | 234 +++++++++++++++++- .../ui/annotation/view/TextFocusListener.java | 95 +++++++ 3 files changed, 325 insertions(+), 9 deletions(-) create mode 100644 org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/TextFocusListener.java diff --git a/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/CreateAnnotationInstanceColumn.java b/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/CreateAnnotationInstanceColumn.java index 25902dae1..fff800606 100644 --- a/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/CreateAnnotationInstanceColumn.java +++ b/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/CreateAnnotationInstanceColumn.java @@ -134,7 +134,10 @@ public class CreateAnnotationInstanceColumn extends ViewerColumn { @Override public void widgetDisposed(DisposeEvent e) { - crButton.dispose(); + if(crButton != null) { + crButton.dispose(); + crButton = null; + } } }); } diff --git a/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/GenericAnnotationView.java b/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/GenericAnnotationView.java index 3ad0201e5..379f0524c 100644 --- a/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/GenericAnnotationView.java +++ b/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/GenericAnnotationView.java @@ -28,11 +28,22 @@ import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerComparator; +import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.swt.SWT; -import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.Text; import org.fortiss.tooling.base.model.element.IAnnotatedSpecification; import org.fortiss.tooling.base.model.element.IModelElement; import org.fortiss.tooling.base.ui.annotation.AnnotationEntry; @@ -63,11 +74,31 @@ public class GenericAnnotationView extends AnnotationViewPartBase { protected TableViewer tableViewer; /** - * Character used to separte the annotation name from the instance name for multi-instance + * Character used to separate the annotation name from the instance name for multi-instance * annotations. */ private final static String MULTIINSTANCE_NAME_SEPARATOR = ":"; + /** Widget for editing of filter text. */ + private Text filterText; + + /** Filter hint text. */ + private static final String FILTER_HINT_TEXT = "type filter text"; + + /** Filter pattern */ + private String filterPattern; + + /** Flag whether columns should be filtered. */ + boolean filterColumns; + + /** + * Last set of {@link AnnotationEntry}s used to {@link #update(Collection)} the + * {@link GenericAnnotationView}, i.e. during the construction of the, or because of a model + * change. The reference is required to update view after a change of the column (annotation) + * filter. + */ + Collection<AnnotationEntry> lastAnnotationEntries; + /** * Data required to identify a column displaying a particular {@link IAnnotatedSpecification} in * a column of the {@link GenericAnnotationView}. Used to sort columns (see @@ -276,9 +307,73 @@ public class GenericAnnotationView extends AnnotationViewPartBase { } } + // Based on org.fortiss.af3.scheduling.ui.specificationsEditor.ComponentResourceTableBackend + /** Filters rows of the {@link GenericAnnotationView} based on the components' names. */ + private class ComponentNamesRowFilter extends ViewerFilter { + /** Flag if the filter is active */ + private boolean active = true; + + /** (De-)activates the filter */ + public void setActive(boolean active) { + this.active = active; + } + + /** {@inheritDoc} */ + @Override + public boolean select(Viewer viewer, Object parentElement, Object element) { + if(!active) { + return true; + } + + if(!(element instanceof AnnotationEntry)) { + return true; + } + + AnnotationEntry annotationEntry = ((AnnotationEntry)element); + if(!(annotationEntry.getModelElement() instanceof INamedElement)) { + return true; + } + + return passesFilter(((INamedElement)annotationEntry.getModelElement()).getName(), + filterPattern); + } + } + + /** + * Returns true if a given {@code input} passes an case-insensitive filter specified by + * {@code filterString}. + */ + private static boolean passesFilter(String input, String filterString) { + // Null-filter accepts every input + if(filterString == null || filterString.equals(FILTER_HINT_TEXT)) { + return true; + } + + // Null-input cannot be accepted by a non-null filter + if(input == null) { + return false; + } + return input.toLowerCase().contains(filterString.toLowerCase()); + } + + /** Updates the table after a filter change */ + private void update() { + // Full update, i.e. including reconstructions of columns is always required due to the + // following scenario: + // - Filter is set for annotation names. + // - Filter scope is changed to component names -> annotation filter needs to be undone + if(lastAnnotationEntries != null) { + update(lastAnnotationEntries); + } + } + /** {@inheritDoc} */ @Override protected void update(Collection<AnnotationEntry> annotationEntries) { + // Remember last annotation entries (required to re-draw table after changing the + // column/annotation filter + lastAnnotationEntries = annotationEntries; + // Minimize flickering while updating the table tableViewer.getTable().setRedraw(false); @@ -304,7 +399,12 @@ public class GenericAnnotationView extends AnnotationViewPartBase { !entry.getInstanceKeys(spec.getClass()).isEmpty()) { for(String instanceKey : entry.getInstanceKeys(spec.getClass())) { - sortedColumnHandles.add(new ColumnHandle(entry, spec, instanceKey, false)); + ColumnHandle columnHandle = + new ColumnHandle(entry, spec, instanceKey, false); + if(!filterColumns || + passesFilter(getColumnName(columnHandle), filterPattern)) { + sortedColumnHandles.add(columnHandle); + } } } @@ -314,8 +414,13 @@ public class GenericAnnotationView extends AnnotationViewPartBase { // created dynamically if(entry.getInstanceKeys(spec.getClass()).isEmpty() || entry.allowsDynamicAnnotationInstances(spec.getClass())) { - sortedColumnHandles.add(new ColumnHandle(entry, spec, null, entry - .allowsDynamicAnnotationInstances(spec.getClass()))); + ColumnHandle columnHandle = + new ColumnHandle(entry, spec, null, + entry.allowsDynamicAnnotationInstances(spec.getClass())); + + if(!filterColumns || passesFilter(getColumnName(columnHandle), filterPattern)) { + sortedColumnHandles.add(columnHandle); + } } } @@ -334,21 +439,134 @@ public class GenericAnnotationView extends AnnotationViewPartBase { tableViewer.refresh(); } + /** Creates the UI controls for the column and row filter. */ + private void createFilter(Composite rootComposite) { + // Rows (= components) are filtered using a specialized ViewerFilter. + // Since the data model is an array of AnnotationEntries, columns (= annotations) + // cannot be filtered using such a filter, and require the reconstruction of the + // table, with an implementation of the filter in update(Set<AnnotationEntry>). + final ComponentNamesRowFilter rowfilter = new ComponentNamesRowFilter(); + rowfilter.setActive(true); + tableViewer.addFilter(rowfilter); + + // Composite for filter GUI controls + Composite filterComposite = new Composite(rootComposite, SWT.NULL); + filterComposite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1)); + filterComposite.setLayout(new GridLayout(2, false)); + + // Text input field for filter pattern + filterText = + new Text(filterComposite, SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_CANCEL); + filterText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true, 1, 1)); + filterText.setText(FILTER_HINT_TEXT); + filterText.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GRAY)); + + // Observe changes of filter pattern + filterText.addModifyListener(new ModifyListener() { + @Override + public void modifyText(ModifyEvent e) { + filterPattern = filterText.getText(); + if(filterPattern.isEmpty()) { + filterText.setText(FILTER_HINT_TEXT); + filterText.selectAll(); + filterPattern = FILTER_HINT_TEXT; + } + if(filterPattern.equals(FILTER_HINT_TEXT)) { + filterText.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_DARK_GRAY)); + } else { + filterText.setForeground(Display.getCurrent().getSystemColor( + SWT.COLOR_WIDGET_FOREGROUND)); + } + update(); + } + }); + + // Select the filter pattern if it is equal to the FILTER_HINT_TEXT + TextFocusListener textSelectionOnFocusListener = new TextFocusListener() { + + @Override + protected void focusOut(Text text, Event e) { + text.clearSelection(); + + } + + @Override + protected void focusIn(Text text, Event e) { + if(text.getText().equals(FILTER_HINT_TEXT) && + (e.type != SWT.MouseUp || text.getSelectionCount() == 0)) { + text.selectAll(); + } + + } + }; + filterText.addListener(SWT.FocusIn, textSelectionOnFocusListener); + filterText.addListener(SWT.FocusOut, textSelectionOnFocusListener); + filterText.addListener(SWT.MouseDown, textSelectionOnFocusListener); + filterText.addListener(SWT.MouseUp, textSelectionOnFocusListener); + + // Radio buttons to select between row and column filtering + Composite radioButtonComposite = new Composite(filterComposite, SWT.NULL); + radioButtonComposite.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1)); + radioButtonComposite.setLayout(new RowLayout()); + + final Button filterComponentNamesButton = new Button(radioButtonComposite, SWT.RADIO); + filterComponentNamesButton.setText("Components names"); + filterComponentNamesButton.setSelection(true); + filterComponentNamesButton.addSelectionListener(new SelectionAdapter() { + + @Override + public void widgetSelected(SelectionEvent e) { + // widgetSelected() is also fired when the selection is removed from the radio + // button, + // so check, if this is the button that just obtained the selection + if(!((Button)e.getSource()).getSelection()) { + return; + } + rowfilter.setActive(true); + filterColumns = false; + update(); + } + }); + + final Button filterAnnotationNamesButton = new Button(radioButtonComposite, SWT.RADIO); + filterAnnotationNamesButton.setText("Annotation names"); + filterAnnotationNamesButton.setSelection(false); + filterAnnotationNamesButton.addSelectionListener(new SelectionAdapter() { + + @Override + public void widgetSelected(SelectionEvent e) { + // widgetSelected() is also fired when the selection is removed from the radio + // button, + // so check, if this is the button that just obtained the selection + if(!((Button)e.getSource()).getSelection()) { + return; + } + rowfilter.setActive(false); + filterColumns = true; + update(); + } + }); + } + /** {@inheritDoc} */ @Override public void createPartControl(Composite parent) { super.createPartControl(parent); - Composite c = new Composite(parent, SWT.NULL); - c.setLayout(new FillLayout(SWT.HORIZONTAL)); + Composite rootComposite = new Composite(parent, SWT.NULL); + rootComposite.setLayout(new GridLayout(1, false)); - tableViewer = new TableViewer(c, SWT.BORDER | SWT.FULL_SELECTION); + tableViewer = new TableViewer(rootComposite, SWT.BORDER | SWT.FULL_SELECTION); Table table = tableViewer.getTable(); table.setHeaderVisible(true); table.setLinesVisible(true); + table.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 1, 1)); createModelElementColumn(); tableViewer.setComparator(new HierarchicalNameComparator()); + + createFilter(rootComposite); } /** Creates the (leading) column which displays the model elements */ diff --git a/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/TextFocusListener.java b/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/TextFocusListener.java new file mode 100644 index 000000000..b87aa9dd1 --- /dev/null +++ b/org.fortiss.tooling.base.ui/trunk/src/org/fortiss/tooling/base/ui/annotation/view/TextFocusListener.java @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------+ +$Id$ +| | +| Copyright 2014 ForTISS GmbH | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); | +| you may not use this file except in compliance with the License. | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software | +| distributed under the License is distributed on an "AS IS" BASIS, | +| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | +| See the License for the specific language governing permissions and | +| limitations under the License. | ++--------------------------------------------------------------------------*/ + +// Based on http://stackoverflow.com/a/10048884, provided by Stackoverflow user "seand". +// (follow link for license) +package org.fortiss.tooling.base.ui.annotation.view; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Text; + +/** + * Listener to react to focus in/out events of {@link Text} controls than can originate from both + * keyboard and mouse events. + */ +public abstract class TextFocusListener implements Listener { + + /** Flag if {@link Text} currently has the focus */ + private boolean hasFocus = false; + + /** Flag required to obtain focus via mouse events */ + private boolean hadFocusOnMousedown = false; + + /** Action to be performed on focus in events */ + protected abstract void focusIn(Text text, Event e); + + /** Action to be performed on focus out events */ + protected abstract void focusOut(Text text, Event e); + + /** {@inheritDoc} */ + @Override + public void handleEvent(Event e) { + if(!(e.widget instanceof Text)) { + return; + } + Text text = (Text)e.widget; + + switch(e.type) { + case SWT.FocusIn: { + // Covers the case where the user focuses by keyboard. + focusIn(text, e); + + // The case where the user focuses by mouse click is special because Eclipse, + // for some reason, fires SWT.FocusIn before SWT.MouseDown, and on mouse down + // it cancels the selection. So we set a variable to keep track of whether the + // control is focused (can't rely on isFocusControl() because sometimes it's + // wrong), and we make it asynchronous so it will get set AFTER SWT.MouseDown is + // fired. + text.getDisplay().asyncExec(new Runnable() { + @Override + public void run() { + hasFocus = true; + } + }); + + break; + } + case SWT.FocusOut: { + hasFocus = false; + focusOut(text, e); + + break; + } + case SWT.MouseDown: { + // Set the variable which is used in SWT.MouseUp. + hadFocusOnMousedown = hasFocus; + + break; + } + case SWT.MouseUp: { + if(!hadFocusOnMousedown) { + focusIn(text, e); + } + + break; + } + } + } +} -- GitLab