/******************************************************************************* * Copyright (c) 2015, 2015 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: * Bruno Medeiros - initial API and implementation *******************************************************************************/ package melnorme.lang.ide.ui.views; import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull; import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue; import static melnorme.utilbox.core.CoreUtil.array; import melnorme.lang.ide.ui.LangUIPlugin; import melnorme.util.swt.jface.TreeViewerExt; import melnorme.util.swt.jface.TreeViewerUtil; import melnorme.utilbox.misc.ArrayUtil; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogSettings; 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.DelegatingStyledCellLabelProvider; import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider; import org.eclipse.jface.viewers.IBaseLabelProvider; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; 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.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; public abstract class AbstractFilteredTreePopupControl extends PopupDialog implements IInformationControl, IInformationControlExtension, IInformationControlExtension2, DisposeListener { protected final int treeStyle; protected TreeElementsFilter treeElementsFilter; protected Text filterText; protected TreeViewerExt treeViewer; protected Composite fViewMenuButtonComposite; protected String filteringString = ""; public AbstractFilteredTreePopupControl(Shell parent, int shellStyle, int treeStyle) { super(parent, shellStyle, true, true, false, true, true, null, null); this.treeStyle = treeStyle; // Title and info text must be set to some value to have the controls created, so set empty values. // These will be updated later. if(hasHeader()) { setTitleText(""); } setInfoText(""); create(); setInfoText("Outline"); // TODO: Use info text for more element information, or key-toggle info } protected Text getFilterText() { return filterText; } protected TreeViewer getTreeViewer() { return treeViewer; } @Override public void dispose() { close(); } protected boolean hasHeader() { return false; // Subclasses may reimplement } /* ----------------- Shell creation (JDT based) ----------------- */ @Override public void setVisible(boolean visible) { if(visible) { open(); } else { saveDialogBounds(getShell()); getShell().setVisible(false); } } @Override public void setSizeConstraints(int maxWidth, int maxHeight) { } @Override public Point computeSizeHint() { // return the shell's size - note that it already has the persisted size if persisting is enabled. return getShell().getSize(); } @Override 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); } @Override protected IDialogSettings getDialogSettings() { return LangUIPlugin.getDialogSettings(getDialogSettingsId()); } protected abstract String getDialogSettingsId(); @Override public boolean hasContents() { return treeViewer != null && treeViewer.getInput() != null; } @Override public void setSize(int width, int height) { getShell().setSize(width, height); } @Override public void addDisposeListener(DisposeListener listener) { getShell().addDisposeListener(listener); } @Override public void removeDisposeListener(DisposeListener listener) { getShell().removeDisposeListener(listener); } @Override public void setForegroundColor(Color foreground) { applyForegroundColor(foreground, getContents()); } @Override public void setBackgroundColor(Color background) { applyBackgroundColor(background, getContents()); } @Override public boolean isFocusControl() { return getShell().getDisplay().getActiveShell() == getShell(); } @Override public void setFocus() { getShell().forceFocus(); filterText.setFocus(); } @Override public void addFocusListener(FocusListener listener) { getShell().addFocusListener(listener); } @Override public void removeFocusListener(FocusListener listener) { getShell().removeFocusListener(listener); } @Override public void widgetDisposed(DisposeEvent event) { treeViewer = null; filterText = null; } /* ----------------- Create controls ----------------- */ @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()) { filterText = createFilterText(parent); } return fViewMenuButtonComposite; } @Override protected Control createTitleControl(Composite parent) { if(hasHeader()) { return super.createTitleControl(parent); } filterText = createFilterText(parent); return filterText; } protected Text createFilterText(Composite parent) { filterText = new Text(parent, SWT.NONE); Dialog.applyDialogFont(filterText); filterText.setLayoutData(new GridData(GridData.FILL, GridData.CENTER, true, false)); filterText.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { if(e.keyCode == 0x0D || e.keyCode == SWT.KEYPAD_CR) { gotoSelectedElement(); } if(e.keyCode == SWT.ARROW_DOWN) { treeViewer.getTree().setFocus(); } if(e.keyCode == SWT.ARROW_UP) { treeViewer.getTree().setFocus(); } if(e.character == SWT.ESC) { dispose(); } } @Override public void keyReleased(KeyEvent e) { } }); return filterText; } protected Tree createTree(Composite parent, int treeStyle) { final Tree tree = new Tree(parent, SWT.SINGLE | (treeStyle & ~SWT.MULTI)); GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true); gd.heightHint = tree.getItemHeight() * 12; tree.setLayoutData(gd); tree.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if(e.character == 0x1B) { dispose(); } } }); // Auto mouse click listeners tree.addSelectionListener(new SelectionAdapter() { @Override public void widgetDefaultSelected(SelectionEvent e) { gotoSelectedElement(); } }); tree.addMouseListener(new MouseAdapter() { @Override public void mouseUp(MouseEvent e) { if(e.button != 1) return; if(tree == e.getSource() && tree.getSelectionCount() == 1) { TreeItem selection = tree.getSelection()[0]; if(selection.equals(tree.getItem(new Point(e.x, e.y)))) { gotoSelectedElement(); } } } }); return tree; } protected void createTreeViewer(Composite parent, int treeStyle) { Tree tree = createTree(parent, treeStyle); treeViewer = new TreeViewerFilterExt(tree); TreeViewerUtil.addTreeViewerMouseAutoScroller(treeViewer); treeElementsFilter = createTreeFilter(); treeViewer.addFilter(treeElementsFilter); treeViewer.setAutoExpandLevel(TreeViewer.ALL_LEVELS); } @Override protected Control createDialogArea(Composite parent) { createTreeViewer(parent, treeStyle); filterText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { String text = ((Text) e.widget).getText(); setFilteringString(text); } }); addDisposeListener(this); return treeViewer.getControl(); } @Override protected void setTabOrder(Composite composite) { if (hasHeader()) { composite.setTabList(array(filterText, treeViewer.getTree())); } else { fViewMenuButtonComposite.setTabList(array(filterText)); composite.setTabList(array(fViewMenuButtonComposite, treeViewer.getTree())); } } /* ----------------- input / selection ----------------- */ @Override public void setInformation(String information) { // ignored, IInformationControlExtension2.setInput(Object) should be used instead. } @Override public void setInput(Object information) { doSetInput(information, information); } protected void doSetInput(Object newInput, Object selectedElement) { filterText.setText(""); treeViewer.setInput(newInput); treeViewer.setSelectedElement(selectedElement); } protected Object getSelectedElement() { if(treeViewer == null) return null; return treeViewer.getSelectionFirstElement(); } protected Object firstDirectlyFilteredInElement; protected void setFilteringString(String pattern) { this.filteringString = pattern; refreshViewerFiltering(); } protected void refreshViewerFiltering() { treeViewer.getControl().setRedraw(false); treeViewer.refresh(); treeViewer.setSelectedElement(firstDirectlyFilteredInElement); treeViewer.expandAll(); treeViewer.getControl().setRedraw(true); } /* ----------------- Filter ----------------- */ protected class TreeViewerFilterExt extends TreeViewerExt { public TreeViewerFilterExt(Tree tree) { super(tree); } @Override protected void internalInitializeTree(Control widget) { AbstractFilteredTreePopupControl.this.firstDirectlyFilteredInElement = null; super.internalInitializeTree(widget); } @Override protected void internalRefresh(Object element, boolean updateLabels) { if(element == getRoot()) { AbstractFilteredTreePopupControl.this.firstDirectlyFilteredInElement = null; } super.internalRefresh(element, updateLabels); } } protected TreeElementsFilter createTreeFilter() { return new TreeElementsFilter(); } protected class TreeElementsFilter extends ViewerFilter { @Override public boolean select(Viewer viewer, Object parentElement, Object element) { assertTrue(viewer == treeViewer); return elementFilteredIn(element); } protected boolean elementFilteredIn(Object element) { if(elementDirectlyFilteredIn(element)) { if(firstDirectlyFilteredInElement == null) { firstDirectlyFilteredInElement = element; } return true; } return hasFilteredInChildren(element); } protected boolean hasFilteredInChildren(Object element) { Object[] children = ((ITreeContentProvider) treeViewer.getContentProvider()).getChildren(element); for(Object child : ArrayUtil.nullToEmpty(children)) { if(elementFilteredIn(child)) { return true; } } return false; } } protected boolean elementDirectlyFilteredIn(Object element) { if(filteringString.length() == 0) return true; String matchName = getText(element); assertNotNull(matchName); return matchName != null && matchNameDirectlyFilteredIn(matchName); } protected String getText(Object element) { IBaseLabelProvider provider = treeViewer.getLabelProvider(); if(provider instanceof ILabelProvider) { ILabelProvider labelProvider = (ILabelProvider) provider; return labelProvider.getText(element); } else if(provider instanceof IStyledLabelProvider) { IStyledLabelProvider labelProvider = (IStyledLabelProvider) provider; return labelProvider.getStyledText(element).getString(); } else if(provider instanceof DelegatingStyledCellLabelProvider) { DelegatingStyledCellLabelProvider styledCellLabelProvider = (DelegatingStyledCellLabelProvider) provider; IStyledLabelProvider labelProvider = styledCellLabelProvider.getStyledStringProvider(); return labelProvider.getStyledText(element).getString(); } return null; } protected abstract boolean matchNameDirectlyFilteredIn(String matchName); /* ----------------- Actions / operations ----------------- */ @Override protected void fillDialogMenu(IMenuManager dialogMenu) { super.fillDialogMenu(dialogMenu); fillViewMenu(dialogMenu); } @SuppressWarnings("unused") protected void fillViewMenu(IMenuManager viewMenu) { } protected abstract void gotoSelectedElement(); }