/*******************************************************************************
* Copyright (c) 2006, 2011 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
* Tasktop Technologies - adapted for Mylyn
* Frank Becker - adapted for Mylyn Task Editor
*******************************************************************************/
package org.eclipse.mylyn.internal.tasks.ui.editors.outline;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.Separator;
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.ILabelDecorator;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.IOpenListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.OpenEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.mylyn.commons.workbench.DecoratingPatternStyledCellLabelProvider;
import org.eclipse.mylyn.internal.tasks.ui.editors.TaskEditorOutlineContentProvider;
import org.eclipse.mylyn.internal.tasks.ui.editors.TaskEditorOutlineModel;
import org.eclipse.mylyn.internal.tasks.ui.editors.TaskEditorOutlineNode;
import org.eclipse.mylyn.tasks.ui.editors.AbstractTaskEditorPage;
import org.eclipse.mylyn.tasks.ui.editors.TaskEditor;
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.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.MouseListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
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.Text;
import org.eclipse.swt.widgets.Tree;
import org.eclipse.swt.widgets.TreeItem;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.dialogs.PatternFilter;
import org.eclipse.ui.forms.editor.IFormPage;
/**
* @author Mik Kersten
* @author Frank Becker
* @author Steffen Pingel
*/
public class QuickOutlineDialog extends PopupDialog implements IInformationControl, IInformationControlExtension,
IInformationControlExtension2, DisposeListener {
public final class Filter extends PatternFilter {
@Override
protected boolean wordMatches(String text) {
return super.wordMatches(text);
}
}
public final class TaskEditorOutlineLabelDecorator implements ILabelDecorator {
public String decorateText(String text, Object element) {
if (element instanceof TaskEditorOutlineNode) {
TaskEditorOutlineNode node = (TaskEditorOutlineNode) element;
if (node.getTaskRelation() != null) {
return NLS.bind(Messages.QuickOutlineDialog_Node_Label_Decoration, text, node.getTaskRelation()
.toString());
}
}
return null;
}
public void addListener(ILabelProviderListener listener) {
// ignore
}
public void dispose() {
// ignore
}
public boolean isLabelProperty(Object element, String property) {
return false;
}
public void removeListener(ILabelProviderListener listener) {
// ignore
}
public Image decorateImage(Image image, Object element) {
return null;
}
}
private class OpenListener implements IOpenListener, IDoubleClickListener, MouseListener {
private final Viewer viewer;
public OpenListener(Viewer viewer) {
this.viewer = viewer;
}
public void mouseDoubleClick(MouseEvent e) {
setSelection(e);
}
public void mouseDown(MouseEvent e) {
setSelection(e);
}
public void mouseUp(MouseEvent e) {
// ignore
}
public void doubleClick(DoubleClickEvent event) {
open(null);
}
public void open(OpenEvent event) {
AbstractTaskEditorPage taskEditorPage = getTaskEditorPage();
if (taskEditorPage == null) {
return;
}
StructuredSelection selection = (StructuredSelection) viewer.getSelection();
Object select = (selection).getFirstElement();
taskEditorPage.selectReveal(select);
}
private void setSelection(MouseEvent event) {
try {
Object selection = ((Tree) event.getSource()).getSelection()[0].getData();
viewer.setSelection(new StructuredSelection(selection));
open(null);
} catch (Exception e) {
// ignore
}
}
}
public static final String ID_VIEWER = "org.eclipse.mylyn.internal.tasks.ui.taskdata.quick"; //$NON-NLS-1$
private TreeViewer viewer;
private Text filterText;
private Filter namePatternFilter;
private OpenListener openListener;
private final IWorkbenchWindow window;
public QuickOutlineDialog(IWorkbenchWindow window) {
super(window.getShell(), SWT.RESIZE, true, true, true, true, true, null, null);
this.window = window;
setInfoText(Messages.QuickOutlineDialog_Press_Esc_Info_Text);
create();
}
@Override
public boolean close() {
// nothing additional to dispose
return super.close();
}
@Override
protected Control createDialogArea(Composite parent) {
createViewer(parent);
createUIListenersTreeViewer();
addDisposeListener(this);
return viewer.getControl();
}
private void createViewer(Composite parent) {
Control composite = super.createDialogArea(parent);
viewer = createCommonViewer((Composite) composite);
openListener = new OpenListener(viewer);
viewer.addOpenListener(openListener);
viewer.getTree().addMouseListener(openListener);
namePatternFilter = new Filter();
namePatternFilter.setIncludeLeadingWildcard(true);
viewer.addFilter(namePatternFilter);
AbstractTaskEditorPage taskEditorPage = getTaskEditorPage();
if (taskEditorPage != null) {
try {
viewer.getControl().setRedraw(false);
TaskEditorOutlineNode root = TaskEditorOutlineNode.parse(taskEditorPage.getModel().getTaskData(), true);
viewer.setInput(new TaskEditorOutlineModel(root));
viewer.expandAll();
TaskEditorOutlineNode attributesNode = root.getChild(TaskEditorOutlineNode.LABEL_ATTRIBUTES);
if (attributesNode != null) {
viewer.collapseToLevel(attributesNode, AbstractTreeViewer.ALL_LEVELS);
}
} finally {
viewer.getControl().setRedraw(true);
}
}
}
protected TreeViewer createCommonViewer(Composite parent) {
TreeViewer viewer = new TreeViewer(parent, SWT.H_SCROLL | SWT.V_SCROLL);
viewer.setUseHashlookup(true);
viewer.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
viewer.setContentProvider(new TaskEditorOutlineContentProvider());
viewer.setLabelProvider(new DecoratingPatternStyledCellLabelProvider(new QuickOutlineLabelProvider(),
new TaskEditorOutlineLabelDecorator(), null));
return viewer;
}
@Override
protected void fillDialogMenu(IMenuManager dialogMenu) {
dialogMenu.add(new Separator());
super.fillDialogMenu(dialogMenu);
}
private void createUIListenersTreeViewer() {
final Tree tree = viewer.getTree();
tree.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
if (e.character == 0x1B) {
// Dispose on ESC key press
dispose();
}
}
public void keyReleased(KeyEvent e) {
// ignore
}
});
tree.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
handleTreeViewerMouseUp(tree, e);
}
});
tree.addSelectionListener(new SelectionListener() {
public void widgetSelected(SelectionEvent e) {
// ignore
}
public void widgetDefaultSelected(SelectionEvent e) {
gotoSelectedElement();
}
});
}
private void handleTreeViewerMouseUp(final Tree tree, MouseEvent e) {
if ((tree.getSelectionCount() < 1) || (e.button != 1) || (tree.equals(e.getSource()) == false)) {
return;
}
// Selection is made in the selection changed listener
Object object = tree.getItem(new Point(e.x, e.y));
TreeItem selection = tree.getSelection()[0];
if (selection.equals(object)) {
gotoSelectedElement();
}
}
private Object getSelectedElement() {
if (viewer == null) {
return null;
}
return ((IStructuredSelection) viewer.getSelection()).getFirstElement();
}
public void addDisposeListener(DisposeListener listener) {
getShell().addDisposeListener(listener);
}
public void addFocusListener(FocusListener listener) {
getShell().addFocusListener(listener);
}
public Point computeSizeHint() {
// Note that it already has the persisted size if persisting is enabled.
return getShell().getSize();
}
public void dispose() {
close();
}
@Override
protected Point getDefaultSize() {
return new Point(400, 300);
}
public boolean isFocusControl() {
if (viewer.getControl().isFocusControl() || filterText.isFocusControl()) {
return true;
}
return false;
}
public void removeDisposeListener(DisposeListener listener) {
getShell().removeDisposeListener(listener);
}
public void removeFocusListener(FocusListener listener) {
getShell().removeFocusListener(listener);
}
public void setBackgroundColor(Color background) {
applyBackgroundColor(background, getContents());
}
public void setFocus() {
getShell().forceFocus();
filterText.setFocus();
}
public void setForegroundColor(Color foreground) {
applyForegroundColor(foreground, getContents());
}
public void setInformation(String information) {
// See IInformationControlExtension2
}
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() == false || getDialogSettings() == null) {
getShell().setLocation(location);
}
}
public void setSize(int width, int height) {
getShell().setSize(width, height);
}
public void setSizeConstraints(int maxWidth, int maxHeight) {
// Ignore
}
public void setVisible(boolean visible) {
if (visible) {
open();
} else {
saveDialogBounds(getShell());
getShell().setVisible(false);
}
}
public boolean hasContents() {
if ((viewer == null) || (viewer.getInput() == null)) {
return false;
}
return true;
}
public void setInput(Object input) {
if (input != null) {
viewer.setSelection(new StructuredSelection(input));
}
}
public void widgetDisposed(DisposeEvent e) {
// Note: We do not reuse the dialog
viewer = null;
filterText = null;
}
@Override
protected Control createTitleControl(Composite parent) {
Composite control = new Composite(parent, SWT.NONE);
GridLayout layout = new GridLayout(1, false);
layout.marginHeight = 0;
control.setLayout(layout);
control.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
// Applies only to dialog title - not body. See createDialogArea
// Create the text widget
createUIWidgetFilterText(control);
// Add listeners to the text widget
createUIListenersFilterText();
// Return the text widget
return control;
}
private void createUIWidgetFilterText(Composite parent) {
// Create the widget
filterText = new Text(parent, SWT.NONE);
// Set the font
GC gc = new GC(parent);
gc.setFont(parent.getFont());
FontMetrics fontMetrics = gc.getFontMetrics();
gc.dispose();
// Create the layout
GridData data = new GridData(GridData.FILL_HORIZONTAL);
data.heightHint = Dialog.convertHeightInCharsToPixels(fontMetrics, 1);
data.horizontalAlignment = GridData.FILL;
data.verticalAlignment = GridData.CENTER;
filterText.setLayoutData(data);
}
/**
*
*/
private void gotoSelectedElement() {
Object selectedElement = getSelectedElement();
if (selectedElement == null) {
return;
}
dispose();
}
private void createUIListenersFilterText() {
filterText.addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
if (e.keyCode == 0x0D) {
// Return key was pressed
gotoSelectedElement();
} else if (e.keyCode == SWT.ARROW_DOWN) {
// Down key was pressed
viewer.getTree().setFocus();
} else if (e.keyCode == SWT.ARROW_UP) {
// Up key was pressed
viewer.getTree().setFocus();
} else if (e.character == 0x1B) {
// Escape key was pressed
dispose();
}
}
public void keyReleased(KeyEvent e) {
// NO-OP
}
});
// Handle text modify events
filterText.addModifyListener(new ModifyListener() {
public void modifyText(ModifyEvent e) {
String text = ((Text) e.widget).getText();
int length = text.length();
if (length > 0) {
// Append a '*' pattern to the end of the text value if it
// does not have one already
if (text.charAt(length - 1) != '*') {
text = text + '*';
}
// Prepend a '*' pattern to the beginning of the text value
// if it does not have one already
if (text.charAt(0) != '*') {
text = '*' + text;
}
}
// Set and update the pattern
setMatcherString(text, true);
}
});
}
/**
* Sets the patterns to filter out for the receiver.
* <p>
* The following characters have special meaning: ? => any character * => any string
* </p>
*
* @param pattern
* the pattern
* @param update
* <code>true</code> if the viewer should be updated
*/
private void setMatcherString(String pattern, boolean update) {
namePatternFilter.setPattern(pattern);
if (update) {
stringMatcherUpdated();
}
}
/**
* The string matcher has been modified. The default implementation refreshes the view and selects the first matched
* element
*/
private void stringMatcherUpdated() {
// Refresh the tree viewer to re-filter
viewer.getControl().setRedraw(false);
viewer.refresh();
viewer.expandAll();
selectFirstMatch();
viewer.getControl().setRedraw(true);
}
protected AbstractTaskEditorPage getTaskEditorPage() {
IWorkbenchPage activePage = window.getActivePage();
if (activePage == null) {
return null;
}
IEditorPart editorPart = activePage.getActiveEditor();
AbstractTaskEditorPage taskEditorPage = null;
if (editorPart instanceof TaskEditor) {
TaskEditor taskEditor = (TaskEditor) editorPart;
IFormPage formPage = taskEditor.getActivePageInstance();
if (formPage instanceof AbstractTaskEditorPage) {
taskEditorPage = (AbstractTaskEditorPage) formPage;
}
}
return taskEditorPage;
}
/**
* Selects the first element in the tree which matches the current filter pattern.
*/
private void selectFirstMatch() {
Tree tree = viewer.getTree();
Object element = findFirstMatchToPattern(tree.getItems());
if (element != null) {
viewer.setSelection(new StructuredSelection(element), true);
} else {
viewer.setSelection(StructuredSelection.EMPTY);
}
}
/**
* @param items
* @return
*/
private Object findFirstMatchToPattern(TreeItem[] items) {
// Match the string pattern against labels
ILabelProvider labelProvider = (ILabelProvider) viewer.getLabelProvider();
// Process each item in the tree
for (TreeItem item : items) {
Object element = item.getData();
// Return the element if it matches the pattern
if (element != null) {
String label = labelProvider.getText(element);
if (namePatternFilter.wordMatches(label)) {
return element;
}
}
// Recursively check the elements children for a match
element = findFirstMatchToPattern(item.getItems());
// Return the child element match if found
if (element != null) {
return element;
}
}
// No match found
return null;
}
}