/******************************************************************************* * Copyright (c) 2010, 2011 Tasktop Technologies 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: * Tasktop Technologies - initial API and implementation *******************************************************************************/ package org.eclipse.mylyn.commons.workbench.search; import java.util.Collection; import java.util.Collections; import org.eclipse.core.runtime.Assert; import org.eclipse.jface.dialogs.PopupDialog; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.viewers.IOpenListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.OpenEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerSorter; import org.eclipse.mylyn.commons.workbench.TableTreePatternFilter; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; 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.graphics.Point; import org.eclipse.swt.graphics.Rectangle; 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.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.PlatformUI; /** * @author Shawn Minto * @since 3.7 */ public class SearchHistoryPopupDialog extends PopupDialog { private static final int MAX_HISTORY_NO_FILTER = 3; private static final int MAX_HISTORY_FILTER = 10; private final int side; private Rectangle trimBounds; private TextSearchControl textSearchControl; private Text textControl; private TableViewer historyTable; private TableTreePatternFilter patternFilter; private Composite additionalControlsComposite; public SearchHistoryPopupDialog(Shell parent, int side) { super(parent, PopupDialog.HOVER_SHELLSTYLE, false, false, false, false, false, null, null); this.side = side; updateBounds(); } @Override protected void initializeBounds() { Rectangle monitorBounds = PlatformUI.getWorkbench() .getActiveWorkbenchWindow() .getShell() .getMonitor() .getClientArea(); Rectangle bounds = getShell().getBounds(); int x = 0; int y = 0; switch (side) { case SWT.TOP: x = trimBounds.x; y = trimBounds.y + trimBounds.height; if (x + bounds.width > monitorBounds.x + monitorBounds.width) { x = (trimBounds.x + trimBounds.width) - bounds.width; } break; case SWT.BOTTOM: x = trimBounds.x; y = trimBounds.y - bounds.height; if (x + bounds.width > monitorBounds.x + monitorBounds.width) { x = (trimBounds.x + trimBounds.width) - bounds.width; } break; case SWT.RIGHT: x = (trimBounds.x + trimBounds.width) - bounds.width; y = trimBounds.y + trimBounds.height; break; case SWT.LEFT: x = trimBounds.x; y = trimBounds.y + trimBounds.height; break; } getShell().setBounds(x, y, Math.max(trimBounds.width, bounds.width), bounds.height); } @Override protected Control createContents(Composite parent) { getShell().setBackground(getShell().getDisplay().getSystemColor(SWT.COLOR_DARK_GRAY)); Control createDialogArea = createDialogArea(parent); return createDialogArea; } @Override protected Control createDialogArea(Composite parent) { Composite composite = new Composite(parent, SWT.NONE); GridLayout gl = new GridLayout(); gl.marginHeight = 0; gl.marginWidth = 0; gl.horizontalSpacing = 0; gl.verticalSpacing = 0; gl.numColumns = 1; composite.setLayout(gl); GridDataFactory.fillDefaults().grab(true, false).applyTo(composite); createTable(composite); additionalControlsComposite = new Composite(composite, SWT.NONE); gl = new GridLayout(); gl.marginHeight = 0; gl.marginWidth = 0; gl.horizontalSpacing = 0; gl.verticalSpacing = 0; gl.numColumns = 1; additionalControlsComposite.setLayout(gl); GridDataFactory.fillDefaults().grab(true, false).applyTo(additionalControlsComposite); createAdditionalSearchRegion(additionalControlsComposite); if (additionalControlsComposite.getChildren().length == 0) { additionalControlsComposite.dispose(); additionalControlsComposite = null; } parent.pack(); return parent; } private void createTable(Composite composite) { historyTable = new TableViewer(composite, SWT.SINGLE | SWT.FULL_SELECTION | SWT.NO_SCROLL); TableColumn bindingNameColumn = new TableColumn(historyTable.getTable(), SWT.LEFT); // XXX fix this bindingNameColumn.setWidth(2000); historyTable.setUseHashlookup(true); historyTable.setContentProvider(new ITreeContentProvider() { public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { } public void dispose() { } public Object[] getElements(Object inputElement) { if (inputElement instanceof Collection<?>) { Object[] elements = ((Collection<?>) inputElement).toArray(); return reverseArray(elements); } return new Object[0]; } public boolean hasChildren(Object element) { return false; } public Object getParent(Object element) { return null; } public Object[] getChildren(Object parentElement) { return null; } private Object[] reverseArray(Object[] originalArray) { int i = 0; int j = originalArray.length - 1; while (i < j) { Object firstElement = originalArray[i]; originalArray[i] = originalArray[j]; originalArray[j] = firstElement; i++; j--; } return originalArray; } }); if (textSearchControl != null) { historyTable.setInput(textSearchControl.getSearchHistory()); } else { historyTable.setInput(Collections.emptyList()); } historyTable.addOpenListener(new IOpenListener() { public void open(OpenEvent event) { String text = getTextFromSelection(event.getSelection()); if (text != null) { textSearchControl.getTextControl().setText(text); textControl.setSelection(text.length()); textSearchControl.addToSearchHistory(text); } close(); } }); // XXX CHANGE TO SELECTION LISTENER?? historyTable.getTable().addMouseListener(new MouseAdapter() { @Override public void mouseDown(MouseEvent e) { String text = getTextFromSelection(historyTable.getSelection()); if (text != null) { textSearchControl.getTextControl().setText(text); textSearchControl.addToSearchHistory(text); } textSearchControl.getTextControl().setFocus(); close(); } }); patternFilter = new TableTreePatternFilter(); patternFilter.setIncludeLeadingWildcard(true); patternFilter.setPattern(textSearchControl.getTextControl().getText()); historyTable.addFilter(patternFilter); if (textControl.getText().length() > 0) { historyTable.setSorter(new ViewerSorter()); historyTable.setItemCount(Math.min(historyTable.getTable().getItemCount(), MAX_HISTORY_FILTER)); } else { historyTable.setSorter(null); historyTable.setItemCount(Math.min(historyTable.getTable().getItemCount(), MAX_HISTORY_NO_FILTER)); } GridDataFactory.fillDefaults() .grab(true, false) .hint(trimBounds.width, SWT.DEFAULT) .applyTo(historyTable.getTable()); setHistoryTableVisible(historyTable.getTable().getItemCount() > 0); } private void setHistoryTableVisible(boolean isVisible) { GridData layoutData = (GridData) historyTable.getTable().getLayoutData(); historyTable.getTable().setVisible(isVisible); boolean wasVisible = !layoutData.exclude; layoutData.exclude = !isVisible; if (wasVisible != isVisible || isVisible) { getShell().pack(); } initializeBounds(); } private boolean shouldOpen() { return (historyTable != null && historyTable.getTable() != null && historyTable.getTable().isVisible()) || additionalControlsComposite != null; } private String getTextFromSelection(ISelection selection) { if (selection instanceof StructuredSelection) { Object firstElement = ((StructuredSelection) selection).getFirstElement(); if (firstElement instanceof String) { return (String) firstElement; } } return null; } private boolean isOpen = false; @Override public int open() { if (!isOpen) { isOpen = true; int rc = super.open(); if (!shouldOpen()) { close(); } return rc; } return 0; } public void asyncClose() { Display.getDefault().asyncExec(new Runnable() { public void run() { if ((textControl != null && !textControl.isDisposed() && textControl.isFocusControl()) || !hasFocus(getShell())) { close(); } } }); } @Override public boolean close() { if (patternFilter != null) { patternFilter.setPattern(null); } boolean b = super.close(); isOpen = !b; return b; } private boolean updateBounds() { Rectangle bounds; if (textSearchControl == null || textSearchControl.getTextControl() == null || textSearchControl.getTextControl().isDisposed()) { bounds = new Rectangle(0, 0, 0, 0); } else { bounds = textSearchControl.getBounds(); Point absPosition = textSearchControl.toDisplay(textSearchControl.getLocation()); bounds.x = absPosition.x - bounds.x; bounds.y = absPosition.y - bounds.y; } if (trimBounds == null || !trimBounds.equals(bounds)) { this.trimBounds = bounds; return true; } return false; } public void attach(final TextSearchControl textSearchControl) { Assert.isNotNull(textSearchControl); Assert.isNotNull(textSearchControl.getTextControl()); this.textSearchControl = textSearchControl; this.textControl = textSearchControl.getTextControl(); textSearchControl.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent e) { if (!hasFocus(textControl)) { // user shouldn't be modifying the text if it doesnt have focus return; } if (!isOpen && textControl != null && !textControl.isDisposed() && textControl.getText().length() > 0) { updateBounds(); open(); } if (isOpen && historyTable != null && !historyTable.getTable().isDisposed() && patternFilter != null) { patternFilter.setPattern(textControl.getText()); historyTable.setSelection(null); historyTable.refresh(); if (textControl.getText().length() > 0) { historyTable.setSorter(new ViewerSorter()); historyTable.setItemCount(Math.min(historyTable.getTable().getItemCount(), MAX_HISTORY_FILTER)); } else { historyTable.setSorter(null); historyTable.setItemCount(Math.min(historyTable.getTable().getItemCount(), MAX_HISTORY_NO_FILTER)); } setHistoryTableVisible(historyTable.getTable().getItemCount() > 0); if (!shouldOpen()) { asyncClose(); } } } }); textControl.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { close(); } }); textControl.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.character == SWT.ESC && isOpen) { close(); e.doit = false; } else if (e.stateMask == 0 && e.keyCode == SWT.ARROW_DOWN && isOpen && historyTable.getTable() != null && !historyTable.getTable().isDisposed() && historyTable.getTable().getItemCount() > 0) { historyTable.getTable().select(0); historyTable.getTable().setFocus(); e.doit = false; } else if ((e.stateMask & SWT.MOD1) != 0 && e.keyCode == SWT.ARROW_DOWN && !isOpen) { updateBounds(); open(); e.doit = false; } } }); Listener moveResizeListener = new Listener() { public void handleEvent(Event event) { if (isOpen) { if (updateBounds()) { initializeBounds(); } } } }; textSearchControl.addListener(SWT.Move, moveResizeListener); textSearchControl.addListener(SWT.Resize, moveResizeListener); textControl.addFocusListener(new FocusListener() { public void focusLost(FocusEvent e) { asyncClose(); } public void focusGained(FocusEvent e) { } }); } private boolean hasFocus(Control control) { if (control != null && !control.isDisposed() && control.isFocusControl()) { return true; } if (control instanceof Composite) { for (Control child : ((Composite) control).getChildren()) { if (hasFocus(child)) { return true; } } } return false; } protected void createAdditionalSearchRegion(Composite composite) { } }