/* * 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 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.components.selector; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.Timer; import com.smartgwt.client.data.Criteria; import com.smartgwt.client.data.DSCallback; import com.smartgwt.client.data.DSRequest; import com.smartgwt.client.data.DSResponse; import com.smartgwt.client.data.Record; import com.smartgwt.client.types.Alignment; import com.smartgwt.client.types.DragDataAction; import com.smartgwt.client.types.DragTrackerMode; import com.smartgwt.client.types.ListGridFieldType; import com.smartgwt.client.types.SelectionStyle; import com.smartgwt.client.types.VerticalAlignment; import com.smartgwt.client.widgets.ImgProperties; import com.smartgwt.client.widgets.Label; import com.smartgwt.client.widgets.TransferImgButton; import com.smartgwt.client.widgets.events.ClickEvent; import com.smartgwt.client.widgets.events.ClickHandler; import com.smartgwt.client.widgets.events.DoubleClickEvent; import com.smartgwt.client.widgets.events.DoubleClickHandler; import com.smartgwt.client.widgets.events.KeyPressEvent; import com.smartgwt.client.widgets.events.KeyPressHandler; import com.smartgwt.client.widgets.form.DynamicForm; import com.smartgwt.client.widgets.form.events.ItemChangedEvent; import com.smartgwt.client.widgets.form.events.ItemChangedHandler; import com.smartgwt.client.widgets.grid.HoverCustomizer; 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.grid.events.RecordDropEvent; import com.smartgwt.client.widgets.grid.events.RecordDropHandler; import com.smartgwt.client.widgets.grid.events.SelectionChangedHandler; import com.smartgwt.client.widgets.grid.events.SelectionEvent; import com.smartgwt.client.widgets.layout.HLayout; import com.smartgwt.client.widgets.layout.LayoutSpacer; import com.smartgwt.client.widgets.layout.SectionStack; import com.smartgwt.client.widgets.layout.SectionStackSection; import com.smartgwt.client.widgets.layout.VStack; import org.rhq.coregui.client.ImageManager; import org.rhq.coregui.client.util.RPCDataSource; import org.rhq.coregui.client.util.enhanced.EnhancedUtility; import org.rhq.coregui.client.util.enhanced.EnhancedVLayout; import org.rhq.coregui.client.util.enhanced.EnhancedVStack; /** * @author Greg Hinkle * @author Ian Springer */ public abstract class AbstractSelector<T, C extends org.rhq.core.domain.criteria.Criteria> extends EnhancedVLayout { private static final String SELECTOR_KEY = "id"; // We only make a single fetch request to load the available records. This is the maximum number we will allow the // DataSource to return. If we don't manage to load the entire data set, we'll display a warning message telling the // user they need to define some filters. private static final int MAX_AVAILABLE_RECORDS = 100; protected ListGridRecord[] initialSelection; protected List<Record> availableRecords; protected DynamicForm availableFilterForm; protected HLayout hlayout; protected ListGrid availableGrid; protected ListGrid assignedGrid; protected RPCDataSource<T, C> datasource; private Set<AssignedItemsChangedHandler> assignedItemsChangedHandlers = new HashSet<AssignedItemsChangedHandler>(); protected TransferImgButton addButton; protected TransferImgButton removeButton; protected TransferImgButton addAllButton; protected TransferImgButton removeAllButton; protected Criteria latestCriteria; private boolean isReadOnly; private Label messageLayout; public AbstractSelector() { this(false); } public AbstractSelector(boolean isReadOnly) { super(); this.isReadOnly = isReadOnly; setWidth100(); setMargin(7); this.hlayout = new HLayout(); this.assignedGrid = new ListGrid(); if (this.isReadOnly) { this.assignedGrid.setSelectionType(SelectionStyle.NONE); } else { this.availableGrid = new ListGrid(); } } public void setAssigned(ListGridRecord[] assignedRecords) { initialSelection = assignedRecords; } /** * Returns the set of currently selected {@link Record record}s. * * @return the set of currently selected {@link Record record}s */ public ListGridRecord[] getSelectedRecords() { return this.assignedGrid.getRecords(); } /** * Returns the set of currently selected {@link T item}s. * * @return the set of currently selected {@link T item}s */ public Set<T> getSelectedItems() { ListGridRecord[] selectedRecords = this.assignedGrid.getRecords(); return getDataSource().buildDataObjects(selectedRecords); } /** * Returns the IDs of the currently selected items * * @return the IDs of the currently selected items */ public Set<Integer> getSelection() { ListGridRecord[] selectedRecords = this.assignedGrid.getRecords(); Set<Integer> ids = new HashSet<Integer>(selectedRecords.length); for (ListGridRecord selectedRecord : selectedRecords) { Integer id = selectedRecord.getAttributeAsInt(getSelectorKey()); ids.add(id); } return ids; } protected abstract DynamicForm getAvailableFilterForm(); protected abstract RPCDataSource<T, C> getDataSource(); protected abstract Criteria getLatestCriteria(DynamicForm availableFilterForm); /** * Subclasses can override this if they want an icon displayed next to each item in the list grids. * * @return the icon to be displayed, or null if no icon should be displayed */ protected String getItemIcon() { return null; } @Override protected void onInit() { super.onInit(); this.hlayout.setAlign(Alignment.LEFT); if (!this.isReadOnly) { // LEFT SIDE this.messageLayout = new Label(); this.messageLayout.setMargin(3); this.messageLayout.setAutoHeight(); addMember(this.messageLayout); this.availableFilterForm = getAvailableFilterForm(); if (this.availableFilterForm != null) { addMember(this.availableFilterForm); LayoutSpacer spacer = new LayoutSpacer(); spacer.setHeight(10); addMember(spacer); } SectionStack availableItemsStack = buildAvailableItemsStack(); hlayout.addMember(availableItemsStack); // CENTER BUTTONS VStack moveButtonStack = buildButtonStack(); hlayout.addMember(moveButtonStack); } // RIGHT SIDE SectionStack assignedItemsStack = buildAssignedItemsStack(); this.hlayout.addMember(assignedItemsStack); // initialize the state of the buttons - allows subclasses to tweek buttons on init time updateButtonEnablement(); addMember(this.hlayout); } @Override public void destroy() { // explicitly destroy non-enhanced member layouts EnhancedUtility.destroyMembers(hlayout); super.destroy(); // For reasons unknown, possibly issues in smartgwt's cleanup of VStack and SectionStack, these // widgets did not always get destroyed (for example, if something was moved to assigned, but nothing // was moved to available - go figure), so destroy them manually when the other cleanup is already done. if (null != availableGrid) { availableGrid.removeFromParent(); availableGrid.destroy(); } if (null != assignedGrid) { assignedGrid.removeFromParent(); assignedGrid.destroy(); } if (null != addButton) { addButton.removeFromParent(); addButton.destroy(); } if (null != addAllButton) { addAllButton.removeFromParent(); addAllButton.destroy(); } if (null != removeButton) { removeButton.removeFromParent(); removeButton.destroy(); } if (null != removeAllButton) { removeAllButton.removeFromParent(); removeAllButton.destroy(); } } private SectionStack buildAvailableItemsStack() { SectionStack availableSectionStack = new SectionStack(); availableSectionStack.setWidth("*"); availableSectionStack.setHeight100(); SectionStackSection availableSection = new SectionStackSection(getAvailableItemsGridTitle()); availableSection.setCanCollapse(false); availableSection.setExpanded(true); // Drag'n'Drop Settings this.availableGrid.setCanReorderRecords(true); this.availableGrid.setCanDragRecordsOut(true); this.availableGrid.setDragDataAction(DragDataAction.MOVE); if (getItemIcon() != null) { this.availableGrid.setDragTrackerMode(DragTrackerMode.ICON); this.availableGrid.setTrackerImage(new ImgProperties(getItemIcon(), 16, 16)); } this.availableGrid.setCanAcceptDroppedRecords(true); this.availableGrid.setLoadingMessage(MSG.common_msg_loading()); this.availableGrid.setEmptyMessage(MSG.common_msg_noItemsToShow()); List<ListGridField> availableFields = new ArrayList<ListGridField>(); String itemIcon = getItemIcon(); if (itemIcon != null) { ListGridField iconField = new ListGridField("icon", 25); iconField.setType(ListGridFieldType.ICON); iconField.setCellIcon(itemIcon); iconField.setShowDefaultContextMenu(false); availableFields.add(iconField); } ListGridField nameField = new ListGridField(getNameField(), MSG.common_title_name()); if (supportsNameHoverCustomizer()) { nameField.setShowHover(true); nameField.setHoverCustomizer(getNameHoverCustomizer()); } availableFields.add(nameField); this.availableGrid.setFields(availableFields.toArray(new ListGridField[availableFields.size()])); availableSection.setItems(this.availableGrid); availableSectionStack.addSection(availableSection); this.datasource = getDataSource(); this.datasource.setDataPageSize(getMaxAvailableRecords()); // Load data. if (this.availableFilterForm != null) { // this grabs any initial criteria prior to the first data fetch latestCriteria = getLatestCriteria(availableFilterForm); this.availableFilterForm.addItemChangedHandler(new ItemChangedHandler() { public void onItemChanged(ItemChangedEvent itemChangedEvent) { latestCriteria = getLatestCriteria(availableFilterForm); Timer timer = new Timer() { @Override public void run() { if (latestCriteria != null) { Criteria criteria = latestCriteria; latestCriteria = null; populateAvailableGrid(criteria); } } }; timer.schedule(500); } }); } populateAvailableGrid((null == latestCriteria) ? new Criteria() : latestCriteria); // Add event handlers. this.availableGrid.addSelectionChangedHandler(new SelectionChangedHandler() { public void onSelectionChanged(SelectionEvent selectionEvent) { updateButtonEnablement(); } }); this.availableGrid.addDoubleClickHandler(new DoubleClickHandler() { public void onDoubleClick(DoubleClickEvent event) { addSelectedRows(); } }); this.availableGrid.addRecordDropHandler(new RecordDropHandler() { public void onRecordDrop(RecordDropEvent recordDropEvent) { removeSelectedRows(); recordDropEvent.cancel(); } }); return availableSectionStack; } protected void populateAvailableGrid(Criteria criteria) { // TODO until http://code.google.com/p/smartgwt/issues/detail?id=490 is fixed always go to the server for data if (datasource == null) { datasource = getDataSource(); } datasource.invalidateCache(); DSRequest requestProperties = new DSRequest(); requestProperties.setStartRow(0); requestProperties.setEndRow(getMaxAvailableRecords()); this.datasource.fetchData(criteria, new DSCallback() { public void execute(DSResponse response, Object rawData, DSRequest request) { try { availableRecords = new ArrayList<Record>(); Record[] allRecords = response.getData(); int assignedNumber = doPostPopulateAvailableGrid(allRecords); int totalRecords = (response.getTotalRows() != null) ? response.getTotalRows() : allRecords.length; int totalAvailableRecords = totalRecords - assignedNumber; if (availableRecords.size() < totalAvailableRecords) { messageLayout.setContents(imgHTML(ImageManager.getAvailabilityYellowIcon()) + " " + MSG.view_selector_availableLessThanTotalAvailable( String.valueOf(availableRecords.size()), String.valueOf(totalAvailableRecords), getItemTitle(), getItemTitle())); } else { // Clear the warning message, if any, from the previous fetch. // Note, surprisingly, setContents(null) doesn't work. if (messageLayout != null) { messageLayout.setContents(" "); } } if (messageLayout != null) { messageLayout.markForRedraw(); } availableGrid.setData(availableRecords.toArray(new Record[availableRecords.size()])); } finally { updateButtonEnablement(); } } }, requestProperties); } protected int doPostPopulateAvailableGrid(Record[] allRecords) { ListGridRecord[] assignedRecords = assignedGrid.getRecords(); if (assignedRecords.length != 0) { Set<String> selectedRecordIds = new HashSet<String>(assignedRecords.length); for (Record record : assignedRecords) { String id = record.getAttribute(getSelectorKey()); selectedRecordIds.add(id); } for (Record record : allRecords) { String id = record.getAttribute(getSelectorKey()); if (!selectedRecordIds.contains(id)) { availableRecords.add(record); } } } else { availableRecords.addAll(Arrays.asList(allRecords)); } return assignedRecords.length; } private VStack buildButtonStack() { VStack moveButtonStack = new EnhancedVStack(6); moveButtonStack.setWidth(42); moveButtonStack.setHeight(250); moveButtonStack.setAlign(VerticalAlignment.CENTER); this.addButton = new TransferImgButton(TransferImgButton.RIGHT); this.addButton.addClickHandler(new ClickHandler() { public void onClick(ClickEvent clickEvent) { addSelectedRows(); } }); moveButtonStack.addMember(this.addButton); this.removeButton = new TransferImgButton(TransferImgButton.LEFT); this.removeButton.addClickHandler(new ClickHandler() { public void onClick(ClickEvent clickEvent) { removeSelectedRows(); } }); moveButtonStack.addMember(this.removeButton); this.addAllButton = new TransferImgButton(TransferImgButton.RIGHT_ALL); this.addAllButton.addClickHandler(new ClickHandler() { public void onClick(ClickEvent clickEvent) { availableGrid.selectAllRecords(); addSelectedRows(); } }); moveButtonStack.addMember(this.addAllButton); this.removeAllButton = new TransferImgButton(TransferImgButton.LEFT_ALL); this.removeAllButton.addClickHandler(new ClickHandler() { public void onClick(ClickEvent clickEvent) { assignedGrid.selectAllRecords(); removeSelectedRows(); } }); moveButtonStack.addMember(this.removeAllButton); return moveButtonStack; } private SectionStack buildAssignedItemsStack() { SectionStack assignedSectionStack = new SectionStack(); assignedSectionStack.setWidth("*"); assignedSectionStack.setHeight100(); assignedSectionStack.setAlign(Alignment.LEFT); SectionStackSection assignedSection = new SectionStackSection(getAssignedItemsGridTitle()); assignedSection.setCanCollapse(false); assignedSection.setExpanded(true); // Drag'n'Drop Settings this.assignedGrid.setCanReorderRecords(true); this.assignedGrid.setCanDragRecordsOut(true); this.assignedGrid.setDragDataAction(DragDataAction.MOVE); if (getItemIcon() != null) { this.assignedGrid.setDragTrackerMode(DragTrackerMode.ICON); this.assignedGrid.setTrackerImage(new ImgProperties(getItemIcon(), 16, 16)); } this.assignedGrid.setCanAcceptDroppedRecords(true); this.assignedGrid.setLoadingMessage(MSG.common_msg_loading()); this.assignedGrid.setEmptyMessage(MSG.common_msg_noItemsToShow()); List<ListGridField> assignedFields = new ArrayList<ListGridField>(); String itemIcon = getItemIcon(); if (itemIcon != null) { ListGridField iconField = new ListGridField("icon", 25); iconField.setType(ListGridFieldType.ICON); iconField.setCellIcon(itemIcon); iconField.setShowDefaultContextMenu(false); assignedFields.add(iconField); } ListGridField nameField = new ListGridField(getNameField(), MSG.common_title_name()); if (supportsNameHoverCustomizer()) { nameField.setShowHover(true); nameField.setHoverCustomizer(getNameHoverCustomizer()); } assignedFields.add(nameField); this.assignedGrid.setFields(assignedFields.toArray(new ListGridField[assignedFields.size()])); assignedSection.setItems(this.assignedGrid); assignedSectionStack.addSection(assignedSection); // Load data. if (this.initialSelection != null) { this.assignedGrid.setData(this.initialSelection); } if (this.isReadOnly) { this.assignedGrid.setDisabled(true); } else { this.assignedGrid.addSelectionChangedHandler(new SelectionChangedHandler() { public void onSelectionChanged(SelectionEvent selectionEvent) { updateButtonEnablement(); } }); this.assignedGrid.addDoubleClickHandler(new DoubleClickHandler() { public void onDoubleClick(DoubleClickEvent event) { removeSelectedRows(); } }); this.assignedGrid.addKeyPressHandler(new KeyPressHandler() { public void onKeyPress(KeyPressEvent event) { if ("Delete".equals(event.getKeyName())) { removeSelectedRows(); } } }); this.assignedGrid.addRecordDropHandler(new RecordDropHandler() { public void onRecordDrop(RecordDropEvent recordDropEvent) { addSelectedRows(); recordDropEvent.cancel(); } }); } return assignedSectionStack; } protected int getMaxAvailableRecords() { return MAX_AVAILABLE_RECORDS; } protected boolean supportsNameHoverCustomizer() { return false; } protected HoverCustomizer getNameHoverCustomizer() { return null; } private void notifyAssignedItemsChangedHandlers() { for (AssignedItemsChangedHandler handler : this.assignedItemsChangedHandlers) { handler.onSelectionChanged(new AssignedItemsChangedEvent(this.assignedGrid.getSelectedRecords())); } } public void reset() { this.assignedGrid.setData(this.initialSelection); populateAvailableGrid(getLatestCriteria(getAvailableFilterForm())); } public HandlerRegistration addAssignedItemsChangedHandler(final AssignedItemsChangedHandler handler) { this.assignedItemsChangedHandlers.add(handler); return new HandlerRegistration() { @Override public void removeHandler() { assignedItemsChangedHandlers.remove(handler); } }; } /** * Moves the rows selected in the assigned grid to the available grid. */ public void removeSelectedRows() { moveSelectedData(this.assignedGrid, this.availableGrid); notifyAssignedItemsChangedHandlers(); updateButtonEnablement(); } /** * Moves the rows selected in the available grid to the assigned grid. */ public void addSelectedRows() { moveSelectedData(this.availableGrid, this.assignedGrid); notifyAssignedItemsChangedHandlers(); updateButtonEnablement(); } private void moveSelectedData(ListGrid sourceGrid, ListGrid targetGrid) { targetGrid.transferSelectedData(sourceGrid); sourceGrid.removeSelectedData(); } protected String getNameField() { return "name"; } /** * Return the item title (i.e. display name), which should be plural and capitalized, e.g. "Resource Groups", "Roles". * * @return the item title (i.e. display name), which should be plural and capitalized, e.g. "Resource Groups", "Roles" */ protected abstract String getItemTitle(); protected String getAvailableItemsGridTitle() { String itemTitle = getItemTitle(); return MSG.view_selector_available(itemTitle); } protected String getAssignedItemsGridTitle() { String itemTitle = getItemTitle(); return MSG.view_selector_assigned(itemTitle); } protected void updateButtonEnablement() { if (!isReadOnly) { addButton.setDisabled(!availableGrid.anySelected()); removeButton.setDisabled(!assignedGrid.anySelected()); addAllButton.setDisabled(!containsAtLeastOneEnabledRecord(this.availableGrid)); removeAllButton.setDisabled(!containsAtLeastOneEnabledRecord(this.assignedGrid)); } } @Deprecated public ListGrid getAvailableGrid() { return availableGrid; } @Deprecated public ListGrid getAssignedGrid() { return assignedGrid; } protected String getSelectorKey() { return SELECTOR_KEY; } private static boolean containsAtLeastOneEnabledRecord(ListGrid grid) { boolean result = false; ListGridRecord[] assignedRecords = grid.getRecords(); for (ListGridRecord assignedRecord : assignedRecords) { if (isEnabled(assignedRecord)) { result = true; break; } } return result; } private static boolean isEnabled(ListGridRecord assignedRecord) { return assignedRecord.getAttribute("enabled") == null || assignedRecord.getEnabled(); } }