/* * RHQ Management Platform * Copyright (C) 2005-2011 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, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * 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 and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.coregui.client.components.table; import com.smartgwt.client.data.Criteria; import com.smartgwt.client.data.SortSpecifier; import com.smartgwt.client.types.AnimationEffect; import com.smartgwt.client.types.VerticalAlignment; import com.smartgwt.client.widgets.AnimationCallback; import com.smartgwt.client.widgets.Canvas; import com.smartgwt.client.widgets.events.DoubleClickEvent; import com.smartgwt.client.widgets.events.DoubleClickHandler; import com.smartgwt.client.widgets.grid.CellFormatter; import com.smartgwt.client.widgets.grid.ListGrid; import com.smartgwt.client.widgets.grid.ListGridField; import com.smartgwt.client.widgets.grid.ListGridRecord; import com.smartgwt.client.widgets.layout.HLayout; import com.smartgwt.client.widgets.layout.LayoutSpacer; import com.smartgwt.client.widgets.layout.VLayout; import org.rhq.coregui.client.BookmarkableView; import org.rhq.coregui.client.CoreGUI; import org.rhq.coregui.client.DetailsView; import org.rhq.coregui.client.InitializableView; import org.rhq.coregui.client.LinkManager; import org.rhq.coregui.client.ViewPath; import org.rhq.coregui.client.components.buttons.BackButton; import org.rhq.coregui.client.util.Log; import org.rhq.coregui.client.util.RPCDataSource; import org.rhq.coregui.client.util.StringUtility; import org.rhq.coregui.client.util.enhanced.EnhancedHLayout; import org.rhq.coregui.client.util.enhanced.EnhancedUtility; import org.rhq.coregui.client.util.enhanced.EnhancedVLayout; /** * Provides the typical table view with the additional ability of traversing to a "details" view * when double-clicking a individual row in the table - a masters/detail view in effect. * * @param <DS> the datasource used to obtain data for the table * @param <ID> the type used for IDs. This identifies the type used to uniquely refer to a row in the table * * @author Greg Hinkle * @author John Mazzitelli */ @SuppressWarnings("unchecked") public abstract class AbstractTableSection<DS extends RPCDataSource, ID> extends Table<DS> implements BookmarkableView, InitializableView { private VLayout detailsHolder; private Canvas detailsView; private Canvas header; private String basePath; private boolean escapeHtmlInDetailsLinkColumn; private boolean initialDisplay; private boolean initialized; protected AbstractTableSection(String tableTitle) { super(tableTitle); } protected AbstractTableSection(String tableTitle, Criteria criteria) { super(tableTitle, criteria); } protected AbstractTableSection(String tableTitle, SortSpecifier[] sortSpecifiers) { super(tableTitle, sortSpecifiers); } protected AbstractTableSection(String tableTitle, Criteria criteria, SortSpecifier[] sortSpecifiers) { super(tableTitle, sortSpecifiers, criteria); } protected AbstractTableSection(String tableTitle, boolean autoFetchData) { super(tableTitle, autoFetchData); } protected AbstractTableSection(String tableTitle, SortSpecifier[] sortSpecifiers, String[] excludedFieldNames) { super(tableTitle, null, sortSpecifiers, excludedFieldNames); } protected AbstractTableSection(String tableTitle, Criteria criteria, SortSpecifier[] sortSpecifiers, String[] excludedFieldNames) { super(tableTitle, criteria, sortSpecifiers, excludedFieldNames); } protected AbstractTableSection(String tableTitle, Criteria criteria, SortSpecifier[] sortSpecifiers, String[] excludedFieldNames, boolean autoFetchData) { super(tableTitle, criteria, sortSpecifiers, excludedFieldNames, autoFetchData); } @Override protected void onInit() { super.onInit(); this.initialDisplay = true; detailsHolder = new EnhancedVLayout(); detailsHolder.setAlign(VerticalAlignment.TOP); //detailsHolder.setWidth100(); //detailsHolder.setHeight100(); detailsHolder.hide(); addMember(detailsHolder); // if the detailsView is already defined it means we want the details view to be rendered prior to // the master view, probably due to a direct navigation or refresh (like F5 when sitting on the details page) if (null != detailsView) { switchToDetailsView(); } this.initialized = true; } @Override public void destroy() { this.initialized = false; super.destroy(); } @Override public boolean isInitialized() { return super.isInitialized() && this.initialized; } /** * The default implementation wraps the {@link #getDetailsLinkColumnCellFormatter()} column with the * {@link #getDetailsLinkColumnCellFormatter()}. This is typically the 'name' column linking to the detail * view, given the 'id'. Also, establishes a double click handler for the row which invokes * {@link #showDetails(com.smartgwt.client.widgets.grid.ListGridRecord)}</br> * </br> * In general, in overrides, call super.configureTable *after* manipulating the ListGrid fields. * * @see org.rhq.coregui.client.components.table.Table#configureTable() */ @Override protected void configureTable() { if (isDetailsEnabled()) { ListGrid grid = getListGrid(); // Make the value of some specific field a link to the details view for the corresponding record. ListGridField field = (grid != null) ? grid.getField(getDetailsLinkColumnName()) : null; if (field != null) { field.setCellFormatter(getDetailsLinkColumnCellFormatter()); } setListGridDoubleClickHandler(new DoubleClickHandler() { @Override public void onDoubleClick(DoubleClickEvent event) { ListGrid listGrid = (ListGrid) event.getSource(); ListGridRecord[] selectedRows = listGrid.getSelectedRecords(); if (selectedRows != null && selectedRows.length == 1) { showDetails(selectedRows[0]); } } }); } } protected boolean isDetailsEnabled() { return true; } public void setEscapeHtmlInDetailsLinkColumn(boolean escapeHtmlInDetailsLinkColumn) { this.escapeHtmlInDetailsLinkColumn = escapeHtmlInDetailsLinkColumn; } /** * Override if you don't want FIELD_NAME to be wrapped ina link. * @return the name of the field to be wrapped, or null if no field should be wrapped. */ protected String getDetailsLinkColumnName() { return FIELD_NAME; } /** * Override if you don't want the detailsLinkColumn to have the default link wrapper. * @return the desired CellFormatter. */ protected CellFormatter getDetailsLinkColumnCellFormatter() { return new CellFormatter() { public String format(Object value, ListGridRecord record, int i, int i1) { if (value == null) { return ""; } ID recordId = getId(record); String detailsUrl = "#" + getBasePath() + "/" + convertIDToCurrentViewPath(recordId); String formattedValue = (escapeHtmlInDetailsLinkColumn) ? StringUtility.escapeHtml(value.toString()) : value.toString(); return LinkManager.getHref(detailsUrl, formattedValue); } }; } /** * Shows the details view for the given record of the table. * * The default implementation of this method assumes there is an * id attribute on the record and passes it to {@link #showDetails(Object)}. * Subclasses are free to override this behavior. Subclasses usually * will need to set the {@link #setDetailsView(Canvas) details view} * explicitly. * * @param record the record whose details are to be shown */ public void showDetails(ListGridRecord record) { if (record == null) { throw new IllegalArgumentException("'record' parameter is null."); } ID id = getId(record); showDetails(id); } /** * Returns the details canvas with information on the item given its list grid record. * * The default implementation of this method is to assume there is an * id attribute on the record and pass that ID to {@link #getDetailsView(Object)}. * Subclasses are free to override this - which you usually want to do * if you know the full details of the item are stored in the record attributes * and thus help avoid making a round trip to the DB. * * @param record the record of the item whose details to be shown; ; null if empty details view should be shown. */ public Canvas getDetailsView(ListGridRecord record) { ID id = getId(record); return getDetailsView(id); } /** * Subclasses define how they want to format their identifiers. These uniquely identify * rows in the table. Typical values/types for IDs are Integers or Strings. * * @param record the individual record that contains the ID to be extracted and returned * * @return the ID of the given row/record from the table. */ protected abstract ID getId(ListGridRecord record); /** * Shows empty details for a new item being created. * This method is usually called when a user clicks a 'New' button. * * Subclasses are free to override this if they need a custom way to show the details view. * * @see #showDetails(ListGridRecord) */ public void newDetails() { CoreGUI.goToView(basePath + "/0"); // assumes the subclasses will understand "0" means "new details page" } /** * Shows the details for an item that has the given ID. * This method is usually called when a user goes to the details * page via a bookmark, double-cick on a list view row, or direct link. * * @param id the id of the row whose details are to be shown; Must be a valid ID. * * @see #showDetails(ListGridRecord) * * @throws IllegalArgumentException if id is invalid */ public abstract void showDetails(ID id); /** * Returns the details canvas with information on the item that has the given ID. * Note that an empty details view should be returned if the id passed in is 0 (as would * be the case if a new item is to be created using the details view). * * @param id the id of the details to be shown; will be "0" if an empty details view should be shown. */ public abstract Canvas getDetailsView(ID id); /** * Given the path from the URL that identifies the ID, this returns the ID represented by that path string. * @param path the path as it was found in the current view path (i.e. in the URL) * @return the ID that identifies the item referred to by the URL */ protected abstract ID convertCurrentViewPathToID(String path); /** * Given the ID of a particular item, this returns a path string suitable for placement in a URL such that that URL will * identify the particular item. * * @return how the ID can be represented within a view path (i.e. in a URL) * @param id the ID that identifies the item to be referred by in a URL */ protected abstract String convertIDToCurrentViewPath(ID id); @Override public void renderView(ViewPath viewPath) { this.basePath = viewPath.getPathToCurrent(); if (!viewPath.isEnd()) { ID id = convertCurrentViewPathToID(viewPath.getCurrent().getPath()); this.detailsView = getDetailsView(id); if (this.detailsView instanceof BookmarkableView) { ((BookmarkableView) this.detailsView).renderView(viewPath); } switchToDetailsView(); } else { switchToTableView(); } } protected String getBasePath() { return this.basePath; } /** * For use by subclasses that want to define their own details view. * * @param detailsView the new details view */ protected void setDetailsView(Canvas detailsView) { this.detailsView = detailsView; } /** * Switches to viewing the details canvas, hiding the table. This does not * do anything with reloading data or switching to the selected row in the table; * this only changes the visibility of canvases. */ protected void switchToDetailsView() { Canvas contents = getTableContents(); // If the Table has not yet been initialized then ignore if (contents != null) { // If the table view is visible then gracefully switch to the details view. if (contents.isVisible()) { contents.animateHide(AnimationEffect.WIPE, new AnimationCallback() { @Override public void execute(boolean b) { buildDetailsView(); } }); } else { // Even if the table view is not visible, it may not be hidden. Instead, it may be the // case that its parent (the encompassing Table/HLayout) may not be visible. This is unusual // because typically we switch between the table and detail view while under the subtab, but // if we navigate to the detail view from another subtab (for example, the drift tree context // menu) the Table may not be visible and the table view may not be hidden. To make a long // story short, ensure the table view is hidden when displaying the details view. contents.hide(); /* * if the programmer chooses to go directly from the detailView in create-mode to the * detailsView in edit-mode, the content canvas will already be hidden, which means the * animateHide would be a no-op (the event won't fire). this causes the detailsHolder * to keep a reference to the previous detailsView (the one in create-mode) instead of the * newly returned reference from getDetailsView(ID) that was called when the renderView * methods were called hierarchically down to render the new detailsView in edit-mode. * therefore, we need to explicitly destroy what's already there (presumably the detailsView * in create-mode), and then rebuild it (presumably the detailsView in edit-mode). */ EnhancedUtility.destroyMembers(detailsHolder); buildDetailsView(); } } } private void buildDetailsView() { detailsView.setWidth100(); detailsView.setHeight100(); boolean isEditable = (detailsView instanceof DetailsView && ((DetailsView) detailsView).isEditable()); if (!isEditable) { // Only add the "Back to List" button if the details are definitely not editable, because if they are // editable, a Cancel button should already be provided by the details view. BackButton backButton = new BackButton(MSG.view_tableSection_backButton(), basePath); HLayout hlayout = new EnhancedHLayout(); hlayout.addMember(backButton); if (header != null) { header.setWidth100(); header.setAlign(com.smartgwt.client.types.Alignment.CENTER); hlayout.addMember(header); } detailsHolder.addMember(hlayout); LayoutSpacer verticalSpacer = new LayoutSpacer(); verticalSpacer.setHeight(8); detailsHolder.addMember(verticalSpacer); } detailsHolder.addMember(detailsView); detailsHolder.animateShow(AnimationEffect.WIPE); } /** * Switches to viewing the table, hiding the details canvas. */ protected void switchToTableView() { final Canvas contents = getTableContents(); if (contents != null) { // If this is not the initial display of the table, refresh the table's data. Otherwise, a refresh would be // redundant, since the data was just loaded when the table was drawn. if (this.initialDisplay) { this.initialDisplay = false; } else { Log.debug("Refreshing data for Table [" + getClass().getName() + "]..."); refresh(); } // if the detailsHolder is visible then gracefully switch views, otherwise just // clean up any lingering details holder and show the table view. if (detailsHolder != null && detailsHolder.isVisible()) { detailsHolder.animateHide(AnimationEffect.WIPE, new AnimationCallback() { @Override public void execute(boolean b) { EnhancedUtility.destroyMembers(detailsHolder); contents.animateShow(AnimationEffect.WIPE); } }); } else { if (detailsHolder != null) { EnhancedUtility.destroyMembers(detailsHolder); } contents.animateShow(AnimationEffect.WIPE); } } } public void setHeader(Canvas header) { this.header = header; } }