/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.coregui.client.searchbar;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.smartgwt.client.core.Rectangle;
import com.smartgwt.client.types.Alignment;
import com.smartgwt.client.types.Overflow;
import com.smartgwt.client.widgets.events.ClickEvent;
import com.smartgwt.client.widgets.events.ClickHandler;
import com.smartgwt.client.widgets.form.DynamicForm;
import com.smartgwt.client.widgets.form.fields.TextItem;
import com.smartgwt.client.widgets.form.fields.events.BlurEvent;
import com.smartgwt.client.widgets.form.fields.events.BlurHandler;
import com.smartgwt.client.widgets.form.fields.events.FocusEvent;
import com.smartgwt.client.widgets.form.fields.events.FocusHandler;
import com.smartgwt.client.widgets.form.fields.events.KeyUpEvent;
import com.smartgwt.client.widgets.form.fields.events.KeyUpHandler;
import com.smartgwt.client.widgets.grid.ListGrid;
import com.smartgwt.client.widgets.grid.ListGridRecord;
import com.smartgwt.client.widgets.toolbar.ToolStripButton;
import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.search.SavedSearch;
import org.rhq.core.domain.search.SearchSubsystem;
import org.rhq.coregui.client.CoreGUI;
import org.rhq.coregui.client.IconEnum;
import org.rhq.coregui.client.Messages;
import org.rhq.coregui.client.UserSessionManager;
import org.rhq.coregui.client.gwt.GWTServiceLookup;
import org.rhq.coregui.client.gwt.SearchGWTServiceAsync;
import org.rhq.coregui.client.util.Log;
import org.rhq.coregui.client.util.enhanced.EnhancedToolStrip;
import org.rhq.coregui.client.util.message.Message;
/**
* The class defines the UI component of the search bar.
* Behavior related to the retrieving/saving of searches is delegated to the Search Strategies.
* The enumMaps confine us to just setting the search modes and the modes determine the strategies.
* This is a little safer than typical strategy pattern where we can set any strategy regardless of the
* search mode (in this case, the search mode and strategies can get out of sync). In fact,
* there is no setter for the search strategy, as you cant set it is setup by the enumMap only.
*
* @author Mike Thompson
*/
public class EnhancedSearchBar extends EnhancedToolStrip {
private static final Messages MSG = CoreGUI.getMessages();
private static final int SEARCH_WIDTH = 300;
private static final int SAVED_SEARCH_WIDTH = 150;
private static final int PICKLIST_HEIGHT = 500;
private SearchSubsystem searchSubsystem;
private ToolStripButton searchTextButton;
private ToolStripButton saveSearchButton;
private TextItem searchTextItem;
private ListGrid pickListGrid;
private TextItem saveSearchTextItem;
private final FavoritesSearchStrategy favoritesSearchStrategy;
private final BasicSearchStrategy basicSearchStrategy;
private String lastSearchTerm;
private Timer searchDelayTimer;
private final SearchGWTServiceAsync searchService = GWTServiceLookup.getSearchService();
/**
* The amount of time delayed before an actual search result is displayed.
* So that we are not bombarding the server with ajax requests.
*/
private static final int SEARCH_KEYUP_DELAY = 1000;
private static final List<String> IGNORED_KEYS;
static {
IGNORED_KEYS = new ArrayList<String>();
IGNORED_KEYS.add("Arrow_Down");
IGNORED_KEYS.add("Arrow_Up");
IGNORED_KEYS.add("Arrow_Left");
IGNORED_KEYS.add("Arrow_Right");
}
enum SearchMode {
BASIC_SEARCH_MODE, SAVED_SEARCH_MODE
}
private SearchMode searchMode = SearchMode.BASIC_SEARCH_MODE;
/**
* The enumMap will act as a simple state machine defining the mappings of search strategy behavior and
* eliminating any if logic.
*/
private EnumMap<SearchMode, AbstractSearchStrategy> searchStrategies = new EnumMap<SearchMode, AbstractSearchStrategy>(
SearchMode.class);
public EnhancedSearchBar(SearchSubsystem searchSubsystem, String initialSearchText) {
if (null == searchSubsystem) {
this.searchSubsystem = SearchSubsystem.RESOURCE; // default to resource
} else {
this.searchSubsystem = searchSubsystem;
}
setOverflow(Overflow.VISIBLE);
setAutoHeight();
setWidth100();
setAlign(Alignment.LEFT);
favoritesSearchStrategy = new FavoritesSearchStrategy(this);
basicSearchStrategy = new BasicSearchStrategy(this);
// now we can fill our enumMap
searchStrategies.put(SearchMode.BASIC_SEARCH_MODE, basicSearchStrategy);
searchStrategies.put(SearchMode.SAVED_SEARCH_MODE, favoritesSearchStrategy);
// set the default search provider
setSearchMode(SearchMode.BASIC_SEARCH_MODE);
pickListGrid = new ListGrid();
configurePickListGrid();
DynamicForm searchTextForm = new DynamicForm();
searchTextForm.setStyleName("searchBar");
searchTextForm.setAutoWidth();
searchTextForm.setNumCols(1);
searchTextItem = new TextItem("search", MSG.common_button_search());
searchTextItem.setShowTitle(false);
searchTextItem.setWidth(SEARCH_WIDTH);
searchTextItem.setBrowserSpellCheck(false);
searchTextItem.addKeyUpHandler(new KeyUpHandler() {
public void onKeyUp(final KeyUpEvent keyUpEvent) {
Log.debug("onKeyUp search Mode: " + searchMode + " key: " + keyUpEvent.getKeyName());
keyUpEvent.cancel();
if (IGNORED_KEYS.contains(keyUpEvent.getKeyName())) {
return;
}
if (keyUpEvent.getKeyName().equals("Enter")) {
if (getSearchMode().equals(SearchMode.SAVED_SEARCH_MODE)) {
return;
}
String currentSearchTerm = (String) keyUpEvent.getItem().getValue();
currentSearchTerm = (null == currentSearchTerm) ? "" : currentSearchTerm; // avoid NPEs
Log.debug("onKeyUp search Mode Enter key pressed");
// stop any duplicate searches
if (!currentSearchTerm.equalsIgnoreCase(lastSearchTerm)) {
getSearchStrategy().searchReturnKeyHandler(keyUpEvent);
searchTextItem.focusInItem();
lastSearchTerm = currentSearchTerm;
} else {
hideList();
}
} else if (keyUpEvent.getKeyName().equals("Escape")) {
searchTextItem.setValue(lastSearchTerm);
hideList();
} else {
if (null == searchDelayTimer) {
searchDelayTimer = new Timer() {
public void run() {
getSearchStrategy().searchKeyUpHandler(keyUpEvent);
pickListGrid.show();
}
};
} else {
searchDelayTimer.cancel();
}
// wait for some typing quiet time before performing a search
searchDelayTimer.schedule(SEARCH_KEYUP_DELAY);
}
}
});
pickListGrid.addClickHandler(new com.smartgwt.client.widgets.events.ClickHandler() {
@Override
public void onClick( com.smartgwt.client.widgets.events.ClickEvent clickEvent) {
pickListGrid.focus();
}
});
searchTextItem.addClickHandler(new com.smartgwt.client.widgets.form.fields.events.ClickHandler() {
@Override
public void onClick(com.smartgwt.client.widgets.form.fields.events.ClickEvent clickEvent) {
if (pickListGrid.isVisible()) {
hideList();
} else {
showList();
}
}
});
searchTextItem.addFocusHandler( new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
showList();
}
});
searchTextItem.addBlurHandler( new BlurHandler() {
@Override
public void onBlur(BlurEvent event) {
Timer t = new Timer() {
public void run() {
if (!pickListGrid.containsFocus()) {
hideList();
}
}
};
t.schedule(500);
}
});
searchTextForm.setFields(searchTextItem);
addMember(searchTextForm);
searchTextItem.setTooltip(MSG.view_searchBar_buttonTooltip());
searchTextItem.setHint("Search");
searchTextItem.setShowHintInField(true);
saveSearchButton = new ToolStripButton();
saveSearchButton.setIcon(IconEnum.STAR_OFF.getIcon16x16Path());
saveSearchButton.setTooltip(MSG.view_searchBar_savedSearch_buttonTooltip());
saveSearchButton.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
toggleFavoriteSearch();
}
});
addButton(saveSearchButton);
DynamicForm saveSearchTextForm = new DynamicForm();
saveSearchTextForm.setAutoWidth();
saveSearchTextItem = new TextItem("savedSearchName");
saveSearchTextItem.setShowTitle(false);
saveSearchTextItem.setWidth(SAVED_SEARCH_WIDTH);
saveSearchTextForm.setFields(saveSearchTextItem);
addMember(saveSearchTextForm);
saveSearchTextItem.hide();
saveSearchTextItem.addKeyUpHandler(new KeyUpHandler() {
@Override
public void onKeyUp(KeyUpEvent keyUpEvent) {
if (keyUpEvent.getKeyName().equals("Enter")) {
saveFavoriteSearch();
}
}
});
}
private void populateInitialSearch() {
getSearchStrategy().searchFocusHandler();
pickListGrid.show();
showPickListGrid();
searchTextItem.focusInItem();
}
private void saveFavoriteSearch() {
String savedSearchName = saveSearchTextItem.getValueAsString();
if (null == savedSearchName || savedSearchName.isEmpty()) {
return;
}
String savedSearchPattern = searchTextItem.getValueAsString();
if (null == savedSearchPattern || savedSearchPattern.isEmpty()) {
return;
}
// This may be a name change, a pattern change or a new saved search. Look at the existing SS list and
// decided what to do.
boolean updated = false;
for (int i = 0, size = pickListGrid.getTotalRows(); !updated && (i < size); ++i) {
ListGridRecord record = pickListGrid.getRecord(i);
String name = record.getAttribute(AbstractSearchStrategy.ATTR_NAME);
String pattern = record.getAttribute(AbstractSearchStrategy.ATTR_PATTERN);
if (savedSearchName.equalsIgnoreCase(name) && savedSearchPattern.equals(pattern)) {
return; // SS already exists
} else if (savedSearchName.equalsIgnoreCase(name)) {
Integer id = record.getAttributeAsInt(AbstractSearchStrategy.ATTR_ID);
updateSavedSearchPattern(id, name, savedSearchPattern);
updated = true;
} else if (savedSearchPattern.equals(pattern)) {
Integer id = record.getAttributeAsInt(AbstractSearchStrategy.ATTR_ID);
updateSavedSearchName(id, savedSearchName);
updated = true;
}
}
if (!updated) {
createSavedSearch(savedSearchName, savedSearchPattern);
}
}
private synchronized void showList() {
if (!pickListGrid.isDrawn() || pickListGrid.isVisible()) {
populateInitialSearch();
} else if (!pickListGrid.isVisible()) {
populateInitialSearch();
}
}
private synchronized void hideList() {
if (pickListGrid.isVisible()) {
pickListGrid.hide();
}
}
public SearchMode getSearchMode() {
return searchMode;
}
public void setSearchMode(SearchMode searchMode) {
this.searchMode = searchMode;
}
private void configurePickListGrid() {
pickListGrid.setCellHeight(getSearchStrategy().getCellHeight());
pickListGrid.addRecordClickHandler(getSearchStrategy());
pickListGrid.addRecordDoubleClickHandler(getSearchStrategy());
pickListGrid.setCellFormatter(getSearchStrategy());
pickListGrid.setShowHeader(false);
}
private void showPickListGrid() {
Rectangle searchTextRect = searchTextItem.getPageRect();
pickListGrid.setLeft(searchTextRect.getLeft());
pickListGrid.setWidth(searchTextRect.getWidth());
pickListGrid.setTop(searchTextRect.getTop() + searchTextRect.getHeight());
pickListGrid.setHeight(PICKLIST_HEIGHT);
pickListGrid.setWidth(PICKLIST_HEIGHT);
pickListGrid.redraw();
}
/**
* This is the one method where the searchMode state is allowed to change.
*/
public void toggleFavoriteSearch() {
if (getSearchMode().equals(SearchMode.SAVED_SEARCH_MODE)) {
switchToBasicSearchMode();
} else {
switchToSavedSearchMode();
}
Log.debug("toggleFavorites searchMode set to: " + searchMode);
configurePickListGrid();
showPickListGrid();
}
public void switchToBasicSearchMode() {
pickListGrid.destroy();
pickListGrid = new ListGrid();
setSearchMode(SearchMode.BASIC_SEARCH_MODE);
saveSearchButton.setIcon(IconEnum.STAR_OFF.getIcon16x16Path());
saveSearchTextItem.hide();
configurePickListGrid();
populateInitialSearch();
}
public void switchToSavedSearchMode() {
pickListGrid.destroy();
pickListGrid = new ListGrid();
setSearchMode(SearchMode.SAVED_SEARCH_MODE);
saveSearchButton.setIcon(IconEnum.STAR_ON.getIcon16x16Path());
saveSearchTextItem.setValue(MSG.view_searchBar_savedSearch_namePrompt());
saveSearchTextItem.selectValue();
saveSearchTextItem.show();
configurePickListGrid();
populateInitialSearch();
}
public String getValue() {
return searchTextItem.getValueAsString();
}
private void createSavedSearch(final String name, final String pattern) {
Subject subject = UserSessionManager.getSessionSubject();
SavedSearch newSavedSearch = new SavedSearch(searchSubsystem, name, pattern, subject);
searchService.createSavedSearch(newSavedSearch, new AsyncCallback<Integer>() {
public void onSuccess(Integer newSavedSearchId) {
Message message = new Message(MSG.view_searchBar_savedSearch_save(name), Message.Severity.Info);
CoreGUI.getMessageCenter().notify(message);
getSearchStrategy().searchFocusHandler();
}
public void onFailure(Throwable caught) {
Message message = new Message(MSG.view_searchBar_savedSearch_failSave(name), Message.Severity.Error);
CoreGUI.getMessageCenter().notify(message);
}
});
}
private void updateSavedSearchName(final int id, final String name) {
Subject subject = UserSessionManager.getSessionSubject();
searchService.updateSavedSearchName(id, name, new AsyncCallback<Boolean>() {
public void onSuccess(Boolean result) {
Message message = new Message(MSG.view_searchBar_savedSearch_save(name), Message.Severity.Info);
CoreGUI.getMessageCenter().notify(message);
getSearchStrategy().searchFocusHandler();
}
@Override
public void onFailure(Throwable caught) {
Message message = new Message(MSG.view_searchBar_savedSearch_failRename(name), Message.Severity.Error);
CoreGUI.getMessageCenter().notify(message);
}
});
}
private void updateSavedSearchPattern(final int id, final String name, final String pattern) {
Subject subject = UserSessionManager.getSessionSubject();
searchService.updateSavedSearchPattern(id, pattern, new AsyncCallback<Boolean>() {
public void onSuccess(Boolean result) {
Message message = new Message(MSG.view_searchBar_savedSearch_save(name), Message.Severity.Info);
CoreGUI.getMessageCenter().notify(message);
getSearchStrategy().searchFocusHandler();
}
@Override
public void onFailure(Throwable caught) {
Message message = new Message(MSG.view_searchBar_savedSearch_failSave(name), Message.Severity.Error);
CoreGUI.getMessageCenter().notify(message);
}
});
}
public AbstractSearchStrategy getSearchStrategy() {
return searchStrategies.get(searchMode);
}
public boolean isFilterEnabled() {
return !SearchMode.SAVED_SEARCH_MODE.equals(getSearchMode());
}
public ListGrid getPickListGrid() {
return pickListGrid;
}
public TextItem getSearchTextItem() {
return searchTextItem;
}
public TextItem getSaveSearchTextItem() {
return saveSearchTextItem;
}
@Override
public void destroy() {
if (null != pickListGrid) {
pickListGrid.destroy();
}
super.destroy();
}
}