/*******************************************************************************
* Copyright (c) 2000, 2012 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package com.redhat.ceylon.eclipse.code.outline;
import static org.eclipse.ui.IWorkbenchCommandConstants.WINDOW_SHOW_VIEW_MENU;
import java.util.StringTokenizer;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.text.IInformationControl;
import org.eclipse.jface.text.IInformationControlExtension;
import org.eclipse.jface.text.IInformationControlExtension2;
import org.eclipse.jface.text.IInformationControlExtension3;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.StyledString.Styler;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
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.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.TextStyle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
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.PlatformUI;
import org.eclipse.ui.commands.ActionHandler;
import org.eclipse.ui.commands.HandlerSubmission;
import org.eclipse.ui.commands.Priority;
import com.redhat.ceylon.eclipse.code.editor.CeylonEditor;
import com.redhat.ceylon.eclipse.ui.CeylonPlugin;
import com.redhat.ceylon.eclipse.util.EditorUtil;
/**
* Abstract class for Show hierarchy in light-weight controls.
*
* @since 2.1
*/
@SuppressWarnings("deprecation")
public abstract class TreeViewPopup extends PopupDialog
implements IInformationControl,
IInformationControlExtension,
IInformationControlExtension2,
IInformationControlExtension3,
DisposeListener {
private static GridLayoutFactory popupLayoutFactory;
protected static GridLayoutFactory getPopupLayout() {
if (popupLayoutFactory == null) {
popupLayoutFactory =
GridLayoutFactory.fillDefaults()
.margins(POPUP_MARGINWIDTH, POPUP_MARGINHEIGHT)
.spacing(POPUP_HORIZONTALSPACING, POPUP_VERTICALSPACING);
}
return popupLayoutFactory;
}
// protected CeylonEditor editor;
private Text filterText;
private TreeViewer treeViewer;
//protected JavaElementPrefixPatternMatcher fPatternMatcher;
/**
* Fields that support the dialog menu
* @since 3.0
* @since 3.2 - now appended to framework menu
*/
private Composite viewMenuButtonComposite;
private IAction showViewMenuAction;
private HandlerSubmission showViewMenuHandlerSubmission;
private StyledText titleLabel;
private TriggerSequence commandBinding;
protected TriggerSequence getCommandBinding() {
return commandBinding;
}
private final CeylonEditor editor; //has to be defined here because it is used from create() called by super constructor!
/**
* 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 invokingCommandId the id of the command that invoked this control or <code>null</code>
* @param showStatusField <code>true</code> iff the control has a status field at the bottom
*/
public TreeViewPopup(Shell parent, int shellStyle,
String invokingCommandId, CeylonEditor editor) {
super(parent, shellStyle, true, true, false, true, true, null, null);
this.editor = editor; //has to be initialized here because it is used from create() called by super constructor!
if (invokingCommandId != null) {
commandBinding =
EditorUtil.getCommandBinding(invokingCommandId);
}
// Title and status text must be set to get the title label created, so force empty values here.
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 Control createContents(Composite parent) {
Composite composite =
(Composite)
super.createContents(parent);
GridLayout layout =
(GridLayout)
composite.getLayout();
layout.verticalSpacing=8;
layout.marginLeft=8;
layout.marginRight=8;
layout.marginTop=8;
layout.marginBottom=8;
Control[] children = composite.getChildren();
// children[2].setVisible(false);
children[children.length-2].setVisible(false);
return composite;
}
/**
* 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(Composite parent) {
treeViewer = createTreeViewer(parent);
treeViewer.setAutoExpandLevel(getDefaultLevel());
//fTreeViewer.setUseHashlookup(true);
//fCustomFiltersActionGroup= new CustomFiltersActionGroup(getId(), fTreeViewer);
final Tree tree = treeViewer.getTree();
tree.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
char character = e.character;
int keyCode = e.keyCode;
int stateMask = e.stateMask;
if (stateMask==SWT.NONE || stateMask==SWT.SHIFT) {
if (Character.isLetter(character)
&& keyCode==Character.toLowerCase(character)) {
String string = filterText.getText() + character;
filterText.setText(string);
filterText.setFocus();
filterText.setSelection(string.length());
}
if (character == SWT.BS && keyCode==SWT.BS) {
String string = filterText.getText();
if (string.length()>0) {
string = string.substring(0,string.length()-1);
}
filterText.setText(string);
filterText.setFocus();
filterText.setSelection(string.length());
}
}
if (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 TreeViewMouseListener(treeViewer));
tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
if (tree.getSelectionCount()>=0 &&
e.button==1 &&
tree.equals(e.getSource())) {
Object o = tree.getItem(new Point(e.x, e.y));
TreeItem[] results = tree.getSelection();
if (results.length>0) {
TreeItem selection = results[0];
if (selection.equals(o)) {
gotoSelectedElement();
}
}
}
}
});
installFilter();
addDisposeListener(this);
return treeViewer.getControl();
}
protected abstract TreeViewer createTreeViewer(Composite parent);
/**
* Returns the name of the dialog settings section.
*
* @return the name of the dialog settings section
*/
protected abstract String getId();
protected TreeViewer getTreeViewer() {
return treeViewer;
}
protected Text getFilterText() {
return filterText;
}
protected Text createFilterText(Composite parent) {
filterText = new Text(parent, SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL);
filterText.setMessage("type filter text");
Dialog.applyDialogFont(filterText);
GridData data = new GridData(GridData.FILL_HORIZONTAL);
data.horizontalAlignment = GridData.FILL;
data.verticalAlignment = GridData.CENTER;
filterText.setLayoutData(data);
filterText.addKeyListener(createViewerKeyListener());
return filterText;
}
protected KeyListener createViewerKeyListener() {
return new KeyListener() {
public void keyPressed(KeyEvent e) {
char character = e.character;
int keyCode = e.keyCode;
if (keyCode == 0x0D || keyCode == SWT.KEYPAD_CR) // Enter key
gotoSelectedElement();
if (keyCode == SWT.ARROW_DOWN)
treeViewer.getTree().setFocus();
if (keyCode == SWT.ARROW_UP)
treeViewer.getTree().setFocus();
if (character == 0x1B) // ESC
dispose();
}
public void keyReleased(KeyEvent e) {
// do nothing
}
};
}
protected void updateStatusFieldText() {
setInfoText(getStatusFieldText());
}
protected String getStatusFieldText() {
return "";
}
protected void installFilter() {
filterText.setText("");
filterText.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
Text input = (Text) e.widget;
String text = input.getText();
setMatcherString(text, true);
}
});
}
protected void reveal() {
treeViewer.expandToLevel(getDefaultLevel());
}
protected int getDefaultLevel() {
// return ALL_LEVELS;
return 4;
}
/**
* 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 JavaElementPrefixPatternMatcher(pattern);
}*/
if (update) {
update();
}
}
protected void update() {
if (!getShell().isDisposed()) {
treeViewer.getControl()
.setRedraw(false);
// refresh viewer to re-filter
treeViewer.refresh();
reveal();
//fTreeViewer.expandAll();
selectFirstMatch(); //TODO select the main declaration instead!
treeViewer.getControl()
.setRedraw(true);
}
}
/**
* Implementers can modify
*
* @return the selected element
*/
protected Object getSelectedElement() {
if (treeViewer == null) {
return null;
}
else {
IStructuredSelection selection =
(IStructuredSelection)
treeViewer.getSelection();
return selection.getFirstElement();
}
}
protected abstract void gotoSelectedElement();
/**
* Selects the first element in the tree which
* matches the current filter pattern.
*/
protected void selectFirstMatch() {
//Object selectedElement= fTreeViewer.testFindItem(fInitiallySelectedType);
TreeItem element;
Tree tree = treeViewer.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 {
treeViewer.setSelection(StructuredSelection.EMPTY);
}
}
private TreeItem findElement(TreeItem[] items) {
return findElement(items, null, true);
}
private TreeItem findElement(TreeItem[] items, TreeItem[] toBeSkipped,
boolean allowToGoUp) {
return items.length > 0 ? items[0] : null;
//TODO: reenable filtering
/*if (fPatternMatcher == null)
return items.length > 0 ? items[0] : null;
ILabelProvider labelProvider= (ILabelProvider)fTreeViewer.getLabelProvider();
// First search at same level
for (int i= 0; i < items.length; i++) {
final TreeItem item= items[i];
IJavaElement element= (IJavaElement)item.getData();
if (element != null) {
String label= labelProvider.getText(element);
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;
}*/
public void setInformation(String information) {
// this method is ignored, see IInformationControlExtension2
}
public abstract void setInput(Object information);
/**
* 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 void inputChanged(Object newInput, Object newSelection) {
treeViewer.setInput(newInput);
if (newSelection!=null) {
treeViewer.setSelection(new StructuredSelection(newSelection));
}
filterText.setText("");
}
public void setVisible(boolean visible) {
if (visible) {
open();
}
else {
removeHandlerAndKeyBindingSupport();
saveDialogBounds(getShell());
getShell().setVisible(false);
}
}
@Override
public int open() {
addHandlerAndKeyBindingSupport();
return super.open();
}
public final void dispose() {
close();
}
public void widgetDisposed(DisposeEvent event) {
removeHandlerAndKeyBindingSupport();
treeViewer = null;
filterText = null;
}
protected void addHandlerAndKeyBindingSupport() {
// Register action with command support
if (showViewMenuHandlerSubmission == null) {
showViewMenuHandlerSubmission= new HandlerSubmission(null, getShell(),
null, showViewMenuAction.getActionDefinitionId(),
new ActionHandler(showViewMenuAction), Priority.MEDIUM);
PlatformUI.getWorkbench().getCommandSupport()
.addHandlerSubmission(showViewMenuHandlerSubmission);
}
}
protected void removeHandlerAndKeyBindingSupport() {
// Remove handler submission
if (showViewMenuHandlerSubmission != null) {
PlatformUI.getWorkbench().getCommandSupport()
.removeHandlerSubmission(showViewMenuHandlerSubmission);
}
}
public boolean hasContents() {
return treeViewer != null &&
treeViewer.getInput() != null;
}
public void setSizeConstraints(int maxWidth, int maxHeight) {
// ignore
}
public Point computeSizeHint() {
// return the shell's size - note that it already has the persisted size if persisting
// is enabled.
return getShell().getSize();
}
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);
}
}
public void setSize(int width, int height) {
getShell().setSize(width, height);
}
public void addDisposeListener(DisposeListener listener) {
getShell().addDisposeListener(listener);
}
public void removeDisposeListener(DisposeListener listener) {
getShell().removeDisposeListener(listener);
}
public void setForegroundColor(Color foreground) {
applyForegroundColor(foreground, getContents());
}
public void setBackgroundColor(Color background) {
applyBackgroundColor(background, getContents());
}
public boolean isFocusControl() {
return getShell() ==
getShell()
.getDisplay()
.getActiveShell();
}
public void setFocus() {
getShell().forceFocus();
filterText.setFocus();
}
public void addFocusListener(FocusListener listener) {
getShell().addFocusListener(listener);
}
public void removeFocusListener(FocusListener listener) {
getShell().removeFocusListener(listener);
}
@Override
protected IDialogSettings getDialogSettings() {
String section = getId();
IDialogSettings dialogSettings =
CeylonPlugin.getInstance()
.getDialogSettings();
IDialogSettings settings =
dialogSettings.getSection(section);
if (settings == null) {
settings = dialogSettings.addNewSection(section);
}
return settings;
}
@Override
protected Control createTitleMenuArea(Composite parent) {
viewMenuButtonComposite =
(Composite)
super.createTitleMenuArea(parent);
// the filter text must be created
// underneath the title and menu area.
filterText = createFilterText(parent);
// Create show view menu action
showViewMenuAction =
new Action("showViewMenu") {
@Override
public void run() {
showDialogMenu();
}
};
showViewMenuAction.setEnabled(true);
showViewMenuAction.setActionDefinitionId(WINDOW_SHOW_VIEW_MENU);
return viewMenuButtonComposite;
}
protected StyledString styleTitle(final StyledText title) {
StyledString result = new StyledString();
StringTokenizer tokens =
new StringTokenizer(title.getText(),
"\u2014", false);
styleDescription(title, result, tokens.nextToken());
result.append("\u2014").append(tokens.nextToken());
return result;
}
protected void styleDescription(StyledText title, StyledString result,
String desc) {
final Display display = title.getDisplay();
final FontData[] fontDatas =
title.getFont()
.getFontData();
for (int i=0; i<fontDatas.length; i++) {
fontDatas[i].setStyle(SWT.BOLD);
}
result.append(desc, new Styler() {
@Override
public void applyStyles(TextStyle textStyle) {
textStyle.font = new Font(display, fontDatas);
}
});
}
@Override
protected Control createTitleControl(Composite parent) {
titleLabel = new StyledText(parent, SWT.NONE);
titleLabel.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
StyleRange[] styleRanges =
styleTitle(titleLabel)
.getStyleRanges();
titleLabel.setStyleRanges(styleRanges);
}
});
titleLabel.setEditable(false);
GridDataFactory.fillDefaults()
.align(SWT.FILL, SWT.CENTER)
.grab(true,false)
.span(1, 1)
.applyTo(titleLabel);
return null;
}
@Override
protected void setTitleText(String text) {
if (titleLabel!=null) {
titleLabel.setText(text);
}
}
@Override
protected void setTabOrder(Composite composite) {
composite.setTabList(new Control[] {
filterText,
treeViewer.getTree()
});
}
@Override
public boolean restoresLocation() {
return getPersistLocation();
}
@Override
public boolean restoresSize() {
return getPersistSize();
}
@Override
public Rectangle getBounds() {
return getShell().getBounds();
}
@Override
public Rectangle computeTrim() {
return getShell().computeTrim(0, 0, 0, 0);
}
protected CeylonEditor getEditor() {
return editor;
}
}