blob: 462bc967f812b744f9e753a7cc85b5436cf2bf28 [file] [log] [blame]
/*
* Copyright 2010 Google Inc.
*
* 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.gwt.sample.expenses.client;
import com.google.gwt.activity.shared.Activity;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.DateCell;
import com.google.gwt.cell.client.TextCell;
import com.google.gwt.cell.client.ValueUpdater;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.sample.expenses.client.place.ReportListPlace;
import com.google.gwt.sample.expenses.client.style.Styles;
import com.google.gwt.sample.expenses.shared.EmployeeProxy;
import com.google.gwt.sample.expenses.shared.ExpensesRequestFactory;
import com.google.gwt.sample.expenses.shared.ReportProxy;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiFactory;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.cellview.client.Column;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.cellview.client.SimplePager;
import com.google.gwt.user.cellview.client.SimplePager.TextLocation;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.NoSelectionModel;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.Range;
import com.google.gwt.view.client.RangeChangeEvent;
import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.web.bindery.requestfactory.gwt.ui.client.EntityProxyKeyProvider;
import com.google.web.bindery.requestfactory.shared.EntityProxyChange;
import com.google.web.bindery.requestfactory.shared.EntityProxyId;
import com.google.web.bindery.requestfactory.shared.Receiver;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* The list of expense reports on the right side of the app.
*/
public class ExpenseReportList extends Composite implements
EntityProxyChange.Handler<ReportProxy>, Activity {
interface Binder extends UiBinder<Widget, ExpenseReportList> {
}
/**
* Custom listener for this widget.
*/
interface Listener {
/**
* Called when the user selects a report.
*
* @param report the selected report
*/
void onReportSelected(ReportProxy report);
}
/**
* The resources applied to the table.
*/
interface TableResources extends CellTable.Resources {
@Source({CellTable.Style.DEFAULT_CSS, "ExpenseListCellTable.css"})
TableStyle cellTableStyle();
}
/**
* The styles applied to the table.
*/
interface TableStyle extends CellTable.Style {
}
/**
* A text box that displays default text.
*/
private static class DefaultTextBox extends TextBox {
/**
* The text color used when the box is disabled and empty.
*/
private static final String TEXTBOX_DISABLED_COLOR = "#aaaaaa";
private final String defaultText;
public DefaultTextBox(final String defaultText) {
this.defaultText = defaultText;
resetDefaultText();
// Add focus and blur handlers.
addFocusHandler(new FocusHandler() {
public void onFocus(FocusEvent event) {
getElement().getStyle().clearColor();
if (defaultText.equals(getText())) {
setText("");
}
}
});
addBlurHandler(new BlurHandler() {
public void onBlur(BlurEvent event) {
if ("".equals(getText())) {
resetDefaultText();
}
}
});
}
public String getDefaultText() {
return defaultText;
}
/**
* Reset the text box to the default text.
*/
public void resetDefaultText() {
setText(defaultText);
getElement().getStyle().setColor(TEXTBOX_DISABLED_COLOR);
}
}
/**
* A cell used to highlight search text.
*/
private class HighlightCell extends AbstractCell<String> {
private static final String replaceString = "<span style='color:red;font-weight:bold;'>$1</span>";
@Override
public void render(Context context, String value, SafeHtmlBuilder sb) {
if (value != null) {
if (searchRegExp != null) {
// The search regex has already been html-escaped
value = searchRegExp.replace(SafeHtmlUtils.htmlEscape(value),
replaceString);
sb.append(SafeHtmlUtils.fromTrustedString(value));
} else {
sb.appendEscaped(value);
}
}
}
}
private static final ProvidesKey<ReportProxy> keyProvider = new EntityProxyKeyProvider<ReportProxy>();
/**
* The auto refresh interval in milliseconds.
*/
private static final int REFRESH_INTERVAL = 5000;
private static Binder uiBinder = GWT.create(Binder.class);
/**
* Utility method to get the first part of the breadcrumb based on the
* department and employee.
*
* @param department the selected department
* @param employee the selected employee
* @return the breadcrumb
*/
public static String getBreadcrumb(String department, EmployeeProxy employee) {
assert null != department;
if (employee != null) {
return "Reports for " + employee.getDisplayName();
} else if (!"".equals(department)) {
return "Reports for " + department;
} else {
return "All Reports";
}
}
@UiField
Element breadcrumb;
@UiField
SimplePager pager;
@UiField
Image searchButton;
@UiField(provided = true)
DefaultTextBox searchBox;
/**
* The main table. We provide this in the constructor before calling
* {@link UiBinder#createAndBindUi(Object)} because the pager depends on it.
*/
@UiField(provided = true)
CellTable<ReportProxy> table;
private final List<SortableHeader> allHeaders = new ArrayList<SortableHeader>();
/**
* The department being searched.
*/
private String department;
/**
* The employee being searched.
*/
private EmployeeProxy employee;
/**
* Indicates that the report count is stale.
*/
private boolean isCountStale = true;
/**
* The field to sort by.
*/
private String orderBy = "purpose";
/**
* The set of Report keys that we have seen. When a new key is added, we
* compare it to the list of known keys to determine if it is new.
*/
private Set<Object> knownReportKeys = null;
/**
* Keep track of the last receiver so that we know if a response is stale.
*/
private Receiver<List<ReportProxy>> lastDataReceiver;
/**
* Keep track of the last receiver so that we know if a response is stale.
*/
private Receiver<Long> lastDataSizeReceiver;
private Listener listener;
/**
* The {@link Timer} used to periodically refresh the table.
*/
private final Timer refreshTimer = new Timer() {
@Override
public void run() {
isCountStale = true;
requestReports(true);
}
};
/**
* The columns to request with each report.
*/
private final String[] reportColumns = new String[] {
"created", "purpose", "notes"};
/**
* The factory used to send requests.
*/
private final ExpensesRequestFactory requestFactory;
/**
* The string that the user searched for.
*/
private RegExp searchRegExp;
/**
* The starts with search string.
*/
private String startsWithSearch;
private ReportListPlace place;
private boolean running;
public ExpenseReportList(ExpensesRequestFactory requestFactory) {
this.requestFactory = requestFactory;
// Initialize the widget.
createTable();
table.addRangeChangeHandler(new RangeChangeEvent.Handler() {
public void onRangeChange(RangeChangeEvent event) {
requestReports(false);
}
});
searchBox = new DefaultTextBox("search");
initWidget(uiBinder.createAndBindUi(this));
// Listen for key events from the text boxes.
searchBox.addKeyUpHandler(new KeyUpHandler() {
public void onKeyUp(KeyUpEvent event) {
// Search on enter.
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
search();
return;
}
// Highlight as the user types.
String text = SafeHtmlUtils.htmlEscape(searchBox.getText());
if (text.length() > 0) {
searchRegExp = RegExp.compile("(" + text + ")", "ig");
} else {
searchRegExp = null;
}
table.redraw();
}
});
searchButton.addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
search();
}
});
}
public String mayStop() {
return null;
}
public void onCancel() {
onStop();
}
public void onProxyChange(EntityProxyChange<ReportProxy> event) {
EntityProxyId<ReportProxy> changedId = event.getProxyId();
List<ReportProxy> records = table.getVisibleItems();
int i = 0;
for (ReportProxy record : records) {
if (record != null && changedId.equals(record.stableId())) {
List<ReportProxy> changedList = new ArrayList<ReportProxy>();
changedList.add(record);
table.setRowData(i + table.getPageStart(), changedList);
}
i++;
}
}
public void onStop() {
running = false;
refreshTimer.cancel();
}
public void setListener(Listener listener) {
this.listener = listener;
}
public void start(AcceptsOneWidget panel, EventBus eventBus) {
running = true;
doUpdateForPlace();
EntityProxyChange.registerForProxyType(eventBus, ReportProxy.class, this);
requestReports(false);
panel.setWidget(this);
}
/**
* In this application, called by {@link ExpensesActivityMapper} each time a
* ReportListPlace is posted. In a more typical set up, this would be a
* constructor argument to a one shot activity, perhaps managing a shared
* widget view instance.
*/
public void updateForPlace(final ReportListPlace place) {
this.place = place;
if (running) {
doUpdateForPlace();
}
}
@UiFactory
SimplePager createPager() {
SimplePager p = new SimplePager(TextLocation.RIGHT);
p.setDisplay(table);
p.setRangeLimited(true);
return p;
}
/**
* Add a sortable column to the table.
*
* @param <C> the data type for the column
* @param text the header text
* @param cell the cell used to render the column
* @param getter the getter to retrieve the value for the column
* @param property the property to sort by
* @return the column
*/
private <C> Column<ReportProxy, C> addColumn(final String text,
final Cell<C> cell, final GetValue<ReportProxy, C> getter,
final String property) {
final Column<ReportProxy, C> column = new Column<ReportProxy, C>(cell) {
@Override
public C getValue(ReportProxy object) {
return getter.getValue(object);
}
};
final SortableHeader header = new SortableHeader(text);
allHeaders.add(header);
// Sort created by default.
if ("created".equals(property)) {
header.setSorted(true);
header.setReverseSort(true);
orderBy = "created" + " DESC";
}
header.setUpdater(new ValueUpdater<String>() {
public void update(String value) {
header.setSorted(true);
header.toggleReverseSort();
for (SortableHeader otherHeader : allHeaders) {
if (otherHeader != header) {
otherHeader.setSorted(false);
otherHeader.setReverseSort(true);
}
}
table.redrawHeaders();
// Request sorted rows.
orderBy = property;
if (header.getReverseSort()) {
orderBy += " DESC";
}
searchBox.resetDefaultText();
searchRegExp = null;
// Go to the first page of the newly-sorted results
pager.firstPage();
requestReports(false);
}
});
table.addColumn(column, header);
return column;
}
/**
* Create the {@link CellTable}.
*/
private void createTable() {
CellTable.Resources resources = GWT.create(TableResources.class);
table = new CellTable<ReportProxy>(20, resources);
table.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
Styles.Common common = Styles.common();
table.addColumnStyleName(0, common.spacerColumn());
table.addColumnStyleName(1, common.expenseListPurposeColumn());
table.addColumnStyleName(3, common.expenseListDepartmentColumn());
table.addColumnStyleName(4, common.expenseListCreatedColumn());
table.addColumnStyleName(5, common.spacerColumn());
// Add a selection model.
final NoSelectionModel<ReportProxy> selectionModel = new NoSelectionModel<ReportProxy>();
table.setSelectionModel(selectionModel);
selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
public void onSelectionChange(SelectionChangeEvent event) {
Object selected = selectionModel.getLastSelectedObject();
if (selected != null && listener != null) {
listener.onReportSelected((ReportProxy) selected);
}
}
});
// Spacer column.
table.addColumn(new SpacerColumn<ReportProxy>());
// Purpose column.
addColumn("Purpose", new HighlightCell(),
new GetValue<ReportProxy, String>() {
public String getValue(ReportProxy object) {
return object.getPurpose();
}
}, "purpose");
// Notes column.
addColumn("Notes", new HighlightCell(),
new GetValue<ReportProxy, String>() {
public String getValue(ReportProxy object) {
return object.getNotes();
}
}, "notes");
// Department column.
addColumn("Department", new TextCell(),
new GetValue<ReportProxy, String>() {
public String getValue(ReportProxy object) {
return object.getDepartment();
}
}, "department");
// Created column.
addColumn("Created", new DateCell(DateTimeFormat.getFormat("MMM dd yyyy")),
new GetValue<ReportProxy, Date>() {
public Date getValue(ReportProxy object) {
return object.getCreated();
}
}, "created");
// Spacer column.
table.addColumn(new SpacerColumn<ReportProxy>());
}
private void doUpdateForPlace() {
if (place.getEmployeeId() == null) {
findDepartmentOrEmployee(place.getDepartment(), null);
} else {
requestFactory.find(place.getEmployeeId()).fire(
new Receiver<EmployeeProxy>() {
@Override
public void onSuccess(EmployeeProxy response) {
findDepartmentOrEmployee("", response);
}
});
}
}
/**
* Set the current department and employee to filter on.
*
* @param department the department, or null if none selected
* @param employee the employee, or null if none selected
*/
private void findDepartmentOrEmployee(String department,
EmployeeProxy employee) {
this.department = department;
this.employee = employee;
isCountStale = true;
searchBox.resetDefaultText();
startsWithSearch = null;
breadcrumb.setInnerText(getBreadcrumb(department, employee));
searchRegExp = null;
pager.setPageStart(0);
requestReports(false);
}
/**
* Send a request for reports in the current range.
*
* @param isPolling true if this request is caused by polling
*/
private void requestReports(boolean isPolling) {
// Cancel the refresh timer.
refreshTimer.cancel();
// Early exit if we don't have a request factory to request from.
if (requestFactory == null) {
return;
}
// Clear the known keys.
if (!isPolling) {
knownReportKeys = null;
}
// Get the parameters.
String startsWith = startsWithSearch;
if (startsWith == null || searchBox.getDefaultText().equals(startsWith)) {
startsWith = "";
}
Range range = table.getVisibleRange();
Long employeeId = employee == null ? -1 : new Long(employee.getId());
String dept = department == null ? "" : department;
// If a search string is specified, the results will not be sorted.
if (startsWith.length() > 0) {
for (SortableHeader header : allHeaders) {
header.setSorted(false);
header.setReverseSort(false);
}
table.redrawHeaders();
}
// Request the total data size.
if (isCountStale) {
isCountStale = false;
if (!isPolling) {
pager.startLoading();
}
lastDataSizeReceiver = new Receiver<Long>() {
@Override
public void onSuccess(Long response) {
if (this == lastDataSizeReceiver) {
int count = response.intValue();
// Treat count == 1000 as inexact due to AppEngine limitation
table.setRowCount(count, count != 1000);
}
}
};
requestFactory.reportRequest().countReportsBySearch(employeeId, dept,
startsWith).fire(lastDataSizeReceiver);
}
// Request reports in the current range.
lastDataReceiver = new Receiver<List<ReportProxy>>() {
@Override
public void onSuccess(List<ReportProxy> newValues) {
if (this == lastDataReceiver) {
int size = newValues.size();
if (size < table.getPageSize()) {
// Now we know the exact data size
table.setRowCount(table.getPageStart() + size, true);
}
if (size > 0) {
table.setRowData(table.getPageStart(), newValues);
}
// Add the new keys to the known keys.
boolean isInitialData = knownReportKeys == null;
if (knownReportKeys == null) {
knownReportKeys = new HashSet<Object>();
}
for (ReportProxy value : newValues) {
Object key = keyProvider.getKey(value);
if (!isInitialData && !knownReportKeys.contains(key)) {
new PhaseAnimation.CellTablePhaseAnimation<ReportProxy>(table,
value, keyProvider).run();
}
knownReportKeys.add(key);
}
}
refreshTimer.schedule(REFRESH_INTERVAL);
}
};
requestFactory.reportRequest().findReportEntriesBySearch(employeeId, dept,
startsWith, orderBy, range.getStart(), range.getLength()).with(
reportColumns).fire(lastDataReceiver);
}
/**
* Search based on the search box text.
*/
private void search() {
isCountStale = true;
startsWithSearch = searchBox.getText();
requestReports(false);
}
}