/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.ui.omni;
import com.google.dart.tools.deploy.Activator;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.CommandException;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.ParseException;
import org.eclipse.jface.bindings.keys.SWTKeySupport;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.util.Util;
import org.eclipse.swt.SWT;
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.events.MouseTrackAdapter;
import org.eclipse.swt.graphics.Point;
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.Menu;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.keys.IBindingService;
import org.eclipse.ui.menus.WorkbenchWindowControlContribution;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
* Contributes the omnibox toolbar control.
*/
public class OmniBoxControlContribution {
private final class Popup extends OmniBoxPopup {
private Popup(IWorkbenchWindow window, Command invokingCommand) {
super(window, invokingCommand);
}
/**
* Close the popup and reset the searchbox to it's initial state.
*/
@Override
public boolean close() {
try {
setWatermarkText();
defocus();
} catch (Throwable th) {
Activator.logError(th);
}
return simpleClose();
}
@Override
protected Point getDefaultLocation(Point initialSize) {
return locationRelativeToControl(textControl);
}
@Override
protected Point getDefaultSize() {
return new Point(textControl.getSize().x - POPUP_PIXEL_HORIZ_NUDGE * 2, 360);
}
/**
* Simple close of the pop-up (that does not reset the watermark or change focus).
*
* @see OmniBoxPopup#close()
*/
protected boolean simpleClose() {
return super.close();
}
}
/**
* Map of windows to control contributions. Needed in order to calculate an appropriate location
* for the omnibox popup when invoked via a command.
*/
private static Map<IWorkbenchWindow, OmniBoxControlContribution> CONTROL_MAP = new HashMap<IWorkbenchWindow, OmniBoxControlContribution>();
private static final String MOD_KEY = SWTKeySupport.getKeyFormatterForPlatform().format(SWT.MOD1);
private static final String COMMAND_KEY_STRING = Util.isMac() ? "COMMAND" : "CTRL";
private static final String WATERMARK_TEXT;
private static int SEARCH_BOX_STYLE_BITS = SWT.SEARCH;
static {
if (Util.isMac()) {
SEARCH_BOX_STYLE_BITS |= SWT.ICON_SEARCH;
}
}
static {
final String spacer = " ";//$NON-NLS-1$
if (Util.isMac()) {
//extra trailing space to mitigate OSX dimming at the edge of the text box
WATERMARK_TEXT = spacer + MOD_KEY + " 3 ";//$NON-NLS-1$
} else {
WATERMARK_TEXT = spacer + MOD_KEY + "-3 ";//$NON-NLS-1$
}
}
/*
* Pixel offset for popup.
*/
private static final int POPUP_PIXEL_HORIZ_NUDGE = 4;
private static final int POPUP_PIXEL_VERT_NUDGE = 3;
public static OmniBoxControlContribution getControlForWindow(IWorkbenchWindow window) {
return CONTROL_MAP.get(window);
}
/**
* Get a location relative to the active workbench window for presenting the omnibox popup. This
* service is required outside the control in case the omnibox is invoked by a command (e.g.,
* keybinding).
*
* @param window the host window
* @return the location to root the popup
*/
public static Point getPopupLocation(IWorkbenchWindow window) {
OmniBoxControlContribution contrib = CONTROL_MAP.get(window);
if (contrib == null) {
return new Point(0, 0);
}
return locationRelativeToControl(contrib.textControl);
}
private static Point locationRelativeToControl(Text control) {
return control.toDisplay(0 + POPUP_PIXEL_HORIZ_NUDGE, control.getSize().y
+ POPUP_PIXEL_VERT_NUDGE);
}
private Text textControl;
private boolean inControl;
private Popup popup;
//used to track whether text is being modified programmatically (e.g., watermark-setting)
private boolean listenForTextModify = true;
//used when we want to advance focus off of the text control (ideally to restore previous)
private Control previousFocusControl;
private final WorkbenchWindowControlContribution controlContribution;
//used to force popup refresh in case text was selected and replaced
private String previousFilterText;
public OmniBoxControlContribution(WorkbenchWindowControlContribution controlContribution) {
this.controlContribution = controlContribution;
}
public Control createControl(Composite parent) {
textControl = createTextControl(parent);
setWatermarkText();
hookupListeners();
CONTROL_MAP.put(getWorkbenchWindow(), this);
updateColors();
return textControl;
}
public void dispose() {
//Remove this control contribution from the cached control map.
IWorkbenchWindow disposedWindow = null;
for (Entry<IWorkbenchWindow, OmniBoxControlContribution> entry : CONTROL_MAP.entrySet()) {
if (entry.getValue() == this) {
disposedWindow = entry.getKey();
break;
}
}
if (disposedWindow != null) {
CONTROL_MAP.remove(disposedWindow);
}
}
public void giveFocus() {
cacheFocusControl(textControl.getDisplay().getFocusControl());
textControl.setFocus();
clearWatermark();
}
protected void defocus() {
if (previousFocusControl != null && !previousFocusControl.isDisposed()) {
previousFocusControl.setFocus();
} else {
Shell activeWorkbenchShell = SearchBoxUtils.getActiveWorkbenchShell();
if (activeWorkbenchShell != null) {
activeWorkbenchShell.setFocus();
} else {
Activator.log("no active workbench shell for searchbox defocus");
}
}
}
protected void refreshPopup() {
if (popup != null && !popup.isDisposed()) {
popup.refresh(getFilterText());
}
}
private void cacheFocusControl(Control focusControl) {
//since the point of caching the control is to restore focus away from us,
//ignore any sets to "self"
if (focusControl != textControl) {
previousFocusControl = focusControl;
}
}
private void clearWatermark() {
//ensure watermark (or valid text) does not get re-cleared
if (textControl.getForeground().equals(OmniBoxColors.SEARCHBOX_TEXT_COLOR)) {
return;
}
silentSetControlText(""); //$NON-NLS-1$
textControl.setForeground(OmniBoxColors.SEARCHBOX_TEXT_COLOR);
updateColors();
}
private Text createTextControl(Composite parent) {
Text text = new Text(parent, SEARCH_BOX_STYLE_BITS);
text.setToolTipText(OmniBoxMessages.OmniBoxControlContribution_control_tooltip);
if (Util.isLinux()) {
GridDataFactory.fillDefaults().indent(0, 1).grab(true, true).applyTo(text);
}
// Disables the default context menu for native SWT text boxes
text.setMenu(new Menu(parent));
return text;
}
private String getFilterText() {
return textControl.getText();
}
private IWorkbenchWindow getWorkbenchWindow() {
return controlContribution.getWorkbenchWindow();
}
private void handleFocusGained() {
//disable global keybinding handlers so we can trap copy/paste/etc
((IBindingService) PlatformUI.getWorkbench().getService(IBindingService.class)).setKeyFilterEnabled(false);
clearWatermark();
}
private void handleFocusLost() {
//re-enable global keybinding handlers
((IBindingService) PlatformUI.getWorkbench().getService(IBindingService.class)).setKeyFilterEnabled(true);
//GTK linux requires special casing to handle the case where a click
//outside the search box (or popup) should cause the popup to close
//We identify this case by keying off focus changes --- if focus
//is transfered to another control we trigger a close
// scheglov: Actually we need to use "asyncExec" on Mac and Windows too.
{
//Exec async to ensure that it occurs after the focus change
Display.getDefault().asyncExec(new Runnable() {
@Override
public void run() {
Control focusControl = Display.getDefault().getFocusControl();
if (focusControl != textControl && popup != null && focusControl != popup.table) {
popup.close();
popup = null;
}
}
});
}
if (popupClosed()) {
setWatermarkText();
}
}
@SuppressWarnings("deprecation")
private void handleKeyPressed(KeyEvent e) {
if (e.keyCode == 'f' && (e.stateMask & SWT.MOD1) == SWT.MOD1) {
if (e.character == '\0') {
return;
}
// special treatment to activate the text search when requested
IWorkbench wb = PlatformUI.getWorkbench();
IBindingService bindings = (IBindingService) wb.getService(IBindingService.class);
try {
KeySequence keys = KeySequence.getInstance(COMMAND_KEY_STRING + "+F");
Binding binding = bindings.getPerfectMatch(keys);
if (binding != null) {
ParameterizedCommand parameterizedCommand = binding.getParameterizedCommand();
if (parameterizedCommand != null) {
defocus();
parameterizedCommand.getCommand().execute(null);
}
}
} catch (ParseException ex) {
Activator.logError(ex);
} catch (CommandException ex) {
Activator.logError(ex);
}
e.doit = false;
return;
}
if (SWT.ARROW_DOWN == e.keyCode) {
if (popupClosed()) {
openPopup();
refreshPopup();
}
}
if (popupClosed()) {
//have escape defocus
if (SWT.ESC == e.character) {
defocus();
return;
}
//and don't let other control characters invoke the popup
if (Character.isISOControl(e.character)) {
return;
}
openPopup();
}
if (popup != null && !popup.isDisposed()) {
popup.sendKeyPress(e);
}
}
private void handleModifyText() {
if (!listenForTextModify) {
return;
}
String filterText = getFilterText();
//we need to re-search if the leading char has changed ('a' -> HOME -> 'x')
boolean needsResearch = hasLeadingFilterCharChanged();
//cache for next time around
previousFilterText = filterText;
if (filterText.length() > 0) {
if (needsResearch && !popupClosed()) {
popup.simpleClose();
popup = null;
}
if (popupClosed()) {
openPopup();
}
refreshPopup();
} else {
popup.simpleClose();
popup = null;
}
}
private void handleMouseEnter() {
inControl = true;
//cache on mouse enter to ensure we can restore focus after an invocation initiated by a mouse click
cacheFocusControl(textControl.getDisplay().getFocusControl());
}
private void handleMouseExit() {
inControl = false;
}
private void handleMouseUp() {
if (inControl) {
handleSelection();
}
}
private void handleSelection() {
clearWatermark();
}
/**
* Tests if the leading character of the filter text has changed since the last recorded text
* modification.
*/
private boolean hasLeadingFilterCharChanged() {
if (previousFilterText != null && !previousFilterText.isEmpty()) {
String filterText = getFilterText();
if (filterText == null || filterText.isEmpty()) {
return false;
}
return previousFilterText.charAt(0) != filterText.charAt(0);
}
return false;
}
private void hookupListeners() {
textControl.addMouseListener(new MouseAdapter() {
@Override
public void mouseUp(MouseEvent e) {
handleMouseUp();
}
});
textControl.addMouseTrackListener(new MouseTrackAdapter() {
@Override
public void mouseEnter(MouseEvent e) {
handleMouseEnter();
}
@Override
public void mouseExit(MouseEvent e) {
handleMouseExit();
}
});
textControl.addModifyListener(new ModifyListener() {
@Override
public void modifyText(ModifyEvent e) {
handleModifyText();
}
});
textControl.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
handleKeyPressed(e);
}
});
textControl.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
handleFocusGained();
}
@Override
public void focusLost(FocusEvent e) {
handleFocusLost();
}
});
}
private void openPopup() {
popup = new Popup(getWorkbenchWindow(), null);
popup.setFilterControl(textControl);
popup.open();
if (Util.isMac()) {
textControl.addListener(SWT.Deactivate, new Listener() {
@Override
public void handleEvent(Event event) {
if (popup != null) {
//selecting the scrollbar will deactivate but in that case we don't want to close
// TODO: bug; going from scrollbar back to text entry clears text!
Control focusControl = event.display.getFocusControl();
//in some cases the focus control goes null even though the text still receives
//key events (issue 1905) and we want to *not* close the popup
if (focusControl != null && focusControl != popup.table) {
popup.close();
popup = null;
}
}
textControl.removeListener(SWT.Deactivate, this);
}
});
}
}
private boolean popupClosed() {
return popup == null || popup.isDisposed();
}
private void setWatermarkText() {
silentSetControlText(WATERMARK_TEXT);
textControl.setForeground(OmniBoxColors.WATERMARK_TEXT_COLOR);
}
//set text without notifying listeners
private void silentSetControlText(String txt) {
try {
listenForTextModify = false;
textControl.setText(txt);
} finally {
listenForTextModify = true;
}
}
private void updateColors() {
//TODO(pquitslund): disabled pending investigation of regressions on ubuntu
// Display display = textControl.getDisplay();
// Color color = DartUI.getEditorForeground(
// DartToolsPlugin.getDefault().getCombinedPreferenceStore(),
// display);
// if (color == null) {
// color = display.getSystemColor(SWT.COLOR_INFO_FOREGROUND);
// }
// textControl.setForeground(color);
// color = DartUI.getEditorBackground(
// DartToolsPlugin.getDefault().getCombinedPreferenceStore(),
// display);
// if (color == null) {
// color = display.getSystemColor(SWT.COLOR_INFO_BACKGROUND);
// }
// textControl.setBackground(color);
}
}