// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
//
// 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.collide.client.search.awesomebox.components;
import com.google.collide.client.editor.Editor;
import com.google.collide.client.editor.FocusManager;
import com.google.collide.client.editor.Editor.KeyListener;
import com.google.collide.client.editor.search.SearchModel;
import com.google.collide.client.editor.search.SearchModel.MatchCountListener;
import com.google.collide.client.search.awesomebox.host.AbstractAwesomeBoxComponent;
import com.google.collide.client.search.awesomebox.host.ComponentHost;
import com.google.collide.client.search.awesomebox.shared.AwesomeBoxResources;
import com.google.collide.client.search.awesomebox.shared.MappedShortcutManager;
import com.google.collide.client.search.awesomebox.shared.ShortcutManager;
import com.google.collide.client.search.awesomebox.shared.AwesomeBoxResources.ComponentCss;
import com.google.collide.client.search.awesomebox.shared.ShortcutManager.ShortcutPressedCallback;
import com.google.collide.client.ui.menu.PositionController.HorizontalAlign;
import com.google.collide.client.ui.menu.PositionController.VerticalAlign;
import com.google.collide.client.ui.tooltip.Tooltip;
import com.google.collide.client.util.CssUtils;
import com.google.collide.client.util.Elements;
import com.google.collide.client.util.input.ModifierKeys;
import com.google.collide.mvp.CompositeView;
import com.google.collide.mvp.HasView;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.collide.shared.util.StringUtils;
import com.google.collide.shared.util.ListenerRegistrar.RemoverManager;
import com.google.common.base.Preconditions;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.SpanElement;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiTemplate;
import org.waveprotocol.wave.client.common.util.SignalEvent;
import elemental.events.Event;
import elemental.events.EventListener;
import elemental.events.KeyboardEvent;
import elemental.events.KeyboardEvent.KeyCode;
import elemental.events.MouseEvent;
/**
* Section that displays find and replace controls. This section is meant to be
* the first item in it's context since it piggy backs off the AwesomeBox input.
*/
public class FindReplaceComponent extends AbstractAwesomeBoxComponent implements HasView<
FindReplaceComponent.View> {
public interface ViewEvents {
public void onFindQueryChanged();
public void onKeydown(KeyboardEvent event);
public void onNextClicked();
public void onPreviousClicked();
public void onReplaceClicked();
public void onReplaceAllClicked();
public void onCloseClicked();
}
public enum FindMode {
FIND, REPLACE
}
private final View view;
private final ShortcutManager shortcutManager = new MappedShortcutManager();
private SearchModel searchModel;
private String lastQuery = "";
private FocusManager focusManager;
// Editor Listener For Esc
// TODO: Long term this should be a global clear event that bubbles
private ListenerRegistrar<KeyListener> editorKeyListenerRegistrar;
private RemoverManager removerManager = new RemoverManager();
// TODO: Handle changes to total matches via document mutations
private final MatchCountListener totalMatchesListener = new MatchCountListener() {
@Override
public void onMatchCountChanged(int total) {
getView().numMatches.setInnerText(String.valueOf(total));
}
};
public FindReplaceComponent(View view) {
super(HideMode.NO_AUTOHIDE, HiddenBehavior.REVERT_TO_DEFAULT, "Find in this file");
this.view = view;
view.setDelegate(new ViewEventsImpl());
}
public void setFindMode(FindMode mode) {
CssUtils.setDisplayVisibility2(
Elements.asJsElement(getView().totalMatchesContainer), mode == FindMode.FIND);
CssUtils.setDisplayVisibility2(
Elements.asJsElement(getView().replaceActions), mode == FindMode.REPLACE);
CssUtils.setDisplayVisibility2(
Elements.asJsElement(getView().replaceRow), mode == FindMode.REPLACE);
}
/**
* Attaches to the editor's search model for querying.
*/
public void attachEditor(Editor editor) {
this.searchModel = editor.getSearchModel();
this.focusManager = editor.getFocusManager();
this.editorKeyListenerRegistrar = editor.getKeyListenerRegistrar();
searchModel.getMatchCountChangedListenerRegistrar().add(totalMatchesListener);
setupShortcuts();
}
@Override
public View getView() {
return view;
}
@Override
public elemental.html.Element getElement() {
return getView().getElement();
}
/**
* Sets the query of the find replace component.
*/
public void setQuery(String query) {
getView().setQuery(query);
if (isActive()) {
Preconditions.checkNotNull(searchModel, "Search model is required to set the query");
getView().selectQuery();
searchModel.setQuery(query);
}
}
@Override
public String getTooltipText() {
return "Press Ctrl+F to quickly find text in the current file";
}
/**
* Initializes the shortcut manager with our shortcuts of interest. The
* {@link ComponentHost} will handle actually notifying us of shortcuts being
* used.
*/
private void setupShortcuts() {
shortcutManager.addShortcut(0, KeyCode.ENTER, new ShortcutPressedCallback() {
@Override
public void onShortcutPressed(KeyboardEvent event) {
event.preventDefault();
if (searchModel != null) {
searchModel.getMatchManager().selectNextMatch();
}
}
});
shortcutManager.addShortcut(ModifierKeys.SHIFT, KeyCode.ENTER, new ShortcutPressedCallback() {
@Override
public void onShortcutPressed(KeyboardEvent event) {
event.preventDefault();
if (searchModel != null) {
searchModel.getMatchManager().selectPreviousMatch();
}
}
});
shortcutManager.addShortcut(ModifierKeys.ACTION, KeyCode.G, new ShortcutPressedCallback() {
@Override
public void onShortcutPressed(KeyboardEvent event) {
event.preventDefault();
if (focusManager != null) {
focusManager.focus();
}
}
});
shortcutManager.addShortcut(
ModifierKeys.ACTION | ModifierKeys.SHIFT, KeyCode.G, new ShortcutPressedCallback() {
@Override
public void onShortcutPressed(KeyboardEvent event) {
event.preventDefault();
searchModel.getMatchManager().selectPreviousMatch();
if (focusManager != null) {
focusManager.focus();
}
}
});
}
@Override
public void onShow(ComponentHost host, ShowReason reason) {
super.onShow(host, reason);
String query = getView().getQuery();
if (StringUtils.isNullOrEmpty(query)) {
getView().setQuery(lastQuery);
searchModel.setQuery(lastQuery);
} else if (!searchModel.getQuery().equals(query)) {
searchModel.setQuery(query);
}
// Listen for esc in the editor while we're showing
// TODO: Use some sort of event system long term
removerManager.track(editorKeyListenerRegistrar.add(new KeyListener() {
@Override
public boolean onKeyPress(SignalEvent event) {
if (event.getKeyCode() == KeyCode.ESC) {
hide();
}
return false;
}
}));
}
@Override
public void focus() {
getView().selectQuery();
getView().focus();
}
@Override
public void onHide() {
lastQuery = getView().getQuery();
if (searchModel != null) {
searchModel.setQuery("");
}
removerManager.remove();
getView().numMatches.setInnerText("0");
}
public class ViewEventsImpl implements ViewEvents {
@Override
public void onFindQueryChanged() {
Preconditions.checkNotNull(searchModel, "Search model must be set for find/replace to work");
String query = getView().getQuery();
searchModel.setQuery(query);
}
@Override
public void onKeydown(KeyboardEvent event) {
shortcutManager.onKeyDown(event);
}
@Override
public void onNextClicked() {
if (searchModel != null && !StringUtils.isNullOrEmpty(searchModel.getQuery())) {
searchModel.getMatchManager().selectNextMatch();
}
}
@Override
public void onPreviousClicked() {
if (searchModel != null && !StringUtils.isNullOrEmpty(searchModel.getQuery())) {
searchModel.getMatchManager().selectPreviousMatch();
}
}
@Override
public void onReplaceAllClicked() {
searchModel.getMatchManager().replaceAllMatches(getView().replaceInput.getValue());
getView().selectQuery();
getView().focus();
}
@Override
public void onReplaceClicked() {
searchModel.getMatchManager().replaceMatch(getView().replaceInput.getValue());
}
@Override
public void onCloseClicked() {
hide();
}
}
public static class View extends CompositeView<ViewEvents> {
@UiTemplate("FindReplaceComponent.ui.xml")
interface FindReplaceUiBinder extends UiBinder<Element, View> {
}
private static FindReplaceUiBinder uiBinder = GWT.create(FindReplaceUiBinder.class);
@UiField(provided = true)
final ComponentCss css;
@UiField(provided = true)
final AwesomeBoxResources res;
@UiField
InputElement findInput;
@UiField
DivElement closeButton;
@UiField
DivElement replaceRow;
@UiField
InputElement replaceInput;
@UiField
AnchorElement prevButton;
@UiField
AnchorElement nextButton;
@UiField
DivElement replaceActions;
@UiField
AnchorElement replaceButton;
@UiField
AnchorElement replaceAllButton;
@UiField
SpanElement numMatches;
@UiField
DivElement totalMatchesContainer;
public View(AwesomeBoxResources res) {
this.res = res;
this.css = res.awesomeBoxComponentCss();
setElement(Elements.asJsElement(uiBinder.createAndBindUi(this)));
createTooltips(res);
handleEvents();
}
public String getQuery() {
return findInput.getValue();
}
public void setQuery(String query) {
findInput.setValue(query);
}
public void selectQuery() {
findInput.select();
}
public void focus() {
findInput.focus();
}
private void createTooltips(AwesomeBoxResources res) {
Tooltip.create(res, Elements.asJsElement(nextButton), VerticalAlign.BOTTOM,
HorizontalAlign.MIDDLE, "Next match");
Tooltip.create(res, Elements.asJsElement(prevButton), VerticalAlign.BOTTOM,
HorizontalAlign.MIDDLE, "Previous match");
Tooltip.create(res, Elements.asJsElement(replaceButton), VerticalAlign.BOTTOM,
HorizontalAlign.MIDDLE, "Replace current match");
Tooltip.create(res, Elements.asJsElement(replaceAllButton), VerticalAlign.BOTTOM,
HorizontalAlign.MIDDLE, "Replace all matches");
}
private void handleEvents() {
Elements.asJsElement(findInput).addEventListener(Event.INPUT, new EventListener() {
@Override
public void handleEvent(Event evt) {
if (getDelegate() != null) {
getDelegate().onFindQueryChanged();
}
}
}, false);
getElement().addEventListener(Event.KEYDOWN, new EventListener() {
@Override
public void handleEvent(Event evt) {
if (getDelegate() != null) {
getDelegate().onKeydown((KeyboardEvent) evt);
}
}
}, false);
getElement().addEventListener(Event.CLICK, new EventListener() {
@Override
public void handleEvent(Event arg0) {
if (getDelegate() == null) {
return;
}
MouseEvent mouseEvent = (MouseEvent) arg0;
if (prevButton.isOrHasChild((Node) mouseEvent.getTarget())) {
getDelegate().onPreviousClicked();
} else if (nextButton.isOrHasChild((Node) mouseEvent.getTarget())) {
getDelegate().onNextClicked();
} else if (replaceButton.isOrHasChild((Node) mouseEvent.getTarget())) {
getDelegate().onReplaceClicked();
} else if (replaceAllButton.isOrHasChild((Node) mouseEvent.getTarget())) {
getDelegate().onReplaceAllClicked();
} else if (closeButton.isOrHasChild((Node) mouseEvent.getTarget())) {
getDelegate().onCloseClicked();
}
}
}, false);
}
}
}