package org.ovirt.engine.ui.common.widget.table; import java.util.ArrayList; import java.util.List; import org.ovirt.engine.ui.common.CommonApplicationConstants; import org.ovirt.engine.ui.common.gin.AssetProvider; import org.ovirt.engine.ui.common.idhandler.WithElementId; import org.ovirt.engine.ui.common.system.ClientStorage; import org.ovirt.engine.ui.common.uicommon.model.DeferredModelCommandInvoker; import org.ovirt.engine.ui.common.uicommon.model.SearchableTableModelProvider; import org.ovirt.engine.ui.common.widget.action.AbstractActionPanel; import org.ovirt.engine.ui.common.widget.label.NoItemsLabel; import org.ovirt.engine.ui.common.widget.table.header.SafeHtmlHeader; import org.ovirt.engine.ui.uicommonweb.UICommand; import org.ovirt.engine.ui.uicommonweb.models.SearchableListModel; import com.google.gwt.core.client.Scheduler; import com.google.gwt.dom.client.BrowserEvents; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Position; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.dom.client.TableCellElement; import com.google.gwt.dom.client.TableRowElement; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ContextMenuEvent; import com.google.gwt.event.dom.client.DoubleClickEvent; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.ScrollEvent; import com.google.gwt.event.shared.EventBus; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.uibinder.client.UiHandler; import com.google.gwt.user.cellview.client.CellTable.Resources; import com.google.gwt.user.cellview.client.Column; import com.google.gwt.user.cellview.client.ColumnSortList.ColumnSortInfo; import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy; import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState; import com.google.gwt.user.cellview.client.RowStyles; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.ButtonBase; import com.google.gwt.user.client.ui.Label; import com.google.gwt.user.client.ui.PopupPanel; import com.google.gwt.user.client.ui.ScrollPanel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; import com.google.gwt.view.client.CellPreviewEvent; import com.google.gwt.view.client.CellPreviewEvent.Handler; import com.google.gwt.view.client.SelectionChangeEvent; import com.google.gwt.view.client.SelectionModel; /** * Base class used to implement action table widgets. * <p> * Subclasses are free to style the UI, given that they declare: * <ul> * <li>{@link #actionPanel} widget into which action button widgets will be rendered * <li>{@link #prevPageButton} widget representing the "previous page" button * <li>{@link #nextPageButton} widget representing the "next page" button * <li>{@link #tableContainer} widget for displaying the actual table * <li>{@link #tableHeaderContainer} widget for displaying the table header * </ul> * * @param <T> * Table row data type. */ public abstract class AbstractActionTable<T> extends AbstractActionPanel<T> implements ActionTable<T>, HasColumns<T> { /** * Allows customizing table row elements after the table finished loading its items. * * @param <T> * Table row data type. */ public interface RowVisitor<T> { /** * @param row * Table row element. * @param item * Value associated with this row. */ void visit(TableRowElement row, T item); } private static final CommonApplicationConstants constants = AssetProvider.getConstants(); @UiField @WithElementId public ButtonBase prevPageButton; @UiField @WithElementId public ButtonBase nextPageButton; @UiField public ScrollPanel tableContainer; @UiField public SimplePanel tableHeaderContainer; private final OrderedMultiSelectionModel<T> selectionModel; @WithElementId("content") public final ActionCellTable<T> table; protected final ActionCellTable<T> tableHeader; // If false, tableHeader widget will be visible, providing a separate table header UI. // If true, tableHeader widget will be hidden, with header UI provided by the main table widget. protected final boolean showDefaultHeader; private boolean multiSelectionDisabled; private final int[] mousePosition = new int[2]; private HandlerRegistration scrollHandlerRegistration; private HandlerRegistration selectionModelScrollHandlerRegistration; // Table container's horizontal scroll position, used to align table header with main table private int tableContainerHorizontalScrollPosition = 0; private boolean doAutoSelect; private int scrollOffset = 0; private RowVisitor<T> rowVisitor; public AbstractActionTable(final SearchableTableModelProvider<T, ?> dataProvider, Resources resources, Resources headerResources, EventBus eventBus, ClientStorage clientStorage) { super(dataProvider, eventBus); this.selectionModel = new OrderedMultiSelectionModel<>(dataProvider); this.table = new ActionCellTable<T>(dataProvider, resources) { @Override protected void onBrowserEvent2(Event event) { // Enable multiple selection only when Control/Shift key is pressed mousePosition[0] = event.getClientX(); mousePosition[1] = event.getClientY(); if (BrowserEvents.CLICK.equals(event.getType()) && !multiSelectionDisabled) { selectionModel.setMultiSelectEnabled(event.getCtrlKey()); selectionModel.setMultiRangeSelectEnabled(event.getShiftKey()); } // Remove focus from the table so refreshes won't try to focus on the // selected row. This is important when the user has scrolled the selected // row off the screen, we don't want the browser to scroll back. table.setFocus(false); super.onBrowserEvent2(event); } @Override public int getKeyboardSelectedRow() { if (selectionModel.getLastSelectedRow() == -1) { return super.getKeyboardSelectedRow(); } return selectionModel.getLastSelectedRow(); } @Override protected void onLoad() { super.onLoad(); if (selectionModel.getLastSelectedRow() == -1) { return; } Scheduler.get().scheduleDeferred(() -> setFocus(true)); } @Override public void setRowData(int start, final List<? extends T> values) { super.setRowData(start, values); selectionModel.resolveChanges(); if (isAttached() && isVisible()) { autoSelectFirst(); } updateTableControls(); enforceScrollPosition(); } @Override protected void onLoadingStateChanged(LoadingState state) { super.onLoadingStateChanged(state); enforceScrollPosition(); if (state == LoadingState.LOADING) { Scheduler.get().scheduleDeferred(() -> doAutoSelect = true); } else if (state == LoadingState.LOADED) { Scheduler.get().scheduleDeferred(() -> { if (rowVisitor != null) { int count = getVisibleItemCount(); for (int i = 0; i < count; i++) { TableRowElement row = getChildElement(i); T item = getVisibleItem(i); rowVisitor.visit(row, item); } } }); } } @Override protected String getGridElementId() { return AbstractActionTable.this.getElementId(); } @Override protected void pushColumnSort(ColumnSortInfo columnSortInfo) { AbstractActionTable.this.pushColumnSort(columnSortInfo); } @Override protected void clearColumnSort() { AbstractActionTable.this.clearColumnSort(); } }; // Can't do this in the onBrowserEvent, as GWT CellTable doesn't support double click. this.table.addDomHandler(event -> { SearchableListModel model = dataProvider.getModel(); UICommand command = model.getDoubleClickCommand(); if (command != null && command.getIsExecutionAllowed()) { DeferredModelCommandInvoker invoker = new DeferredModelCommandInvoker(model); invoker.invokeCommand(command); } }, DoubleClickEvent.getType()); // Create table header row this.tableHeader = new ActionCellTable<T>(dataProvider, headerResources) { @Override public void onResizeEnd(Column<T, ?> column, Element headerElement) { super.onResizeEnd(column, headerElement); // Redraw main table table.redraw(); } @Override public void resizeColumn(Column<T, ?> column, int newWidth) { super.resizeColumn(column, newWidth); // Resize the corresponding column in main table table.resizeColumn(column, newWidth); } @Override protected void configureElementId(Column<T, ?> column) { // No-op, don't set element ID here, since column // instances are shared between main and header table } @Override protected String getGridElementId() { return AbstractActionTable.this.getElementId(); } @Override protected void pushColumnSort(ColumnSortInfo columnSortInfo) { AbstractActionTable.this.pushColumnSort(columnSortInfo); } @Override protected void clearColumnSort() { AbstractActionTable.this.clearColumnSort(); } @Override public void setColumnVisible(Column<T, ?> column, boolean visible) { super.setColumnVisible(column, visible); // Update main table table.setColumnVisible(column, visible); } @Override public void swapColumns(Column<T, ?> columnOne, Column<T, ?> columnTwo) { super.swapColumns(columnOne, columnTwo); // Update main table table.swapColumns(columnOne, columnTwo); } }; this.tableHeader.setRowData(new ArrayList<T>()); this.showDefaultHeader = headerResources == null; // Apply selection model to the table widget this.selectionModel.setDataDisplay(table); // Default to 'no items to display' this.table.setEmptyTableWidget(new NoItemsLabel()); // column resizing persistence -- can be enabled only when the tableHeader widget is visible if (isTableHeaderVisible()) { tableHeader.enableColumnWidthPersistence(clientStorage, dataProvider.getModel()); table.enableColumnWidthPersistence(clientStorage, dataProvider.getModel()); } addModelSearchStringChangeListener(dataProvider.getModel()); addScrollSelectionModelChangeListener(); } public void setRowVisitor(RowVisitor<T> rowVisitor) { this.rowVisitor = rowVisitor; } @Override public void onLoad() { super.onLoad(); autoSelectFirst(); } private void autoSelectFirst() { if (table.getRowCount() == 1 && selectionModel.getSelectedList().isEmpty() && doAutoSelect) { Scheduler.get().scheduleDeferred(() -> { if (table.getVisibleItemCount() > 0) { selectionModel.setSelected(table.getVisibleItems().get(0), true); } }); doAutoSelect = false; } } private void addScrollSelectionModelChangeListener() { if (selectionModelScrollHandlerRegistration != null) { selectionModelScrollHandlerRegistration.removeHandler(); } selectionModelScrollHandlerRegistration = selectionModel.addSelectionChangeHandler( event -> { if (selectionModel.getSelectedList().isEmpty()) { scrollOffset = 0; } else { updateScrollPosition(); } enforceScrollPosition(); }); } private void addScrollListener() { if (scrollHandlerRegistration != null) { scrollHandlerRegistration.removeHandler(); } scrollHandlerRegistration = tableContainer.addScrollHandler(event -> updateScrollPosition()); } void addModelSearchStringChangeListener(final SearchableListModel<?, ?> model) { if (model.supportsServerSideSorting()) { model.getPropertyChangedEvent().addListener((ev, sender, args) -> { if ("SearchString".equals(args.propertyName)) { //$NON-NLS-1$ if (!model.isSearchValidForServerSideSorting()) { model.clearSortOptions(); clearColumnSort(); } } }); } } protected void updateTableControls() { prevPageButton.setEnabled(getDataProvider().canGoBack()); nextPageButton.setEnabled(getDataProvider().canGoForward()); prevPageButton.addStyleName("prevPageButton_pfly_fix"); //$NON-NLS-1$ nextPageButton.addStyleName("nextPageButton_pfly_fix"); //$NON-NLS-1$ } public void showPagingButtons() { prevPageButton.setVisible(true); nextPageButton.setVisible(true); } public void showSelectionCountTooltip() { this.selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { private PopupPanel tooltip = null; @Override public void onSelectionChange(SelectionChangeEvent event) { int selectedItems = selectionModel.getSelectedList().size(); if (selectedItems < 2) { return; } if (tooltip != null) { tooltip.hide(); } tooltip = new PopupPanel(true); tooltip.setWidget(new Label(selectionModel.getSelectedList().size() + " " + constants.selectedActionTable())); //$NON-NLS-1$ if (mousePosition[0] == 0 && mousePosition[1] == 0) { mousePosition[0] = Window.getClientWidth() / 2; mousePosition[1] = Window.getClientHeight() / 3; } tooltip.setPopupPosition(mousePosition[0] + 15, mousePosition[1]); tooltip.show(); Timer t = new Timer() { @Override public void run() { tooltip.hide(); } }; t.schedule(500); } }); } @Override protected SearchableTableModelProvider<T, ?> getDataProvider() { return (SearchableTableModelProvider<T, ?>) super.getDataProvider(); } @Override protected void initWidget(Widget widget) { super.initWidget(widget); initTable(); addScrollListener(); } /** * Initialize the table widget and attach it to the corresponding panel. */ void initTable() { // Set up table data provider getDataProvider().addDataDisplay(table); // Set up sort handler initSortHandler(); // Set up table selection model table.setSelectionModel(selectionModel); // Enable keyboard selection table.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.ENABLED); // Add arrow key handler table.addDomHandler(event -> { boolean shiftPageDown = event.isShiftKeyDown() && KeyCodes.KEY_PAGEDOWN == event.getNativeKeyCode(); boolean shiftPageUp = event.isShiftKeyDown() && KeyCodes.KEY_PAGEUP == event.getNativeKeyCode(); boolean ctrlA = event.isControlKeyDown() && ('a' == event.getNativeKeyCode() || 'A' == event.getNativeKeyCode()); boolean arrow = KeyDownEvent.isArrow(event.getNativeKeyCode()); if (shiftPageUp || shiftPageDown || ctrlA || arrow) { event.preventDefault(); event.stopPropagation(); } else { return; } if (shiftPageDown) { selectionModel.selectAllNext(); } else if (shiftPageUp) { selectionModel.selectAllPrev(); } else if (ctrlA) { selectionModel.selectAll(); } else if (arrow) { selectionModel.setMultiSelectEnabled(event.isControlKeyDown() && !multiSelectionDisabled); selectionModel.setMultiRangeSelectEnabled(event.isShiftKeyDown() && !multiSelectionDisabled); if (event.isDownArrow()) { selectionModel.selectNext(); } else if (event.isUpArrow()) { selectionModel.selectPrev(); } } }, KeyDownEvent.getType()); // Add context menu handler for table widget addContextMenuHandler(tableContainer); // Use fixed table layout setWidth("100%", true); //$NON-NLS-1$ // Attach table widget to the corresponding panel tableContainer.setWidget(table); tableHeaderContainer.setWidget(tableHeader); tableHeaderContainer.setVisible(isTableHeaderVisible()); // Use relative positioning for tableHeader, in order to align it with main table tableHeader.getElement().getStyle().setPosition(Position.RELATIVE); // Attach scroll event handler to main table container, so that the tableHeader widget // can have its position aligned with main table container's current scroll position tableContainer.addDomHandler(event -> { tableContainerHorizontalScrollPosition = tableContainer.getElement().getScrollLeft(); updateTableHeaderPosition(); }, ScrollEvent.getType()); // Reset main table container's scroll position enforceScrollPosition(); } void initSortHandler() { // Allow sorting by one column at a time tableHeader.getColumnSortList().setLimit(1); table.getColumnSortList().setLimit(1); // Attach column sort handler ActionCellTable<T> tableWithHeader = isTableHeaderVisible() ? tableHeader : table; tableWithHeader.initModelSortHandler(getDataProvider().getModel()); } void pushColumnSort(ColumnSortInfo columnSortInfo) { tableHeader.getColumnSortList().push(columnSortInfo); table.getColumnSortList().push(columnSortInfo); } void clearColumnSort() { tableHeader.getColumnSortList().clear(); table.getColumnSortList().clear(); } void enforceScrollPosition() { tableContainer.getElement().setScrollLeft(tableContainerHorizontalScrollPosition); updateTableHeaderPosition(); if (table.getParent() != null && table.getParent().getElement() != null) { table.getParent().getElement().setScrollTop(scrollOffset); } } void updateTableHeaderPosition() { tableHeader.getElement().getStyle().setLeft(-tableContainerHorizontalScrollPosition, Unit.PX); } @Override public void resetScrollPosition() { tableContainerHorizontalScrollPosition = 0; enforceScrollPosition(); } @Override protected void onContextMenu(ContextMenuEvent event) { super.onContextMenu(event); Element target = event.getNativeEvent().getEventTarget().cast(); T value = getValueFromElement(target); if (value != null && !selectionModel.isSelected(value)) { selectionModel.setMultiSelectEnabled(false); selectionModel.setMultiRangeSelectEnabled(false); selectionModel.setSelected(value, true); } } private T getValueFromElement(Element target) { TableCellElement tableCell = findNearestParentCell(target); if (tableCell != null) { Element trElem = tableCell.getParentElement(); TableRowElement tr = TableRowElement.as(trElem); int row = tr.getSectionRowIndex(); return table.getVisibleItemCount() > 0 ? table.getVisibleItem(row) : null; } else { return null; } } private TableCellElement findNearestParentCell(Element elem) { while ((elem != null) && (elem != table.getElement())) { String tagName = elem.getTagName(); if ("td".equalsIgnoreCase(tagName) || "th".equalsIgnoreCase(tagName)) { //$NON-NLS-1$ //$NON-NLS-2$ return elem.cast(); } elem = elem.getParentElement(); } return null; } public void setWidth(String width, boolean isFixedLayout) { table.setWidth(width, isFixedLayout); tableHeader.setWidth(width, isFixedLayout); } @UiHandler("prevPageButton") public void handlePrevPageButtonClick(ClickEvent event) { getDataProvider().goBack(); } @UiHandler("nextPageButton") public void handleNextPageButtonClick(ClickEvent event) { getDataProvider().goForward(); } public void setColumnWidth(Column<T, ?> column, String width) { table.setColumnWidth(column, width); tableHeader.setColumnWidth(column, width); } /* (non-Javadoc) * @see org.ovirt.engine.ui.common.widget.table.HasAddableColumns#addColumn(com.google.gwt.user.cellview.client.Column, java.lang.String) */ @Override public void addColumn(Column<T, ?> column, String headerText) { table.addColumn(column, headerText); tableHeader.addColumn(column, headerText); } /* (non-Javadoc) * @see org.ovirt.engine.ui.common.widget.table.HasAddableColumns#addColumn(com.google.gwt.user.cellview.client.Column, java.lang.String, java.lang.String) */ @Override public void addColumn(Column<T, ?> column, String headerText, String width) { addColumn(column, headerText); setColumnWidth(column, width); } /* (non-Javadoc) * @see org.ovirt.engine.ui.common.widget.table.HasAddableColumns#addColumnWithHtmlHeader(com.google.gwt.user.cellview.client.Column, com.google.gwt.safehtml.shared.SafeHtml) */ @Override public void addColumnWithHtmlHeader(Column<T, ?> column, SafeHtml headerHtml) { table.addColumnWithHtmlHeader(column, headerHtml); tableHeader.addColumnWithHtmlHeader(column, headerHtml); } /* (non-Javadoc) * @see org.ovirt.engine.ui.common.widget.table.HasAddableColumns#addColumnWithHtmlHeader(com.google.gwt.user.cellview.client.Column, com.google.gwt.safehtml.shared.SafeHtml, java.lang.String) */ @Override public void addColumnWithHtmlHeader(Column<T, ?> column, SafeHtml headerHtml, String width) { table.addColumnWithHtmlHeader(column, headerHtml, width); tableHeader.addColumnWithHtmlHeader(column, headerHtml, width); } /* (non-Javadoc) * @see org.ovirt.engine.ui.common.widget.table.HasAddableColumns#addColumn(com.google.gwt.user.cellview.client.Column, org.ovirt.engine.ui.common.widget.table.header.SafeHtmlHeader) */ @Override public void addColumn(Column<T, ?> column, SafeHtmlHeader header) { table.addColumn(column, header); tableHeader.addColumn(column, header); } /* (non-Javadoc) * @see org.ovirt.engine.ui.common.widget.table.HasAddableColumns#addColumn(com.google.gwt.user.cellview.client.Column, org.ovirt.engine.ui.common.widget.table.header.SafeHtmlHeader, java.lang.String) */ @Override public void addColumn(Column<T, ?> column, SafeHtmlHeader header, String width) { addColumn(column, header); setColumnWidth(column, width); } /** * Ensures that the given column is visible or hidden. */ public void ensureColumnVisible(Column<T, ?> column, String headerText, boolean visible) { table.ensureColumnVisible(column, headerText, visible); tableHeader.ensureColumnVisible(column, headerText, visible); } /** * Ensures that the given column is visible or hidden. * <p> * This method also sets the column width in case the column needs to be added. */ public void ensureColumnVisible(Column<T, ?> column, String headerText, boolean visible, String width) { table.ensureColumnVisible(column, headerText, visible, width); tableHeader.ensureColumnVisible(column, headerText, visible, width); } /** * Ensures that the given column is visible or hidden. * <p> * This method also sets the column width in case the column needs to be added. */ public void ensureColumnVisible(Column<T, ?> column, SafeHtml headerHtml, boolean visible, String width) { table.ensureColumnVisible(column, headerHtml, visible, width); tableHeader.ensureColumnVisible(column, headerHtml, visible, width); } /** * Ensures that the given column is visible or hidden. * <p> * This method also sets the column width in case the column needs to be added. */ public void ensureColumnVisible(Column<T, ?> column, SafeHtmlHeader header, boolean visible, String width) { table.ensureColumnVisible(column, header, visible, width); tableHeader.ensureColumnVisible(column, header, visible, width); } /** * Enables header context menu triggered by right-clicking table header area. * <p> * <em>After calling this method, each column must have non-empty header HTML content <b>or</b> * {@linkplain org.ovirt.engine.ui.common.widget.table.column.AbstractColumn#setContextMenuTitle * custom context menu title} defined, otherwise the context menu will contain "unnamed column" * items.</em> */ public void enableHeaderContextMenu() { ActionCellTable<T> tableWithHeader = isTableHeaderVisible() ? tableHeader : table; tableWithHeader.enableHeaderContextMenu(); } /** * Allows table columns to be resized by dragging their right-hand border using mouse. * <p> * This method should be called before calling any {@code addColumn} methods. * <p> * <em>After calling this method, each column must have an explicit width defined in PX units, otherwise the resize * behavior will not function properly.</em> */ public void enableColumnResizing() { // Column resizing is supported only when the tableHeader widget is visible if (isTableHeaderVisible()) { table.enableColumnResizing(); tableHeader.enableColumnResizing(); } } @Override public OrderedMultiSelectionModel<T> getSelectionModel() { return selectionModel; } public void setTableSelectionModel(SelectionModel<T> selectionModel, CellPreviewEvent.Handler<T> selectionEventManager) { table.setSelectionModel(selectionModel, selectionEventManager); } public boolean isMultiSelectionDisabled() { return multiSelectionDisabled; } public void setMultiSelectionDisabled(boolean multiSelectionDisabled) { this.multiSelectionDisabled = multiSelectionDisabled; } @Override public List<T> getSelectedItems() { return selectionModel.getSelectedList(); } @Override public void setLoadingState(LoadingState state) { table.setLoadingState(state); } /** * Gets the instance of RowStyles class and sets it to the cell table. Can be used when the rows have special styles * according to the data they are displaying. */ public void setExtraRowStyles(RowStyles<T> rowStyles) { table.setRowStyles(rowStyles); } public String getContentTableElementId() { return table.getElementId(); } boolean isTableHeaderVisible() { return !showDefaultHeader; } public String getColumnWidth(Column<T, ?> column) { return table.getColumnWidth(column); } private void updateScrollPosition() { if (tableContainer.getMaximumVerticalScrollPosition() > 0) { scrollOffset = tableContainer.getVerticalScrollPosition(); } } public void setVisibleRange(int start, int length) { this.table.setVisibleRange(start, length); } public void addCellPreviewHandler(Handler<T> handler) { table.addCellPreviewHandler(handler); } }