blob: 5333b0eab2a42c8192373ac17340eb52046e6359 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2000, 2021 IBM Corporation and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0.
#
# SPDX-License-Identifier: EPL-2.0
#
# Contributors:
# IBM Corporation - org.eclipse.jdt: initial API and implementation
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.ecommons.ui.dialogs;
import static org.eclipse.statet.ecommons.ui.actions.UIActions.VIEW_FILTER_GROUP_ID;
import static org.eclipse.statet.ecommons.ui.actions.UIActions.VIEW_SORT_GROUP_ID;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.bindings.keys.SWTKeySupport;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlExtension;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.viewers.AbstractTreeViewer;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Item;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.ecommons.models.core.util.ElementProxy;
import org.eclipse.statet.ecommons.ui.SharedMessages;
import org.eclipse.statet.ecommons.ui.components.SearchText;
import org.eclipse.statet.ecommons.ui.content.IElementFilter;
import org.eclipse.statet.ecommons.ui.content.ITextElementFilter;
import org.eclipse.statet.ecommons.ui.content.TextElementFilter;
import org.eclipse.statet.ecommons.ui.util.LayoutUtils;
import org.eclipse.statet.ecommons.ui.util.UIAccess;
import org.eclipse.statet.internal.ecommons.ui.UIMiscellanyPlugin;
/**
* Abstract class for showing tree in light-weight controls.
*/
@NonNullByDefault
public abstract class QuickTreeInformationControl extends PopupDialog
implements IInformationControl, IInformationControlExtension, IInformationControlExtension2,
DisposeListener {
private static class ElementWithName implements ElementProxy {
private IAdaptable element;
private String name;
public void set(final IAdaptable element, final String name) {
this.element= element;
this.name= name;
}
@Override
public IAdaptable getElement() {
return this.element;
}
@Override
public <T> T getAdapter(final Class<T> adapterType) {
return this.element.getAdapter(adapterType);
}
@Override
public String toString() {
return this.name;
}
}
/**
* The filter selects the elements which match the given string patterns.
*/
protected class SearchFilter extends ViewerFilter {
public SearchFilter() {
}
@Override
public boolean select(final Viewer viewer, final Object parentElement, final Object element) {
if (QuickTreeInformationControl.this.select(element)) {
return true;
}
// has unfiltered child?
final Object[] children= ((ITreeContentProvider) QuickTreeInformationControl.this.treeViewer.getContentProvider())
.getChildren(element);
for (int i= 0; i < children.length; i++) {
if (select(viewer, element, children[i])) {
return true;
}
}
return false;
}
}
/** The control's text widget */
private SearchText filterText;
/** The control's tree widget */
private TreeViewer treeViewer;
/** The current string matcher */
protected ITextElementFilter nameFilter;
private IElementFilter.IFinalFilter finalNameFilter;
private final ElementWithName nameFilterElement= new ElementWithName();
private final int iterationCount;
private int iterationPosition;
private final String commandId;
private String commandBestKeyStrokeFormatted;
private List<KeyStroke> commandActiveKeyStrokes;
private KeyListener commandKeyListener;
// private IAction fShowViewMenuAction;
// private HandlerSubmission fShowViewMenuHandlerSubmission;
/**
* Creates a tree information control with the given shell as parent. The given
* styles are applied to the shell and the tree widget.
*
* @param parent the parent shell
* @param shellStyle the additional styles for the shell
* @param treeStyle the additional styles for the tree widget
* @param commandId the id of the command that invoked this control or <code>null</code>
*/
public QuickTreeInformationControl(final Shell parent, final int shellStyle,
final boolean showStatusField, final String commandId, final int iterationCount) {
super(parent, shellStyle, true, true, false, true, true, null, null);
if (iterationCount < 1) {
throw new IllegalArgumentException("iterationCount"); //$NON-NLS-1$
}
this.commandId= commandId;
this.iterationCount= iterationCount;
if (this.commandId != null && this.iterationCount > 1) {
initIterateKeys();
}
// Title and status text must be set to get the title label created, so force empty values here.
setInfoText(""); // //$NON-NLS-1$
this.nameFilter= createNameFilter();
create();
}
private void initIterateKeys() {
if (this.commandId == null || this.iterationCount == 1) {
return;
}
final IBindingService bindingSvc= PlatformUI.getWorkbench().getService(IBindingService.class);
if (bindingSvc == null) {
return;
}
{ final TriggerSequence sequence= bindingSvc.getBestActiveBindingFor(this.commandId);
final KeyStroke keyStroke= getKeyStroke(sequence);
if (keyStroke == null) {
return;
}
this.commandBestKeyStrokeFormatted= keyStroke.format();
}
{ final TriggerSequence[] sequences= bindingSvc.getActiveBindingsFor(this.commandId);
this.commandActiveKeyStrokes= new ArrayList<>(sequences.length);
for (int i= 0; i < sequences.length; i++) {
final KeyStroke keyStroke= getKeyStroke(sequences[i]);
if (keyStroke != null) {
this.commandActiveKeyStrokes.add(keyStroke);
}
}
}
this.commandKeyListener= new KeyAdapter() {
@Override
public void keyPressed(final KeyEvent event) {
final KeyStroke keyStroke= SWTKeySupport.convertAcceleratorToKeyStroke(
SWTKeySupport.convertEventToUnmodifiedAccelerator(event) );
for (final KeyStroke activeKeyStroke : QuickTreeInformationControl.this.commandActiveKeyStrokes) {
if (activeKeyStroke.equals(keyStroke)) {
event.doit= false;
iterate();
return;
}
}
}
};
}
private @Nullable KeyStroke getKeyStroke(final TriggerSequence triggerSequence) {
if (triggerSequence instanceof KeySequence) {
final KeyStroke[] keyStrokes= ((KeySequence) triggerSequence).getKeyStrokes();
if (keyStrokes.length == 1) {
return keyStrokes[0];
}
}
return null;
}
protected abstract String getDescription(int iterationPosition);
protected void updateInfoText() {
final StringBuilder sb= new StringBuilder(
getDescription(getIterationPosition()) );
if (this.commandBestKeyStrokeFormatted != null) {
sb.append("\u2004\u2004"); //$NON-NLS-1$
sb.append(NLS.bind(SharedMessages.DoToShow_message,
this.commandBestKeyStrokeFormatted,
getDescription(getNextIterationPosition()) ));
}
sb.append('\u2006');
setInfoText(sb.toString());
}
/**
* Create the main content for this information control.
*
* @param parent The parent composite
* @return The control representing the main content.
* @since 3.2
*/
@Override
protected Control createDialogArea(final Composite parent) {
final TreeViewer viewer= new TreeViewer(parent, SWT.SINGLE | SWT.V_SCROLL | SWT.H_SCROLL);
final Tree tree= viewer.getTree();
{ final GridData gd= new GridData(GridData.FILL_BOTH);
gd.heightHint= LayoutUtils.hintHeight(tree, 12);
tree.setLayoutData(gd);
}
viewer.setUseHashlookup(true);
viewer.setAutoExpandLevel(AbstractTreeViewer.ALL_LEVELS);
configureViewer(viewer);
viewer.addFilter(new SearchFilter());
// this.fCustomFiltersActionGroup= new CustomFiltersActionGroup(getId(), this.viewer);
this.treeViewer= viewer;
tree.addSelectionListener(new SelectionListener() {
@Override
public void widgetSelected(final SelectionEvent e) {
// do nothing
}
@Override
public void widgetDefaultSelected(final SelectionEvent e) {
gotoSelectedElement();
}
});
tree.addMouseMoveListener(new MouseMoveListener() {
TreeItem lastItem= null;
@Override
public void mouseMove(final MouseEvent e) {
if (tree.equals(e.getSource())) {
final Object o= tree.getItem(new Point(e.x, e.y));
if (this.lastItem == null ^ o == null) {
tree.setCursor(o == null ? null : tree.getDisplay().getSystemCursor(SWT.CURSOR_HAND));
}
if (o instanceof TreeItem) {
final Rectangle clientArea= tree.getClientArea();
if (!o.equals(this.lastItem)) {
this.lastItem= (TreeItem)o;
tree.setSelection(new TreeItem[] { this.lastItem });
} else if (e.y - clientArea.y < tree.getItemHeight() / 4) {
// Scroll up
final Point p= tree.toDisplay(e.x, e.y);
final Item item= QuickTreeInformationControl.this.treeViewer.scrollUp(p.x, p.y);
if (item instanceof TreeItem) {
this.lastItem= (TreeItem)item;
tree.setSelection(new TreeItem[] { this.lastItem });
}
} else if (clientArea.y + clientArea.height - e.y < tree.getItemHeight() / 4) {
// Scroll down
final Point p= tree.toDisplay(e.x, e.y);
final Item item= QuickTreeInformationControl.this.treeViewer.scrollDown(p.x, p.y);
if (item instanceof TreeItem) {
this.lastItem= (TreeItem)item;
tree.setSelection(new TreeItem[] { this.lastItem });
}
}
} else if (o == null) {
this.lastItem= null;
}
}
}
});
if (this.commandKeyListener != null) {
tree.addKeyListener(this.commandKeyListener);
}
tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(final MouseEvent e) {
if (tree.getSelectionCount() < 1) {
return;
}
if (e.button != 1) {
return;
}
if (tree.equals(e.getSource())) {
final Object o= tree.getItem(new Point(e.x, e.y));
final TreeItem selection= tree.getSelection()[0];
if (selection.equals(o)) {
gotoSelectedElement();
}
}
}
});
addDisposeListener(this);
return this.treeViewer.getControl();
}
protected abstract void configureViewer(TreeViewer viewer);
protected TreeViewer getTreeViewer() {
return this.treeViewer;
}
protected SearchText getFilterText() {
return this.filterText;
}
protected SearchText createFilterText(final Composite parent) {
this.filterText= new SearchText(parent, "", SWT.NONE); //$NON-NLS-1$
Dialog.applyDialogFont(this.filterText);
this.filterText.addListener(new SearchText.Listener() {
@Override
public void textChanged(final boolean user) {
setMatcherString(QuickTreeInformationControl.this.filterText.getText(), true);
}
@Override
public void okPressed() {
gotoSelectedElement();
}
@Override
public void downPressed() {
QuickTreeInformationControl.this.treeViewer.getTree().setFocus();
}
});
this.filterText.getTextControl().addListener(SWT.KeyDown, new Listener() {
@Override
public void handleEvent(final Event event) {
if (event.character == SWT.ESC) {
close();
event.doit= false;
}
}
});
if (this.commandKeyListener != null) {
this.filterText.getTextControl().addKeyListener(this.commandKeyListener);
}
return this.filterText;
}
protected void createHorizontalSeparator(final Composite parent) {
final Label separator= new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL | SWT.LINE_DOT);
separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
}
/**
* Sets the patterns to filter out for the receiver.
*
* @param pattern the pattern
* @param update <code>true</code> if the viewer should be updated
*/
protected void setMatcherString(final String pattern, final boolean update) {
if (this.nameFilter.setText(pattern) && update) {
stringMatcherUpdated();
}
}
/**
* The string matcher has been modified.
*
* The default implementation refreshes the view and selects the first matched element.
*/
protected void stringMatcherUpdated() {
// refresh viewer to re-filter
this.treeViewer.getControl().setRedraw(false);
this.finalNameFilter= this.nameFilter.getFinal(false);
this.treeViewer.refresh();
this.treeViewer.expandAll();
selectFirstMatch();
this.treeViewer.getControl().setRedraw(true);
}
/**
* Implementers can modify
*
* @return the selected element
*/
protected Object getSelectedElement() {
if (this.treeViewer == null) {
return null;
}
return ((IStructuredSelection) this.treeViewer.getSelection()).getFirstElement();
}
protected void gotoSelectedElement() {
final Object selectedElement= getSelectedElement();
if (selectedElement != null) {
try {
dispose();
openElement(selectedElement);
}
catch (final CoreException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, UIMiscellanyPlugin.BUNDLE_ID,
"An error occurred when opening the selected element.", e ));
}
}
}
protected abstract void openElement(final Object element) throws CoreException;
/**
* Selects the first element in the tree which matches the current filter pattern.
*/
protected void selectFirstMatch() {
final Tree tree= this.treeViewer.getTree();
final Object element= findFirstMatch(tree.getItems());
if (element != null) {
this.treeViewer.setSelection(new StructuredSelection(element), true);
} else {
this.treeViewer.setSelection(StructuredSelection.EMPTY);
}
}
private @Nullable Object findFirstMatch(final TreeItem[] items) {
if (items.length > 0) {
// Process each item in the tree
for (int i= 0; i < items.length; i++) {
final Object element= items[i].getData();
if (element == null) {
continue;
}
if (select(element)) {
return element;
}
}
for (int i= 0; i < items.length; i++) {
// Recursively check the elements children for a match
final Object element= findFirstMatch(items[i].getItems());
// Return the child element match if found
if (element != null) {
return element;
}
}
}
// No match found
return null;
}
@Override
public void setInformation(final String information) {
// this method is ignored, see IInformationControlExtension2
}
@Override
public abstract void setInput(Object information);
protected void inputChanged(final int iterationPage, final Object newInput,
final Object newSelection) {
this.filterText.clearText();
resetFilter();
this.iterationPosition= iterationPage;
updateInfoText();
this.treeViewer.setInput(newInput);
if (newSelection != null) {
this.treeViewer.setSelection(new StructuredSelection(newSelection));
}
}
protected ITextElementFilter createNameFilter() {
return new TextElementFilter();
}
protected void resetFilter() {
this.nameFilter.setText(null);
this.finalNameFilter= this.nameFilter.getFinal(true);
}
protected String getElementName(final IAdaptable element) {
final IBaseLabelProvider labelProvider= this.treeViewer.getLabelProvider();
if (labelProvider instanceof ILabelProvider) {
return ((ILabelProvider) labelProvider).getText(element);
}
return element.toString();
}
protected boolean select(final Object element) {
if (this.finalNameFilter == null) {
return true;
}
if (element instanceof IAdaptable) {
final IAdaptable adaptable= (IAdaptable) element;
final String name= getElementName(adaptable);
if (name != null) {
this.nameFilterElement.set(adaptable, name);
return this.finalNameFilter.select(this.nameFilterElement);
}
}
return false;
}
@Override
protected void fillDialogMenu(final IMenuManager menu) {
super.fillDialogMenu(menu);
menu.add(new Separator(VIEW_SORT_GROUP_ID));
menu.add(new Separator(VIEW_FILTER_GROUP_ID));
}
@Override
public void setVisible(final boolean visible) {
if (visible) {
open();
} else {
disableActions();
saveDialogBounds(getShell());
getShell().setVisible(false);
}
}
@Override
public int open() {
initActions();
return super.open();
}
@Override
public final void dispose() {
close();
}
@Override
public void widgetDisposed(final DisposeEvent event) {
disableActions();
this.treeViewer= null;
this.filterText= null;
}
/**
* Adds handler and key binding support.
*/
protected void initActions() {
// IWorkbenchCommandConstants.WINDOW_SHOW_VIEW_MENU -> showDialogMenu()
// // Register action with command support
// if (this.fShowViewMenuHandlerSubmission == null) {
// this.fShowViewMenuHandlerSubmission= new HandlerSubmission(null, getShell(), null, this.fShowViewMenuAction.getActionDefinitionId(), new ActionHandler(this.fShowViewMenuAction), Priority.MEDIUM);
// PlatformUI.getWorkbench().getCommandSupport().addHandlerSubmission(this.fShowViewMenuHandlerSubmission);
// }
}
/**
* Removes handler and key binding support.
*/
protected void disableActions() {
// // Remove handler submission
// if (this.fShowViewMenuHandlerSubmission != null) {
// PlatformUI.getWorkbench().getCommandSupport().removeHandlerSubmission(this.fShowViewMenuHandlerSubmission);
// }
}
@Override
public boolean hasContents() {
return (this.treeViewer != null && this.treeViewer.getInput() != null);
}
@Override
public void setSizeConstraints(final int maxWidth, final int maxHeight) {
// ignore
}
@Override
public Point computeSizeHint() {
//Rreturn the shell's size
// Note that it already has the persisted size if persisting is enabled.
return getShell().getSize();
}
@Override
public void setLocation(final Point location) {
/*
* If the location is persisted, it gets managed by PopupDialog - fine. Otherwise, the location is
* computed in Window#getInitialLocation, which will center it in the parent shell / main
* monitor, which is wrong for two reasons:
* - we want to center over the editor / subject control, not the parent shell
* - the center is computed via the initalSize, which may be also wrong since the size may
* have been updated since via min/max sizing of AbstractInformationControlManager.
* In that case, override the location with the one computed by the manager. Note that
* the call to constrainShellSize in PopupDialog.open will still ensure that the shell is
* entirely visible.
*/
if (!getPersistLocation() || getDialogSettings() == null) {
getShell().setLocation(location);
}
}
@Override
public void setSize(final int width, final int height) {
getShell().setSize(width, height);
}
@Override
public void addDisposeListener(final DisposeListener listener) {
getShell().addDisposeListener(listener);
}
@Override
public void removeDisposeListener(final DisposeListener listener) {
getShell().removeDisposeListener(listener);
}
@Override
public void setForegroundColor(final Color foreground) {
applyForegroundColor(foreground, getContents());
}
@Override
public void setBackgroundColor(final Color background) {
applyBackgroundColor(background, getContents());
}
@Override
public boolean isFocusControl() {
final Shell shell= getShell();
return (shell != null && shell.getDisplay().getActiveShell() == shell);
}
@Override
public void setFocus() {
getShell().forceFocus();
this.filterText.setFocus();
}
@Override
public void addFocusListener(final FocusListener listener) {
getShell().addFocusListener(listener);
}
@Override
public void removeFocusListener(final FocusListener listener) {
getShell().removeFocusListener(listener);
}
protected final String getCommandId() {
return this.commandId;
}
protected final int getIterationPosition() {
return this.iterationPosition;
}
private int getNextIterationPosition() {
final int page= this.iterationPosition + 1;
return (page < this.iterationCount) ? page : 0;
}
private void iterate() {
this.iterationPosition= getNextIterationPosition();
updateInfoText();
iterated(this.iterationPosition);
}
protected void iterated(final int iterationPosition) {
final Object selectedElement= ((IStructuredSelection) this.treeViewer.getSelection()).getFirstElement();
this.treeViewer.refresh();
if (selectedElement != null && this.treeViewer.getTree().getSelectionCount() == 0) {
// if tree path changed, try again
this.treeViewer.setSelection(new StructuredSelection(selectedElement), true);
}
}
@Override
protected Control createTitleControl(final Composite parent) {
this.filterText= createFilterText(parent);
this.filterText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
return this.filterText;
}
protected @Nullable IProgressMonitor getProgressMonitor() {
final IWorkbenchPart part= UIAccess.getActiveWorkbenchPart(true);
IEditorPart editor;
if (part instanceof IEditorPart) {
editor= (IEditorPart) part;
}
else {
return null;
}
return editor.getEditorSite().getActionBars().getStatusLineManager().getProgressMonitor();
}
}