package com.anjlab.eclipse.e4.tapestry5.handlers;
import org.eclipse.jdt.ui.actions.CustomFiltersActionGroup;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
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.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ISelection;
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.TextProcessor;
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.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
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.Item;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IWorkbenchCommandConstants;
import com.anjlab.eclipse.tapestry5.Openable;
import com.anjlab.eclipse.tapestry5.views.NameSorter;
import com.anjlab.eclipse.tapestry5.views.TapestryDecoratingLabelProvider;
import com.anjlab.eclipse.tapestry5.views.TreeObject;
import com.anjlab.eclipse.tapestry5.views.TreeObjectDoubleClickListener;
import com.anjlab.eclipse.tapestry5.views.TreeParent;
/**
* Inspired by org.eclipse.jdt.internal.ui.text.JavaOutlineInformationControl
*
* @author dmitrygusev
*
*/
public abstract class AbstractTapestryContextInformation extends PopupDialog
implements IInformationControl, IInformationControlExtension,
IInformationControlExtension2, DisposeListener
{
protected interface ContentProviderCreator
{
ITreeContentProvider createContentProvider();
}
/**
* The NamePatternFilter selects the elements which match the given string
* patterns.
*
* @since 2.0
*/
protected class NamePatternFilter extends ViewerFilter
{
/*
* @see org.eclipse.jface.viewers.ViewerFilter#select(org.eclipse.jface.
* viewers.Viewer, java.lang.Object, java.lang.Object)
*/
@Override
public boolean select(Viewer viewer, Object parentElement, Object element)
{
PatternMatcher matcher = getMatcher();
if (matcher == null || !(viewer instanceof TreeViewer))
{
return true;
}
TreeViewer treeViewer = (TreeViewer) viewer;
String matchName = getText(element, treeViewer);
matchName = TextProcessor.deprocess(matchName);
if (matchName != null
&& matcher.matches(matchName)
&& (isOpenable(element)
|| !(element instanceof TreeParent)))
{
return true;
}
return hasUnfilteredChild(treeViewer, element);
}
private boolean isOpenable(Object element)
{
return ((TreeObject) element).getData() instanceof Openable;
}
private boolean hasUnfilteredChild(TreeViewer viewer, Object element)
{
if (element instanceof TreeParent)
{
Object[] children = ((ITreeContentProvider) viewer.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 Text fFilterText;
/** The control's tree widget */
private TreeViewer fTreeViewer;
/** The current string matcher */
private PatternMatcher fPatternMatcher;
/**
* Fields that support the dialog menu
*
* @since 3.0
* @since 3.2 - now appended to framework menu
*/
private Composite fViewMenuButtonComposite;
private CustomFiltersActionGroup fCustomFiltersActionGroup;
private IAction fShowViewMenuAction;
private IDoubleClickListener gotoListener = new TreeObjectDoubleClickListener();
private ContentProviderCreator contentProviderCreator;
public AbstractTapestryContextInformation(Shell parent, ContentProviderCreator contentProviderCreator)
{
super(parent, SWT.RESIZE, true, true, false, true, true, null, null);
this.contentProviderCreator = contentProviderCreator;
// Title and status text must be set to get the title label created,
// so force empty values here.
if (hasHeader())
setTitleText("");
setInfoText("");
// Create all controls early to preserve the life cycle of the original
// implementation.
create();
// Status field text can only be computed after widgets are created.
setInfoText(getStatusFieldText());
}
protected String getStatusFieldText()
{
return "";
}
protected String getText(Object element, TreeViewer treeViewer)
{
return ((org.eclipse.jface.viewers.DecoratingStyledCellLabelProvider) treeViewer
.getLabelProvider()).getStyledStringProvider()
.getStyledText(element).toString();
}
/**
* Fills the view menu. Clients can extend or override.
*
* @param viewMenu
* the menu manager that manages the menu
* @since 3.0
*/
protected void fillViewMenu(IMenuManager viewMenu)
{
fCustomFiltersActionGroup.fillViewMenu(viewMenu);
}
@Override
protected void fillDialogMenu(IMenuManager dialogMenu)
{
super.fillDialogMenu(dialogMenu);
fillViewMenu(dialogMenu);
}
protected PatternMatcher getMatcher()
{
return fPatternMatcher;
}
@Override
protected Control createTitleMenuArea(Composite parent)
{
fViewMenuButtonComposite = (Composite) super.createTitleMenuArea(
parent);
// If there is a header, then the filter text must be created
// underneath the title and menu area.
if (hasHeader())
{
fFilterText = createFilterText(parent);
}
// Create show view menu action
fShowViewMenuAction = new Action("showViewMenu") //$NON-NLS-1$
{
/*
* @see org.eclipse.jface.action.Action#run()
*/
@Override
public void run()
{
showDialogMenu();
}
};
fShowViewMenuAction.setEnabled(true);
fShowViewMenuAction.setActionDefinitionId(IWorkbenchCommandConstants.WINDOW_SHOW_VIEW_MENU);
return fViewMenuButtonComposite;
}
/**
* Returns <code>true</code> if the control has a header, <code>false</code>
* otherwise.
* <p>
* The default is to return <code>false</code>.
* </p>
*
* @return <code>true</code> if the control has a header
*/
protected boolean hasHeader()
{
// default is to have no header
return false;
}
@Override
protected Control createTitleControl(Composite parent)
{
if (hasHeader())
{
return super.createTitleControl(parent);
}
fFilterText = createFilterText(parent);
return fFilterText;
}
@Override
protected void setTabOrder(Composite composite)
{
if (hasHeader())
{
composite.setTabList(
new Control[] { fFilterText, fTreeViewer.getTree() });
} else
{
fViewMenuButtonComposite.setTabList(new Control[] { fFilterText });
composite.setTabList(new Control[] { fViewMenuButtonComposite,
fTreeViewer.getTree() });
}
}
protected Text createFilterText(Composite parent)
{
fFilterText = new Text(parent, SWT.NONE);
Dialog.applyDialogFont(fFilterText);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
data.horizontalAlignment = GridData.FILL;
data.verticalAlignment = GridData.CENTER;
fFilterText.setLayoutData(data);
fFilterText.addKeyListener(new KeyListener()
{
public void keyPressed(KeyEvent e)
{
if (e.keyCode == 0x0D || e.keyCode == SWT.KEYPAD_CR)
{
// Enter key
gotoSelectedElement();
}
else if (e.keyCode == SWT.ARROW_DOWN)
{
fTreeViewer.getTree().setFocus();
}
else if (e.keyCode == SWT.ARROW_UP)
{
fTreeViewer.getTree().setFocus();
}
else if (e.character == 0x1B)
{
// ESC
dispose();
}
}
public void keyReleased(KeyEvent e)
{
// do nothing
}
});
return fFilterText;
}
private void gotoSelectedElement()
{
ISelection selection = fTreeViewer.getSelection();
if (selection != null && !selection.isEmpty())
{
DoubleClickEvent event = new DoubleClickEvent(fTreeViewer, selection);
dispose();
gotoListener.doubleClick(event);
}
}
private void installFilter()
{
fFilterText.setText(""); //$NON-NLS-1$
fFilterText.addModifyListener(new ModifyListener()
{
public void modifyText(ModifyEvent e)
{
String text = ((Text) e.widget).getText();
setMatcherString(text, true);
}
});
}
/**
* 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(String pattern, boolean update)
{
if (pattern.length() == 0)
{
fPatternMatcher = null;
}
else
{
fPatternMatcher = new SimplePatternMatcher(pattern);
}
if (update)
{
stringMatcherUpdated();
}
}
/**
* Selects the first element in the tree which matches the current filter
* pattern.
*/
protected void selectFirstMatch()
{
Object selectedElement = fTreeViewer.testFindItem(null);
TreeItem element;
final Tree tree = fTreeViewer.getTree();
if (selectedElement instanceof TreeItem)
{
element = findElement(new TreeItem[] { (TreeItem) selectedElement });
}
else
{
element = findElement(tree.getItems());
}
if (element != null)
{
tree.setSelection(element);
tree.showItem(element);
}
else
{
fTreeViewer.setSelection(StructuredSelection.EMPTY);
}
}
private TreeItem findElement(TreeItem[] items)
{
return findElement(items, null, true);
}
private TreeItem findElement(TreeItem[] items, TreeItem[] toBeSkipped, boolean allowToGoUp)
{
if (fPatternMatcher == null)
{
return items.length > 0 ? items[0] : null;
}
// First search at same level
for (int i = 0; i < items.length; i++)
{
final TreeItem item = items[i];
Object element = item.getData();
if (element != null)
{
String label = getText(element, fTreeViewer);
if (fPatternMatcher.matches(label))
{
return item;
}
}
}
// Go one level down for each item
for (int i = 0; i < items.length; i++)
{
final TreeItem item = items[i];
TreeItem foundItem = findElement(
selectItems(item.getItems(), toBeSkipped), null, false);
if (foundItem != null)
{
return foundItem;
}
}
if (!allowToGoUp || items.length == 0)
{
return null;
}
// Go one level up (parent is the same for all items)
TreeItem parentItem = items[0].getParentItem();
if (parentItem != null)
{
return findElement(new TreeItem[] { parentItem }, items, true);
}
// Check root elements
return findElement(selectItems(items[0].getParent().getItems(), items), null, false);
}
private boolean canSkip(TreeItem item, TreeItem[] toBeSkipped)
{
if (toBeSkipped == null)
{
return false;
}
for (int i = 0; i < toBeSkipped.length; i++)
{
if (toBeSkipped[i] == item)
{
return true;
}
}
return false;
}
private TreeItem[] selectItems(TreeItem[] items, TreeItem[] toBeSkipped)
{
if (toBeSkipped == null || toBeSkipped.length == 0)
{
return items;
}
int j = 0;
for (int i = 0; i < items.length; i++)
{
TreeItem item = items[i];
if (!canSkip(item, toBeSkipped))
{
items[j++] = item;
}
}
if (j == items.length)
{
return items;
}
TreeItem[] result = new TreeItem[j];
System.arraycopy(items, 0, result, 0, j);
return result;
}
/**
* 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
fTreeViewer.getControl().setRedraw(false);
fTreeViewer.refresh();
fTreeViewer.expandAll();
selectFirstMatch();
fTreeViewer.getControl().setRedraw(true);
}
@Override
protected Control createDialogArea(Composite parent)
{
fTreeViewer = createTreeViewer(parent);
fCustomFiltersActionGroup = new CustomFiltersActionGroup(getId(), fTreeViewer);
final Tree tree = fTreeViewer.getTree();
tree.addKeyListener(new KeyListener()
{
public void keyPressed(KeyEvent e)
{
if (e.character == 0x1B)
{
// ESC
dispose();
}
}
public void keyReleased(KeyEvent e)
{
// do nothing
}
});
tree.addSelectionListener(new SelectionListener()
{
public void widgetSelected(SelectionEvent e)
{
// do nothing
}
public void widgetDefaultSelected(SelectionEvent e)
{
gotoSelectedElement();
}
});
tree.addMouseMoveListener(new MouseMoveListener()
{
TreeItem fLastItem = null;
public void mouseMove(MouseEvent e)
{
if (tree.equals(e.getSource()))
{
Object o = tree.getItem(new Point(e.x, e.y));
if (fLastItem == null ^ o == null)
{
tree.setCursor(o == null
? null
: tree.getDisplay()
.getSystemCursor(SWT.CURSOR_HAND));
}
if (o instanceof TreeItem)
{
Rectangle clientArea = tree.getClientArea();
if (!o.equals(fLastItem))
{
fLastItem = (TreeItem) o;
tree.setSelection(new TreeItem[] { fLastItem });
}
else if (e.y - clientArea.y < tree.getItemHeight() / 4)
{
// Scroll up
Point p = tree.toDisplay(e.x, e.y);
Item item = fTreeViewer.scrollUp(p.x, p.y);
if (item instanceof TreeItem)
{
fLastItem = (TreeItem) item;
tree.setSelection(new TreeItem[] { fLastItem });
}
}
else if (clientArea.y + clientArea.height - e.y < tree.getItemHeight() / 4)
{
// Scroll down
Point p = tree.toDisplay(e.x, e.y);
Item item = fTreeViewer.scrollDown(p.x, p.y);
if (item instanceof TreeItem)
{
fLastItem = (TreeItem) item;
tree.setSelection(new TreeItem[] { fLastItem });
}
}
}
else if (o == null)
{
fLastItem = null;
}
}
}
});
tree.addMouseListener(new MouseAdapter()
{
@Override
public void mouseUp(MouseEvent e)
{
if (tree.getSelectionCount() < 1)
return;
if (e.button != 1)
return;
if (tree.equals(e.getSource()))
{
Object o = tree.getItem(new Point(e.x, e.y));
TreeItem selection = tree.getSelection()[0];
if (selection.equals(o))
{
gotoSelectedElement();
}
}
}
});
installFilter();
addDisposeListener(this);
return fTreeViewer.getControl();
}
private String getId()
{
return TapestryContextInformationControl.class.getName();
}
public void addDisposeListener(DisposeListener listener)
{
getShell().addDisposeListener(listener);
}
protected TreeViewer createTreeViewer(Composite parent)
{
int style = SWT.V_SCROLL | SWT.H_SCROLL;
TreeViewer treeViewer = new TreeViewer(parent,
SWT.SINGLE | (style & ~SWT.MULTI));
// Hard-coded filters
treeViewer.addFilter(new NamePatternFilter());
treeViewer.setContentProvider(getContentProvider());
treeViewer.setLabelProvider(new TapestryDecoratingLabelProvider());
treeViewer.setSorter(new NameSorter());
treeViewer.setAutoExpandLevel(AbstractTreeViewer.ALL_LEVELS);
treeViewer.setInput(this);
Tree tree = treeViewer.getTree();
GridData gd = new GridData(GridData.FILL_BOTH);
gd.heightHint = tree.getItemHeight() * 12;
tree.setLayoutData(gd);
return treeViewer;
}
/**
* {@inheritDoc}
*
* @param event
* can be null
* <p>
* Subclasses may extend.
* </p>
*/
public void widgetDisposed(DisposeEvent event)
{
fTreeViewer = null;
fFilterText = null;
}
@Override
public void setInput(Object input)
{
if (input != null)
{
fTreeViewer.setSelection(
new StructuredSelection(new TreeObject("", input)));
}
}
/**
* {@inheritDoc}
*/
public void setInformation(String information)
{
// this method is ignored, see IInformationControlExtension2
}
/**
* {@inheritDoc}
*/
public void setVisible(boolean visible)
{
if (visible)
{
open();
}
else
{
saveDialogBounds(getShell());
getShell().setVisible(false);
}
}
/**
* {@inheritDoc}
*/
public boolean hasContents()
{
return fTreeViewer != null && fTreeViewer.getInput() != null;
}
/**
* {@inheritDoc}
*/
public void setSizeConstraints(int maxWidth, int maxHeight)
{
// ignore
}
/**
* {@inheritDoc}
*/
public Point computeSizeHint()
{
// return the shell's size - note that it already has the persisted size
// if persisting
// is enabled.
return getShell().getSize();
}
/**
* {@inheritDoc}
*/
public void setLocation(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);
}
}
/**
* {@inheritDoc}
*/
public void setSize(int width, int height)
{
getShell().setSize(width, height);
}
/**
* {@inheritDoc}
*/
public void removeDisposeListener(DisposeListener listener)
{
getShell().removeDisposeListener(listener);
}
/**
* {@inheritDoc}
*/
public void setForegroundColor(Color foreground)
{
applyForegroundColor(foreground, getContents());
}
/**
* {@inheritDoc}
*/
public void setBackgroundColor(Color background)
{
applyBackgroundColor(background, getContents());
}
/**
* {@inheritDoc}
*/
public boolean isFocusControl()
{
return getShell().getDisplay().getActiveShell() == getShell();
}
/**
* {@inheritDoc}
*/
public void setFocus()
{
getShell().forceFocus();
fFilterText.setFocus();
}
/**
* {@inheritDoc}
*/
public void addFocusListener(FocusListener listener)
{
getShell().addFocusListener(listener);
}
/**
* {@inheritDoc}
*/
public void removeFocusListener(FocusListener listener)
{
getShell().removeFocusListener(listener);
}
/**
* {@inheritDoc}
*/
public final void dispose()
{
close();
}
private ITreeContentProvider getContentProvider()
{
return contentProviderCreator.createContentProvider();
}
}