blob: 720c9e604e95d69e5f5d380dc8cb9c4d39a1e5b0 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010-2017 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
package org.eclipse.scout.rt.client.ui.basic.table;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.eclipse.scout.rt.client.ModelContextProxy;
import org.eclipse.scout.rt.client.ModelContextProxy.ModelContext;
import org.eclipse.scout.rt.client.extension.ui.action.tree.MoveActionNodesHandler;
import org.eclipse.scout.rt.client.extension.ui.basic.table.ITableExtension;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableAppLinkActionChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableContentChangedChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableCopyChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableCreateTableRowDataMapperChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableDecorateCellChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableDecorateRowChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableDisposeTableChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableDragChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableDropChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableInitTableChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableResetColumnsChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableRowActionChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableRowClickChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableRowsCheckedChain;
import org.eclipse.scout.rt.client.extension.ui.basic.table.TableChains.TableRowsSelectedChain;
import org.eclipse.scout.rt.client.services.common.icon.IIconProviderService;
import org.eclipse.scout.rt.client.ui.AbstractEventBuffer;
import org.eclipse.scout.rt.client.ui.AbstractWidget;
import org.eclipse.scout.rt.client.ui.ClientUIPreferences;
import org.eclipse.scout.rt.client.ui.IEventHistory;
import org.eclipse.scout.rt.client.ui.IWidget;
import org.eclipse.scout.rt.client.ui.MouseButton;
import org.eclipse.scout.rt.client.ui.action.keystroke.IKeyStroke;
import org.eclipse.scout.rt.client.ui.action.keystroke.KeyStroke;
import org.eclipse.scout.rt.client.ui.action.menu.IMenu;
import org.eclipse.scout.rt.client.ui.action.menu.MenuUtility;
import org.eclipse.scout.rt.client.ui.action.menu.root.ITableContextMenu;
import org.eclipse.scout.rt.client.ui.action.menu.root.internal.TableContextMenu;
import org.eclipse.scout.rt.client.ui.basic.cell.Cell;
import org.eclipse.scout.rt.client.ui.basic.cell.ICell;
import org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractBooleanColumn;
import org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractColumn;
import org.eclipse.scout.rt.client.ui.basic.table.columns.AbstractStringColumn;
import org.eclipse.scout.rt.client.ui.basic.table.columns.IBooleanColumn;
import org.eclipse.scout.rt.client.ui.basic.table.columns.IColumn;
import org.eclipse.scout.rt.client.ui.basic.table.columns.INumberColumn;
import org.eclipse.scout.rt.client.ui.basic.table.controls.AbstractTableControl;
import org.eclipse.scout.rt.client.ui.basic.table.controls.ITableControl;
import org.eclipse.scout.rt.client.ui.basic.table.customizer.ITableCustomizer;
import org.eclipse.scout.rt.client.ui.basic.table.customizer.ITableCustomizerProvider;
import org.eclipse.scout.rt.client.ui.basic.table.internal.InternalTableRow;
import org.eclipse.scout.rt.client.ui.basic.table.menus.OrganizeColumnsMenu;
import org.eclipse.scout.rt.client.ui.basic.table.organizer.ITableOrganizer;
import org.eclipse.scout.rt.client.ui.basic.table.organizer.ITableOrganizerProvider;
import org.eclipse.scout.rt.client.ui.basic.table.userfilter.TableUserFilterManager;
import org.eclipse.scout.rt.client.ui.basic.table.userfilter.UserTableRowFilter;
import org.eclipse.scout.rt.client.ui.basic.userfilter.IColumnAwareUserFilterState;
import org.eclipse.scout.rt.client.ui.basic.userfilter.IUserFilter;
import org.eclipse.scout.rt.client.ui.basic.userfilter.IUserFilterState;
import org.eclipse.scout.rt.client.ui.dnd.IDNDSupport;
import org.eclipse.scout.rt.client.ui.dnd.TextTransferObject;
import org.eclipse.scout.rt.client.ui.dnd.TransferObject;
import org.eclipse.scout.rt.client.ui.form.fields.IFormField;
import org.eclipse.scout.rt.client.ui.form.fields.booleanfield.IBooleanField;
import org.eclipse.scout.rt.client.ui.form.fields.tablefield.AbstractTableField;
import org.eclipse.scout.rt.platform.BEANS;
import org.eclipse.scout.rt.platform.Order;
import org.eclipse.scout.rt.platform.OrderedComparator;
import org.eclipse.scout.rt.platform.Replace;
import org.eclipse.scout.rt.platform.annotations.ConfigOperation;
import org.eclipse.scout.rt.platform.annotations.ConfigProperty;
import org.eclipse.scout.rt.platform.classid.ClassId;
import org.eclipse.scout.rt.platform.classid.ITypeWithClassId;
import org.eclipse.scout.rt.platform.exception.ExceptionHandler;
import org.eclipse.scout.rt.platform.exception.PlatformError;
import org.eclipse.scout.rt.platform.html.HTML;
import org.eclipse.scout.rt.platform.reflect.ConfigurationUtility;
import org.eclipse.scout.rt.platform.resource.BinaryResource;
import org.eclipse.scout.rt.platform.status.IStatus;
import org.eclipse.scout.rt.platform.util.BooleanUtility;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
import org.eclipse.scout.rt.platform.util.CompositeObject;
import org.eclipse.scout.rt.platform.util.ObjectUtility;
import org.eclipse.scout.rt.platform.util.StringUtility;
import org.eclipse.scout.rt.platform.util.collection.OrderedCollection;
import org.eclipse.scout.rt.platform.util.concurrent.OptimisticLock;
import org.eclipse.scout.rt.platform.util.visitor.CollectingVisitor;
import org.eclipse.scout.rt.platform.util.visitor.TreeTraversals;
import org.eclipse.scout.rt.shared.data.basic.NamedBitMaskHelper;
import org.eclipse.scout.rt.shared.data.basic.table.AbstractTableRowData;
import org.eclipse.scout.rt.shared.data.form.fields.tablefield.AbstractTableFieldBeanData;
import org.eclipse.scout.rt.shared.dimension.IDimensions;
import org.eclipse.scout.rt.shared.extension.AbstractExtension;
import org.eclipse.scout.rt.shared.extension.ContributionComposite;
import org.eclipse.scout.rt.shared.extension.ExtensionUtility;
import org.eclipse.scout.rt.shared.extension.IContributionOwner;
import org.eclipse.scout.rt.shared.extension.IExtensibleObject;
import org.eclipse.scout.rt.shared.extension.IExtension;
import org.eclipse.scout.rt.shared.extension.ObjectExtensions;
import org.eclipse.scout.rt.shared.services.common.code.ICode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Columns are defined as inner classes<br>
* for every inner column class there is a generated getXYColumn method directly on the table
*/
@ClassId("e88f7f88-9747-40ea-88bd-744803aef7a7")
public abstract class AbstractTable extends AbstractWidget implements ITable, IContributionOwner, IExtensibleObject {
private static final String AUTO_DISCARD_ON_DELETE = "AUTO_DISCARD_ON_DELETE";
private static final String SORT_VALID = "SORT_VALID";
private static final String INITIAL_MULTI_LINE_TEXT = "INITIAL_MULTI_LINE_TEXT";
private static final String ACTION_RUNNING = "ACTION_RUNNING";
private static final Logger LOG = LoggerFactory.getLogger(AbstractTable.class);
private static final NamedBitMaskHelper FLAGS_BIT_HELPER = new NamedBitMaskHelper(AUTO_DISCARD_ON_DELETE, SORT_VALID, INITIAL_MULTI_LINE_TEXT, ACTION_RUNNING);
private static final NamedBitMaskHelper ENABLED_BIT_HELPER = new NamedBitMaskHelper(IDimensions.ENABLED);
public interface IResetColumnsOption {
String VISIBILITY = "visibility";
String ORDER = "order";
String SORTING = "sorting";
String WIDTHS = "widths";
String BACKGROUND_EFFECTS = "backgroundEffects";
String FILTERS = "filters";
}
private final OptimisticLock m_initLock;
private List<ITableRow> m_rows; // synchronized list
private List<ITableRow> m_rootRows; // synchronized list
private final Object m_cachedRowsLock;
private final Map<CompositeObject, ITableRow> m_rowsByKey;
private final Map<CompositeObject, ITableRow> m_deletedRows;
private final List<ITableRowFilter> m_rowFilters;
private final Map<String, BinaryResource> m_attachments;
private final TableListeners m_listeners;
private final Object m_cachedFilteredRowsLock;
private final ObjectExtensions<AbstractTable, ITableExtension<? extends AbstractTable>> m_objectExtensions;
/**
* Provides 8 dimensions for enabled state.<br>
* Internally used: {@link IDimensions#ENABLED}.<br>
* 7 dimensions remain for custom use. This Table is enabled, if all dimensions are enabled (all bits set).
*/
private byte m_enabled;
/**
* Provides 8 boolean flags.<br>
* Currently used: {@link #INITIALIZED}, {@link #AUTO_DISCARD_ON_DELETE}, {@link #SORT_VALID},
* {@link #INITIAL_MULTI_LINE_TEXT}, {@link #ACTION_RUNNING}
*/
private byte m_flags;
private ColumnSet m_columnSet;
private List<ITableRow> m_cachedRows;
private List<ITableRow/* ordered by rowIndex */> m_selectedRows;
private Set<ITableRow/* ordered by rowIndex */> m_checkedRows;
private Map<Class<?>, Class<? extends IMenu>> m_menuReplacementMapping;
private ITableUIFacade m_uiFacade;
private String m_userPreferenceContext;
private int m_tableChanging;
private AbstractEventBuffer<TableEvent> m_eventBuffer;
private int m_eventBufferLoopDetection;
private Set<ITableRow> m_rowDecorationBuffer;
private Map<Integer, Set<ITableRow>> m_rowValueChangeBuffer;
private P_CellEditorContext m_editContext;
private IBooleanColumn m_checkableColumn;
private List<ITableRow> m_cachedFilteredRows;
private IEventHistory<TableEvent> m_eventHistory;
private ContributionComposite m_contributionHolder;
private List<ITableControl> m_tableControls;
private IReloadHandler m_reloadHandler;
private int m_valueChangeTriggerEnabled = 1;// >=1 is true
private ITableOrganizer m_tableOrganizer;
private boolean m_treeStructureDirty;
private List<? extends IColumn<?>> m_dynamicColumns;
public AbstractTable() {
this(true);
}
public AbstractTable(boolean callInitializer) {
super(false);
m_enabled = NamedBitMaskHelper.ALL_BITS_SET; // default enabled
m_selectedRows = new ArrayList<>();
m_checkedRows = new LinkedHashSet<>();
m_rowDecorationBuffer = new HashSet<>();
m_rowValueChangeBuffer = new HashMap<>();
m_listeners = new TableListeners();
m_cachedRowsLock = new Object();
m_cachedFilteredRowsLock = new Object();
m_rows = Collections.synchronizedList(new ArrayList<>(1));
m_rootRows = Collections.synchronizedList(new ArrayList<>(1));
m_rowsByKey = Collections.synchronizedMap(new HashMap<>());
m_deletedRows = new HashMap<>();
m_rowFilters = new ArrayList<>(1);
m_attachments = new HashMap<>(0);
m_initLock = new OptimisticLock();
m_objectExtensions = new ObjectExtensions<>(this, false);
//add single observer listener
addTableListener(e -> {
try {
interceptRowsSelected(e.getRows());
}
catch (Exception ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
}, TableEvent.TYPE_ROWS_SELECTED);
if (callInitializer) {
callInitializer();
}
}
@Override
public final List<? extends ITableExtension<? extends AbstractTable>> getAllExtensions() {
return m_objectExtensions.getAllExtensions();
}
protected ITableExtension<? extends AbstractTable> createLocalExtension() {
return new LocalTableExtension<>(this);
}
@Override
public <T extends IExtension<?>> T getExtension(Class<T> c) {
return m_objectExtensions.getExtension(c);
}
@Override
public final <T> T optContribution(Class<T> contribution) {
return m_contributionHolder.optContribution(contribution);
}
@Override
protected void initConfigInternal() {
m_objectExtensions.initConfigAndBackupExtensionContext(createLocalExtension(), this::initConfig);
}
@Override
public String classId() {
String simpleClassId = ConfigurationUtility.getAnnotatedClassIdWithFallback(getClass());
if (getContainer() != null) {
return simpleClassId + ID_CONCAT_SYMBOL + getContainer().classId();
}
return simpleClassId;
}
@Override
public final List<Object> getAllContributions() {
return m_contributionHolder.getAllContributions();
}
@Override
public final <T> List<T> getContributionsByClass(Class<T> type) {
return m_contributionHolder.getContributionsByClass(type);
}
@Override
public final <T> T getContribution(Class<T> contribution) {
return m_contributionHolder.getContribution(contribution);
}
/*
* Configuration
*/
/**
* Configures the title of this table. The title of the table is rarely used because a table is usually surrounded by
* an {@link AbstractTableField} having its own title / label.
* <p>
* Subclasses can override this method. Default is {@code null}.
*
* @return Title of this table.
*/
@ConfigProperty(ConfigProperty.TEXT)
@Order(10)
protected String getConfiguredTitle() {
return null;
}
/**
* Configures the default icon for this table. The default icon is used for each row in the table.
* <p>
* This has only an effect, if {@link #getConfiguredRowIconVisible()} is set to true.
* <p>
* Subclasses can override this method. Default is {@code null}.
*
* @return the ID (name) of the icon
* @see IIconProviderService
*/
@ConfigProperty(ConfigProperty.ICON_ID)
@Order(20)
protected String getConfiguredDefaultIconId() {
return null;
}
/**
* Configures whether the row icon is visible.
* <p>
* If set to true the gui creates a column which contains the row icons. The column has a fixed width (@see
* {@link AbstractTable#getConfiguredRowIconColumnWidth()}, is not movable and always the first column (resp. the
* second if the table is checkable). The column is not available in the model.
* <p>
* If you need other settings or if you need the icon at another column position, you cannot use the row icons.
* Instead you have to create a column and use {@link Cell#setIconId(String)} to set the icons on it's cells.
* <p>
* Subclasses can override this method. Default is false.
*
* @return {@code true} if the row icon is visible, {@code false} otherwise.
* @see ITableRow#getIconId()
* @see #getConfiguredDefaultIconId()
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(25)
protected boolean getConfiguredRowIconVisible() {
return false;
}
/**
* Configures the row icon column width.
* <p>
* Has no effect if the row icon is not visible.
*
* @see #getConfiguredDefaultIconId()
* @see #getConfiguredRowIconVisible()
*/
@ConfigProperty(ConfigProperty.INTEGER)
@Order(27)
protected int getConfiguredRowIconColumnWidth() {
return IColumn.NARROW_MIN_WIDTH;
}
/**
* Configures whether only one row can be selected at once in this table.
* <p>
* Subclasses can override this method. Default is {@code true}.
*
* @return {@code true} if more then one row in this table can be selected at once, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(30)
protected boolean getConfiguredMultiSelect() {
return true;
}
/**
* Configures whether only one row can be checked in this table. This configuration is only useful if
* {@link #getConfiguredCheckable()} is {@code true} .
* <p>
* Subclasses can override this method. Default is {@code true}.
*
* @return {@code true} if more then one row in this table can be checked, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(32)
protected boolean getConfiguredMultiCheck() {
return true;
}
/**
* Configures the default menu that is used on the ENTER (action key) or the double click on a table row.
* <p>
* Subclasses can override this method. Default is {@code null}.
*
* @return The default menu to use.
*/
@ConfigProperty(ConfigProperty.MENU_CLASS)
@Order(35)
protected Class<? extends IMenu> getConfiguredDefaultMenu() {
return null;
}
/**
* Interception method used for customizing the default menu. Should be used by the framework only.
*
* @since 3.8.1
*/
protected Class<? extends IMenu> getDefaultMenuInternal() {
return getConfiguredDefaultMenu();
}
/**
* Configures whether deleted rows are automatically erased or cached for later processing (service deletion).
* <p>
* Subclasses can override this method. Default is {@code false}.
*
* @return {@code true} if deleted rows are automatically erased, {@code false} if deleted nodes are cached for later
* processing.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(50)
protected boolean getConfiguredAutoDiscardOnDelete() {
return false;
}
/**
* Configures whether sort is enabled for this table. If sort is enabled, the table rows are sorted based on their
* sort index (see {@link AbstractColumn#getConfiguredSortIndex()}) and the user might change the sorting at run time.
* If sort is disabled, the table rows are not sorted and the user cannot change the sorting.
* <p>
* Subclasses can override this method. Default is {@code true}.
*
* @return {@code true} if sort is enabled, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(60)
protected boolean getConfiguredSortEnabled() {
return true;
}
/**
* Configures whether the header row is visible. The header row contains the titles of each column.
* <p>
* Subclasses can override this method. Default is {@code true}.
*
* @return {@code true} if the header row is visible, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(70)
protected boolean getConfiguredHeaderVisible() {
return true;
}
/**
* Configures whether the header row is enabled. In a disabled header, it is not possible to move or resize the
* columns and the table header menu cannot be opened.
* <p>
* Subclasses can override this method. Default is {@code true}.
*
* @return {@code true} if the header row is enabled, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(70)
protected boolean getConfiguredHeaderEnabled() {
return true;
}
/**
* Configures whether the columns are auto resized. If true, all columns are resized so that the table never needs
* horizontal scrolling. This is especially useful for tables inside a form.
* <p>
* Subclasses can override this method. Default is {@code false}.
*
* @return {@code true} if the columns are auto resized, {@code false} otherwise.
* @see {@link AbstractColumn#getConfiguredWidth()}
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(80)
protected boolean getConfiguredAutoResizeColumns() {
return false;
}
/**
* Configures whether the table supports multiline text. If multiline text is supported and a string column has set
* the {@link AbstractStringColumn#getConfiguredTextWrap()} property to true, the text is wrapped and uses two or more
* lines.
* <p>
* Subclasses can override this method. Default is {@code false}. If the method is not overridden and at least one
* string column has set the {@link AbstractStringColumn#getConfiguredTextWrap()} to true, the multiline property is
* set automatically to true.
*
* @return {@code true} if the table supports multiline text, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(90)
protected boolean getConfiguredMultilineText() {
return false;
}
/**
* Configures whether the table is checkable.
* <p>
* Subclasses can override this method. Default is {@code false}.
*
* @return {@code true} if the table is checkable, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(100)
protected boolean getConfiguredCheckable() {
return false;
}
/**
* Configures the checkable column. The checkable column represents the check state of the row, i.e. if it is checked
* or not. If no checkable column is configured, only the row itself represents if the row was checked or not.
* <p>
* Subclasses can override this method. Default is {@code null}.
*
* @return A column class extending {@link AbstractBooleanColumn} that represents the row check state.
*/
@ConfigProperty(ConfigProperty.TABLE_COLUMN)
@Order(102)
protected Class<? extends AbstractBooleanColumn> getConfiguredCheckableColumn() {
return null;
}
/**
* Configures the maximum size for a drop request (in bytes).
* <p>
* Subclasses can override this method. Default is defined by {@link IDNDSupport#DEFAULT_DROP_MAXIMUM_SIZE}.
*
* @return maximum size in bytes.
*/
@ConfigProperty(ConfigProperty.LONG)
@Order(190)
protected long getConfiguredDropMaximumSize() {
return DEFAULT_DROP_MAXIMUM_SIZE;
}
/**
* Configures the drop support of this table.
* <p>
* Subclasses can override this method. Default is {@code 0} (no drop support).
*
* @return {@code 0} for no support or one or more of {@link IDNDSupport#TYPE_FILE_TRANSFER},
* {@link IDNDSupport#TYPE_IMAGE_TRANSFER}, {@link IDNDSupport#TYPE_JAVA_ELEMENT_TRANSFER} or
* {@link IDNDSupport#TYPE_TEXT_TRANSFER} (e.g. {@code TYPE_TEXT_TRANSFER | TYPE_FILE_TRANSFER}).
*/
@ConfigProperty(ConfigProperty.DRAG_AND_DROP_TYPE)
@Order(190)
protected int getConfiguredDropType() {
return 0;
}
/**
* Configures the drag support of this table.
* <p>
* Subclasses can override this method. Default is {@code 0} (no drag support).
*
* @return {@code 0} for no support or one or more of {@link IDNDSupport#TYPE_FILE_TRANSFER},
* {@link IDNDSupport#TYPE_IMAGE_TRANSFER}, {@link IDNDSupport#TYPE_JAVA_ELEMENT_TRANSFER} or
* {@link IDNDSupport#TYPE_TEXT_TRANSFER} (e.g. {@code TYPE_TEXT_TRANSFER | TYPE_FILE_TRANSFER}).
*/
@ConfigProperty(ConfigProperty.DRAG_AND_DROP_TYPE)
@Order(190)
protected int getConfiguredDragType() {
return 0;
}
/**
* Configures whether the keyboard can be used for navigation in table. When activated, the user can click on a column
* in the table. Now starting to type some letters, the row matching the typed letters in the column will be selected.
* <p>
* Subclasses can override this method. Default is {@code true}.
*
* @return {@code true} if the keyboard navigation is supported, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(200)
protected boolean getConfiguredKeyboardNavigation() {
return true;
}
/**
* Configures whether the table always scrolls to the selection. When activated and the selection in a table changes,
* the table is scrolled to the selection so that the selected row is visible.
* <p>
* Subclasses can override this method. Default is {@code false}.
*
* @return {@code true} if the table scrolls to the selection, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(230)
protected boolean getConfiguredScrollToSelection() {
return false;
}
@ConfigProperty(ConfigProperty.OBJECT)
@Order(240)
protected GroupingStyle getConfiguredGroupingStyle() {
return GroupingStyle.BOTTOM;
}
@ConfigProperty(ConfigProperty.OBJECT)
@Order(250)
protected HierarchicalStyle getConfiguredHierarchicalStyle() {
return HierarchicalStyle.DEFAULT;
}
/**
* Called after a drag operation was executed on one or several table rows.
* <p>
* Subclasses can override this method. The default does nothing.
*
* @param rows
* Table rows that were dragged (unmodifiable list).
* @return A transferable object representing the given rows.
*/
@ConfigOperation
@Order(10)
protected TransferObject execDrag(List<ITableRow> rows) {
return null;
}
/**
* Called after a drop operation was executed on the table.
* <p>
* Subclasses can override this method. The default does nothing.
*
* @param row
* Table row on which the transferable object was dropped (row may be null for empty space drop).
* @param t
* Transferable object that was dropped on the row.
*/
@ConfigOperation
@Order(20)
protected void execDrop(ITableRow row, TransferObject t) {
}
/**
* Called by a <code>CTRL-C</code> event on the table to copy the given rows into the clipboard.
* <p>
* Subclasses can override this method. The default creates a {@link TextTransferObject} of the table content (HTML
* table).
*
* @param rows
* The selected table rows to copy.
* @return A transferable object representing the given rows or null to not populate the clipboard.
*/
@ConfigOperation
@Order(30)
protected TransferObject execCopy(List<? extends ITableRow> rows) {
if (!CollectionUtility.hasElements(rows)) {
return null;
}
StringBuilder plainText = new StringBuilder();
List<IColumn<?>> columns = getColumnSet().getVisibleColumns();
boolean firstRow = true;
for (ITableRow row : rows) {
appendCopyTextForRow(plainText, row, firstRow, columns);
firstRow = false;
}
TextTransferObject transferObject = new TextTransferObject(plainText.toString());
return transferObject;
}
/**
* Called by {@link #execCopy(List)} for each row in case of a <code>CTRL-C</code> event on the table to copy the
* given rows into the clipboard.
*/
protected void appendCopyTextForRow(StringBuilder clipboardPlainText, ITableRow row, boolean firstRow, List<IColumn<?>> columns) {
if (!firstRow) {
clipboardPlainText.append(System.getProperty("line.separator"));
}
boolean firstColumn = true;
for (IColumn<?> column : columns) {
appendCopyTextForColumn(clipboardPlainText, row, column, firstColumn);
firstColumn = false;
}
}
/**
* Called by {@link #execCopy(List)} for each row and each visible column in case of a <code>CTRL-C</code> event on
* the table to copy the given rows into the clipboard.
*/
protected void appendCopyTextForColumn(StringBuilder clipboardPlainText, ITableRow row, IColumn<?> column, boolean firstColumn) {
String text;
if (column instanceof IBooleanColumn) {
boolean value = BooleanUtility.nvl(((IBooleanColumn) column).getValue(row), false);
text = value ? "X" : "";
}
else {
text = StringUtility.emptyIfNull(row.getCell(column).getText());
}
// special intercept for html
if (text != null && row.getCell(column).isHtmlEnabled()) {
text = HTML.raw(text).toPlainText();
}
// text/plain
if (!firstColumn) {
clipboardPlainText.append("\t");
}
clipboardPlainText.append(StringUtility.emptyIfNull(StringUtility.unwrapText(text)));
}
/**
* Called after the table content changed, rows were added, removed or changed.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(40)
protected void execContentChanged() {
}
/**
* Called after {@link AbstractColumn#execDecorateCell(Cell,ITableRow)} on the column to decorate the cell.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(50)
protected void execDecorateCell(Cell view, ITableRow row, IColumn<?> col) {
}
/**
* Called during initialization of this table, after the columns were initialized.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(60)
protected void execInitTable() {
}
/**
* Called when this table is disposed, after the columns were disposed.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(70)
protected void execDisposeTable() {
}
/**
* Called when the user clicks on a row in this table.
* <p>
* Subclasses can override this method. The default fires a {@link TableEvent#TYPE_ROW_CLICK} event.
*
* @param Row
* that was clicked (never null).
* @param mouseButton
* the mouse button ({@link MouseButton}) which triggered this method
*/
@ConfigOperation
@Order(80)
protected void execRowClick(ITableRow row, MouseButton mouseButton) {
TableEvent e = new TableEvent(this, TableEvent.TYPE_ROW_CLICK, CollectionUtility.arrayList(row));
fireTableEventInternal(e);
}
/**
* Called when the row has been activated.
* <p>
* Subclasses can override this method. The default opens the configured default menu or if no default menu is
* configured, fires a {@link TableEvent#TYPE_ROW_ACTION} event.
*
* @param row
* that was activated (never null).
*/
@ConfigOperation
@Order(90)
protected void execRowAction(ITableRow row) {
Class<? extends IMenu> defaultMenuType = getDefaultMenuInternal();
if (defaultMenuType != null) {
try {
runMenu(defaultMenuType);
}
catch (Exception ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
}
else {
TableEvent e = new TableEvent(this, TableEvent.TYPE_ROW_ACTION, CollectionUtility.arrayList(row));
fireTableEventInternal(e);
}
}
/**
* Called whenever the selection changes.
* <p>
* Subclasses can override this method. The default does nothing.
*
* @param rows
* an unmodifiable list of the selected rows, may be empty but not null.
*/
@ConfigOperation
@Order(100)
protected void execRowsSelected(List<? extends ITableRow> rows) {
}
/**
* Called when the row is going to be decorated.
* <p>
* Subclasses can override this method. The default does nothing.
*
* @param row
* that is going to be decorated.
*/
@ConfigOperation
@Order(110)
protected void execDecorateRow(ITableRow row) {
}
/**
* Called when an app link has been clicked.
* <p>
* Subclasses can override this method. The default does nothing.
*/
@ConfigOperation
@Order(120)
protected void execAppLinkAction(String ref) {
}
/**
* Called when rows get checked or unchecked.
* <p>
* Subclasses can override this method.
*
* @param rows
* list of rows which have been checked or unchecked (never null).
*/
@ConfigOperation
@Order(130)
protected void execRowsChecked(Collection<? extends ITableRow> rows) {
}
/**
* This method is called during initializing the table and is thought to add header menus to the given collection of
* menus. Menus added in this method should be of menu type {@link ITableMenu.TableMenuType#Header}.<br>
* To change the order or specify the insert position use {@link IMenu#setOrder(double)}.
*
* @param menus
* a live collection of the menus. Add additional header menus to this list optionally add some separators at
* the end.
*/
protected void addHeaderMenus(OrderedCollection<IMenu> menus) {
menus.addLast(new OrganizeColumnsMenu(this));
}
protected List<Class<? extends IMenu>> getDeclaredMenus() {
Class[] dca = ConfigurationUtility.getDeclaredPublicClasses(getClass());
List<Class<IMenu>> filtered = ConfigurationUtility.filterClasses(dca, IMenu.class);
return ConfigurationUtility.removeReplacedClasses(filtered);
}
protected List<Class<? extends ITableControl>> getConfiguredTableControls() {
Class[] dca = ConfigurationUtility.getDeclaredPublicClasses(getClass());
List<Class<ITableControl>> filtered = ConfigurationUtility.filterClasses(dca, ITableControl.class);
return ConfigurationUtility.removeReplacedClasses(filtered);
}
private List<Class<? extends IColumn>> getConfiguredColumns() {
Class[] dca = ConfigurationUtility.getDeclaredPublicClasses(getClass());
List<Class<IColumn>> foca = ConfigurationUtility.filterClasses(dca, IColumn.class);
return ConfigurationUtility.removeReplacedClasses(foca);
}
private List<Class<? extends IKeyStroke>> getConfiguredKeyStrokes() {
Class[] dca = ConfigurationUtility.getDeclaredPublicClasses(getClass());
List<Class<IKeyStroke>> fca = ConfigurationUtility.filterClasses(dca, IKeyStroke.class);
return ConfigurationUtility.removeReplacedClasses(fca);
}
@Override
protected void initConfig() {
super.initConfig();
m_eventHistory = createEventHistory();
m_eventBuffer = createEventBuffer();
m_uiFacade = BEANS.get(ModelContextProxy.class).newProxy(createUIFacade(), ModelContext.copyCurrent());
m_contributionHolder = new ContributionComposite(this);
setEnabled(true);
setLoading(false);
setGroupingStyle(getConfiguredGroupingStyle());
setHierarchicalStyle(getConfiguredHierarchicalStyle());
setTitle(getConfiguredTitle());
setAutoDiscardOnDelete(getConfiguredAutoDiscardOnDelete());
setSortEnabled(getConfiguredSortEnabled());
setDefaultIconId(getConfiguredDefaultIconId());
setCssClass((getConfiguredCssClass()));
setRowIconVisible(getConfiguredRowIconVisible());
setRowIconColumnWidth(getConfiguredRowIconColumnWidth());
setHeaderVisible(getConfiguredHeaderVisible());
setHeaderEnabled(getConfiguredHeaderEnabled());
setAutoResizeColumns(getConfiguredAutoResizeColumns());
setCheckable(getConfiguredCheckable());
setMultiCheck(getConfiguredMultiCheck());
setMultiSelect(getConfiguredMultiSelect());
setInitialMultilineText(getConfiguredMultilineText());
setMultilineText(getConfiguredMultilineText());
setKeyboardNavigation(getConfiguredKeyboardNavigation());
setDragType(getConfiguredDragType());
setDropType(getConfiguredDropType());
setDropMaximumSize(getConfiguredDropMaximumSize());
setScrollToSelection(getConfiguredScrollToSelection());
setTableStatusVisible(getConfiguredTableStatusVisible());
if (getTableCustomizer() == null) {
setTableCustomizer(createTableCustomizer());
}
// columns
createColumnsInternal();
// table controls
createTableControlsInternal();
// menus
initMenus();
// key strokes
List<Class<? extends IKeyStroke>> ksClasses = getConfiguredKeyStrokes();
List<IKeyStroke> ksList = new ArrayList<>(ksClasses.size());
for (Class<? extends IKeyStroke> clazz : ksClasses) {
IKeyStroke ks = ConfigurationUtility.newInnerInstance(this, clazz);
ks.init();
if (ks.getKeyStroke() != null) {
ksList.add(ks);
}
}
//add ENTER key stroke when default menu is used or execRowAction has an override
Class<? extends IMenu> defaultMenuType = getDefaultMenuInternal();
if (defaultMenuType != null || ConfigurationUtility.isMethodOverwrite(AbstractTable.class, "execRowAction", new Class[]{ITableRow.class}, this.getClass())) {
ksList.add(new KeyStroke("ENTER") {
@Override
protected void execAction() {
fireRowAction(getSelectedRow());
}
});
}
// add keystroke contributions
List<IKeyStroke> contributedKeyStrokes = m_contributionHolder.getContributionsByClass(IKeyStroke.class);
ksList.addAll(contributedKeyStrokes);
setKeyStrokes(ksList);
m_tableOrganizer = BEANS.get(ITableOrganizerProvider.class).createTableOrganizer(this);
// add Convenience observer for drag & drop callbacks, event history and ui sort possible check
addTableListener(new TableAdapter() {
@Override
public void tableChanged(TableEvent e) {
//event history
IEventHistory<TableEvent> h = getEventHistory();
if (h != null) {
h.notifyEvent(e);
}
//dnd
switch (e.getType()) {
case TableEvent.TYPE_ROWS_DRAG_REQUEST: {
if (e.getDragObject() == null) {
try {
e.setDragObject(interceptDrag(e.getRows()));
}
catch (RuntimeException ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
}
break;
}
case TableEvent.TYPE_ROW_DROP_ACTION: {
if (e.getDropObject() != null && isEnabled()) {
try {
interceptDrop(e.getFirstRow(), e.getDropObject());
}
catch (RuntimeException ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
}
break;
}
case TableEvent.TYPE_ROWS_COPY_REQUEST: {
if (e.getCopyObject() == null) {
try {
e.setCopyObject(interceptCopy(e.getRows()));
}
catch (RuntimeException ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
}
break;
}
case TableEvent.TYPE_ALL_ROWS_DELETED:
case TableEvent.TYPE_ROWS_DELETED:
case TableEvent.TYPE_ROWS_INSERTED:
case TableEvent.TYPE_ROWS_UPDATED: {
if (isValueChangeTriggerEnabled()) {
try {
interceptContentChanged();
}
catch (RuntimeException ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
}
break;
}
case TableEvent.TYPE_ROWS_CHECKED:
try {
interceptRowsChecked(e.getRows());
}
catch (RuntimeException ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
break;
case TableEvent.TYPE_COLUMN_HEADERS_UPDATED:
case TableEvent.TYPE_COLUMN_STRUCTURE_CHANGED:
checkIfColumnPreventsUiSortForTable();
break;
}
}
},
TableEvent.TYPE_ROWS_DRAG_REQUEST,
TableEvent.TYPE_ROW_DROP_ACTION,
TableEvent.TYPE_ROWS_COPY_REQUEST,
TableEvent.TYPE_ALL_ROWS_DELETED,
TableEvent.TYPE_ROWS_DELETED,
TableEvent.TYPE_ROWS_INSERTED,
TableEvent.TYPE_ROWS_UPDATED,
TableEvent.TYPE_ROWS_CHECKED,
TableEvent.TYPE_COLUMN_HEADERS_UPDATED,
TableEvent.TYPE_COLUMN_STRUCTURE_CHANGED);
}
protected void initMenus() {
List<Class<? extends IMenu>> ma = getDeclaredMenus();
OrderedCollection<IMenu> menus = new OrderedCollection<>();
Map<Class<?>, Class<? extends IMenu>> replacements = ConfigurationUtility.getReplacementMapping(ma);
if (!replacements.isEmpty()) {
m_menuReplacementMapping = replacements;
}
for (Class<? extends IMenu> clazz : ma) {
IMenu menu = ConfigurationUtility.newInnerInstance(this, clazz);
menus.addOrdered(menu);
}
List<IMenu> contributedMenus = m_contributionHolder.getContributionsByClass(IMenu.class);
menus.addAllOrdered(contributedMenus);
injectMenusInternal(menus);
addHeaderMenus(menus);
//set container on menus
for (IMenu menu : menus) {
menu.setContainerInternal(this);
}
new MoveActionNodesHandler<>(menus).moveModelObjects();
ITableContextMenu contextMenu = new TableContextMenu(this, menus.getOrderedList());
setContextMenu(contextMenu);
}
@Override
public AbstractEventBuffer<TableEvent> createEventBuffer() {
return BEANS.get(TableEventBuffer.class);
}
protected AbstractEventBuffer<TableEvent> getEventBuffer() {
return m_eventBuffer;
}
private void initColumnsInternal() {
getColumnSet().initColumns();
}
private void disposeColumnsInternal() {
getColumnSet().disposeColumns();
}
private void createTableControlsInternal() {
List<Class<? extends ITableControl>> tcs = getConfiguredTableControls();
OrderedCollection<ITableControl> tableControls = new OrderedCollection<>();
for (Class<? extends ITableControl> clazz : tcs) {
ITableControl tableControl = ConfigurationUtility.newInnerInstance(this, clazz);
((AbstractTableControl) tableControl).setTable(this);
tableControls.addOrdered(tableControl);
}
m_tableControls = tableControls.getOrderedList();
}
private void createColumnsInternal() {
List<Class<? extends IColumn>> ca = getConfiguredColumns();
OrderedCollection<IColumn<?>> columns = new OrderedCollection<>();
// configured columns
for (Class<? extends IColumn> clazz : ca) {
IColumn<?> column = ConfigurationUtility.newInnerInstance(this, clazz);
columns.addOrdered(column);
}
// contributed columns
List<IColumn> contributedColumns = m_contributionHolder.getContributionsByClass(IColumn.class);
for (IColumn c : contributedColumns) {
columns.addOrdered(c);
}
// dynamically injected columns
injectColumnsInternal(columns);
// move columns
ExtensionUtility.moveModelObjects(columns);
m_columnSet = new ColumnSet(this, columns.getOrderedList());
if (getConfiguredCheckableColumn() != null) {
AbstractBooleanColumn checkableColumn = getColumnSet().getColumnByClass(getConfiguredCheckableColumn());
setCheckableColumn(checkableColumn);
}
PropertyChangeListener columnVisibleListener = evt -> {
// disable ui sort possible property if needed
checkIfColumnPreventsUiSortForTable();
// prevent invisible context column (because the UI does not know of invisible columns)
checkIfContextColumnIsVisible();
};
for (IColumn column : m_columnSet.getColumns()) {
column.addPropertyChangeListener(IColumn.PROP_VISIBLE, columnVisibleListener);
}
}
/**
* Override this internal method only in order to make use of dynamic columns<br>
* To change the order or specify the insert position use {@link IColumn#setOrder(double)}.
*
* @param columns
* live and mutable collection of configured columns, not yet initialized
*/
protected void injectColumnsInternal(OrderedCollection<IColumn<?>> columns) {
ITableCustomizer c = getTableCustomizer();
if (c != null) {
c.injectCustomColumns(columns);
}
List<? extends IColumn<?>> dynamicColumns = getDynamicColumns();
if (dynamicColumns != null) {
columns.addAllOrdered(dynamicColumns);
}
}
/**
* Override this internal method only in order to make use of dynamic menus<br>
* Used to manage menu list and add/remove menus.<br>
* To change the order or specify the insert position use {@link IMenu#setOrder(double)}.
*
* @param menus
* live and mutable collection of configured menus
*/
protected void injectMenusInternal(OrderedCollection<IMenu> menus) {
}
protected ITableUIFacade createUIFacade() {
return new P_TableUIFacade();
}
/*
* Runtime
*/
@Override
public String getUserPreferenceContext() {
return m_userPreferenceContext;
}
@Override
public void setUserPreferenceContext(String context) {
m_userPreferenceContext = context;
if (isInitDone()) {
//re-initialize
try {
reinit();
}
catch (RuntimeException e) {
LOG.error("Failed re-initializing table {}", getClass().getName(), e);
}
}
}
@Override
protected final void initInternal() {
super.initInternal();
try {
if (m_initLock.acquire()) {
try {
setTableChanging(true);
initTableInternal();
interceptInitTable();
}
finally {
setTableChanging(false);
}
}
}
finally {
m_initLock.release();
}
}
/**
* @deprecated will be removed with 8.0, use {@link #init()} {@link #reinit()} or {@link #initInternal()} instead
*/
@Deprecated
@SuppressWarnings("deprecation")
@Override
public final void initTable() {
init();
}
protected void initTableInternal() {
initColumnsInternal();
if (getUserFilterManager() == null) {
setUserFilterManager(createUserFilterManager());
}
}
@Override
protected final void disposeInternal() {
try {
disposeTableInternal();
interceptDisposeTable();
}
catch (Exception e) {
LOG.error("Could not dispose table [{}]", getClass().getName(), e);
}
super.disposeInternal();
}
@SuppressWarnings("deprecation")
@Override
public final void disposeTable() {
dispose();
}
protected void disposeTableInternal() {
disposeColumnsInternal();
}
@Override
public void doAppLinkAction(String ref) {
if (isActionRunning()) {
return;
}
try {
setActionRunning(true);
interceptAppLinkAction(ref);
}
finally {
setActionRunning(false);
}
}
@Override
public void addAttachment(BinaryResource attachment) {
if (attachment != null) {
m_attachments.put(attachment.getFilename(), attachment);
}
}
@Override
public Set<BinaryResource> getAttachments() {
return CollectionUtility.hashSet(m_attachments.values());
}
@Override
public BinaryResource getAttachment(String filename) {
return m_attachments.get(filename);
}
@Override
public void removeAttachment(BinaryResource attachment) {
if (attachment != null) {
m_attachments.remove(attachment.getFilename());
}
}
@Override
public List<ITableRowFilter> getRowFilters() {
return CollectionUtility.arrayList(m_rowFilters);
}
@Override
public void addRowFilter(ITableRowFilter filter) {
if (filter != null && !m_rowFilters.contains(filter)) {
m_rowFilters.add(filter);
applyRowFilters();
}
}
@Override
public void removeRowFilter(ITableRowFilter filter) {
if (filter != null && m_rowFilters.remove(filter)) {
applyRowFilters();
}
}
public void removeUserRowFilters() {
removeUserRowFilters(true);
}
public void removeUserRowFilters(boolean applyRowFilters) {
for (ITableRowFilter filter : getRowFilters()) {
if (filter instanceof UserTableRowFilter) {
m_rowFilters.remove(filter);
}
}
if (applyRowFilters) {
applyRowFilters();
}
}
@Override
public void applyRowFilters() {
boolean filterChanged = applyRowFiltersInternal();
if (filterChanged) {
fireRowFilterChanged();
}
}
private boolean applyRowFiltersInternal() {
boolean filterChanged = false;
for (ITableRow row : m_rows) {
boolean wasFilterAccepted = row.isFilterAccepted();
applyRowFiltersInternal((InternalTableRow) row);
if (row.isFilterAccepted() != wasFilterAccepted) {
filterChanged = true;
}
}
return filterChanged;
}
private void applyRowFiltersInternal(InternalTableRow row) {
List<ITableRowFilter> rejectingFilters = new ArrayList<>();
row.setFilterAcceptedInternal(true);
row.setRejectedByUser(false);
if (!m_rowFilters.isEmpty()) {
for (ITableRowFilter filter : m_rowFilters) {
if (!filter.accept(row)) {
row.setFilterAcceptedInternal(false);
/*
* ticket 95770
*/
if (isSelectedRow(row)) {
deselectRow(row);
}
rejectingFilters.add(filter);
}
}
}
// Prefer row.isRejectedByUser to allow a filter to set this flag
row.setRejectedByUser(row.isRejectedByUser() || rejectingFilters.size() == 1 && rejectingFilters.get(0) instanceof IUserFilter);
}
@Override
public String getTitle() {
return propertySupport.getPropertyString(PROP_TITLE);
}
@Override
public void setTitle(String s) {
propertySupport.setPropertyString(PROP_TITLE, s);
}
private boolean isSortValid() {
return FLAGS_BIT_HELPER.isBitSet(SORT_VALID, m_flags);
}
private void setSortValid(boolean valid) {
m_flags = FLAGS_BIT_HELPER.changeBit(SORT_VALID, valid, m_flags);
}
private boolean isActionRunning() {
return FLAGS_BIT_HELPER.isBitSet(ACTION_RUNNING, m_flags);
}
private void setActionRunning(boolean running) {
m_flags = FLAGS_BIT_HELPER.changeBit(ACTION_RUNNING, running, m_flags);
}
@Override
public boolean isAutoResizeColumns() {
return propertySupport.getPropertyBool(PROP_AUTO_RESIZE_COLUMNS);
}
@Override
public void setAutoResizeColumns(boolean b) {
propertySupport.setPropertyBool(PROP_AUTO_RESIZE_COLUMNS, b);
}
@Override
public void expandAll(ITableRow startRow) {
expandAllInternal(startRow, true);
}
@Override
public void collapseAll(ITableRow startRow) {
expandAllInternal(startRow, false);
}
public void expandRows(List<ITableRow> rows) {
expandRowsInternal(rows, true);
}
public void collapseRows(List<ITableRow> rows) {
expandRowsInternal(rows, false);
}
private void expandAllInternal(ITableRow startRow, boolean expanded) {
final List<ITableRow> rows;
if (startRow != null) {
rows = CollectionUtility.arrayList(startRow);
}
else {
rows = m_rootRows;
}
CollectingVisitor<ITableRow> collector = new CollectingVisitor<ITableRow>() {
@Override
protected boolean accept(ITableRow element) {
return element.isExpanded() != expanded;
}
};
rows.forEach(root -> TreeTraversals.create(collector, ITableRow::getChildRows).traverse(root));
expandRowsInternal(collector.getCollection(), expanded);
}
protected void expandRowsInternal(List<? extends ITableRow> rows, boolean expanded) {
try {
setTableChanging(true);
List<? extends ITableRow> changedRows = rows.stream().filter(row -> row.setExpanded(expanded)).collect(Collectors.toList());
fireRowsExpanded(changedRows);
}
finally {
setTableChanging(false);
}
}
@Override
public boolean isExpanded(ITableRow row) {
return false;
}
@Override
public void setRowExpanded(ITableRow row, boolean expanded) {
if (row == null || row.isExpanded() == expanded) {
return;
}
expandRowsInternal(CollectionUtility.arrayList(row), expanded);
}
@Override
public ColumnSet getColumnSet() {
return m_columnSet;
}
@Override
public int getColumnCount() {
return getColumnSet().getColumnCount();
}
@Override
public List<IColumn<?>> getColumns() {
return getColumnSet().getColumns();
}
@Override
public List<String> getColumnNames() {
List<String> columnNames = new ArrayList<>(getColumnCount());
for (IColumn col : getColumns()) {
columnNames.add(col.getHeaderCell().getText());
}
return columnNames;
}
@Override
public int getVisibleColumnCount() {
return getColumnSet().getVisibleColumnCount();
}
@Override
public IHeaderCell getVisibleHeaderCell(int visibleColumnIndex) {
return getHeaderCell(getColumnSet().getVisibleColumn(visibleColumnIndex));
}
@Override
public IHeaderCell getHeaderCell(int columnIndex) {
return getHeaderCell(getColumnSet().getColumn(columnIndex));
}
@Override
public IHeaderCell getHeaderCell(IColumn<?> col) {
return col.getHeaderCell();
}
@Override
public ICell getVisibleCell(int rowIndex, int visibleColumnIndex) {
return getVisibleCell(getRow(rowIndex), visibleColumnIndex);
}
@Override
public ICell getVisibleCell(ITableRow row, int visibleColumnIndex) {
return getCell(row, getColumnSet().getVisibleColumn(visibleColumnIndex));
}
@Override
public ICell getCell(int rowIndex, int columnIndex) {
return getCell(getRow(rowIndex), getColumnSet().getColumn(columnIndex));
}
@Override
public ICell getSummaryCell(int rowIndex) {
return getSummaryCell(getRow(rowIndex));
}
@Override
public ICell getSummaryCell(ITableRow row) {
List<IColumn<?>> a = getColumnSet().getSummaryColumns();
if (a.isEmpty()) {
IColumn<?> col = getColumnSet().getFirstDefinedVisibileColumn();
if (col != null) {
a = CollectionUtility.arrayList(col);
}
}
if (a.isEmpty()) {
return new Cell();
}
else if (a.size() == 1) {
Cell cell = new Cell(getCell(row, a.get(0)));
if (cell.getIconId() == null) {
// use icon of row
cell.setIconId(row.getIconId());
}
return cell;
}
else {
Cell cell = new Cell(getCell(row, a.get(0)));
if (cell.getIconId() == null) {
// use icon of row
cell.setIconId(row.getIconId());
}
StringBuilder b = new StringBuilder();
for (IColumn<?> c : a) {
if (b.length() > 0) {
b.append(" ");
}
b.append(getCell(row, c).getText());
}
cell.setText(b.toString());
return cell;
}
}
@Override
public ICell getCell(ITableRow row, IColumn<?> col) {
row = resolveRow(row);
if (row == null || col == null) {
return null;
}
return row.getCell(col.getColumnIndex());
}
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return isCellEditable(getRow(rowIndex), getColumnSet().getColumn(columnIndex));
}
@Override
public boolean isCellEditable(ITableRow row, IColumn<?> column) {
return row != null && column != null && column.isCellEditable(row);
}
@Override
public Object getProperty(String name) {
return propertySupport.getProperty(name);
}
@Override
public void setProperty(String name, Object value) {
propertySupport.setProperty(name, value);
}
@Override
public boolean hasProperty(String name) {
return propertySupport.hasProperty(name);
}
@Override
public boolean isCheckable() {
return propertySupport.getPropertyBool(PROP_CHECKABLE);
}
@Override
public void setCheckable(boolean b) {
propertySupport.setPropertyBool(PROP_CHECKABLE, b);
}
@Override
public void setDragType(int dragType) {
propertySupport.setPropertyInt(PROP_DRAG_TYPE, dragType);
}
@Override
public int getDragType() {
return propertySupport.getPropertyInt(PROP_DRAG_TYPE);
}
@Override
public void setDropType(int dropType) {
propertySupport.setPropertyInt(PROP_DROP_TYPE, dropType);
}
@Override
public int getDropType() {
return propertySupport.getPropertyInt(PROP_DROP_TYPE);
}
@Override
public void setDropMaximumSize(long dropMaximumSize) {
propertySupport.setPropertyLong(PROP_DROP_MAXIMUM_SIZE, dropMaximumSize);
}
@Override
public long getDropMaximumSize() {
return propertySupport.getPropertyInt(PROP_DROP_MAXIMUM_SIZE);
}
@Override
public boolean isMultilineText() {
return propertySupport.getPropertyBool(PROP_MULTILINE_TEXT);
}
@Override
public void setMultilineText(boolean on) {
propertySupport.setPropertyBool(PROP_MULTILINE_TEXT, on);
}
@Override
public boolean isInitialMultilineText() {
return FLAGS_BIT_HELPER.isBitSet(INITIAL_MULTI_LINE_TEXT, m_flags);
}
@Override
public void setInitialMultilineText(boolean on) {
m_flags = FLAGS_BIT_HELPER.changeBit(INITIAL_MULTI_LINE_TEXT, on, m_flags);
}
@Override
public boolean hasKeyboardNavigation() {
return propertySupport.getPropertyBool(PROP_KEYBOARD_NAVIGATION);
}
@Override
public void setKeyboardNavigation(boolean on) {
propertySupport.setPropertyBool(PROP_KEYBOARD_NAVIGATION, on);
}
@Override
public boolean isMultiSelect() {
return propertySupport.getPropertyBool(PROP_MULTI_SELECT);
}
@Override
public void setMultiSelect(boolean b) {
propertySupport.setPropertyBool(PROP_MULTI_SELECT, b);
}
@Override
public boolean isMultiCheck() {
return propertySupport.getPropertyBool(PROP_MULTI_CHECK);
}
@Override
public void setMultiCheck(boolean b) {
propertySupport.setPropertyBool(PROP_MULTI_CHECK, b);
}
@Override
public IBooleanColumn getCheckableColumn() {
return m_checkableColumn;
}
@Override
public void setCheckableColumn(IBooleanColumn checkableColumn) {
m_checkableColumn = checkableColumn;
}
@Override
public boolean isAutoDiscardOnDelete() {
return FLAGS_BIT_HELPER.isBitSet(AUTO_DISCARD_ON_DELETE, m_flags);
}
@Override
public void setAutoDiscardOnDelete(boolean on) {
m_flags = FLAGS_BIT_HELPER.changeBit(AUTO_DISCARD_ON_DELETE, on, m_flags);
}
/**
* @deprecated will be removed with 8.0, use {@link #isInitDone()} instead
*/
@Override
@Deprecated
public boolean isTableInitialized() {
return isInitDone();
}
@Override
public boolean isTableChanging() {
return m_tableChanging > 0;
}
@Override
@SuppressWarnings("squid:S1143")
public void setTableChanging(boolean b) {
// use a stack counter because setTableChanging might be called in nested
// loops
if (b) {
m_tableChanging++;
if (m_tableChanging == 1) {
// 0 --> 1
propertySupport.setPropertiesChanging(true);
}
}
else {
// all calls to further methods are wrapped into a try-catch block so that the change counters are adjusted correctly
if (m_tableChanging > 0) {
Throwable saveEx = null;
try {
if (m_tableChanging == 1) {
if (m_treeStructureDirty) {
rebuildTreeStructureInternal();
}
//will be going to zero, but process decorations here, so events are added to the event buffer
processDecorationBuffer();
if (!isSortValid()) {
sort();
}
}
}
catch (RuntimeException | PlatformError t) {
// covers all unchecked exceptions
saveEx = t;
throw t;
}
finally {
// exceptions in above try-block will not be thrown if the finally-block throws, thus we suppress them.
m_tableChanging--;
if (m_tableChanging == 0) {
try {
processEventBuffer();
}
catch (RuntimeException | PlatformError t) {
if (saveEx != null) {
saveEx.addSuppressed(t);
}
else {
saveEx = t;
throw t;
}
}
finally {
try {
propertySupport.setPropertiesChanging(false);
}
catch (RuntimeException | PlatformError t) {
if (saveEx != null) {
saveEx.addSuppressed(t);
}
else {
throw t;
}
}
}
}
}
}
}
}
@Override
public List<IKeyStroke> getKeyStrokes() {
return CollectionUtility.arrayList(getKeyStrokesInternal());
}
/**
* Returns a modifiable live list
*/
protected List<IKeyStroke> getKeyStrokesInternal() {
return propertySupport.<IKeyStroke> getPropertyList(PROP_KEY_STROKES);
}
@Override
public List<? extends IWidget> getChildren() {
return CollectionUtility.flatten(super.getChildren(), getMenus(), getKeyStrokesInternal(), m_tableControls);
}
@Override
public void setKeyStrokes(List<? extends IKeyStroke> keyStrokes0) {
propertySupport.setPropertyList(PROP_KEY_STROKES, CollectionUtility.arrayListWithoutNullElements(keyStrokes0));
}
@Override
public void requestFocus() {
fireRequestFocus();
}
@Override
public void requestFocusInCell(IColumn<?> column, ITableRow row) {
if (isCellEditable(row, column)) {
fireRequestFocusInCell(column, row);
}
}
@Override
public ITableRowDataMapper createTableRowDataMapper(Class<? extends AbstractTableRowData> rowType) {
return interceptCreateTableRowDataMapper(rowType);
}
/**
* Creates a {@link TableRowDataMapper} that is used for reading and writing data from the given
* {@link AbstractTableRowData} type.
*
* @param rowType
* @return
* @since 3.8.2
*/
@ConfigOperation
@Order(130)
protected ITableRowDataMapper execCreateTableRowDataMapper(Class<? extends AbstractTableRowData> rowType) {
return new TableRowDataMapper(rowType, getColumnSet());
}
@Override
public void exportToTableBeanData(AbstractTableFieldBeanData target) {
ITableRowDataMapper rowMapper = createTableRowDataMapper(target.getRowType());
for (int i = 0, ni = getRowCount(); i < ni; i++) {
ITableRow row = getRow(i);
if (rowMapper.acceptExport(row)) {
AbstractTableRowData rowData = target.addRow();
rowMapper.exportTableRowData(row, rowData);
}
}
List<ITableRow> deletedRows = getDeletedRows();
for (ITableRow delRow : deletedRows) {
if (rowMapper.acceptExport(delRow)) {
AbstractTableRowData rowData = target.addRow();
rowMapper.exportTableRowData(delRow, rowData);
rowData.setRowState(AbstractTableRowData.STATUS_DELETED);
}
}
}
@Override
public void importFromTableBeanData(AbstractTableFieldBeanData source) {
importFromTableRowBeanData(CollectionUtility.arrayList(source.getRows()), source.getRowType());
}
public void importFromTableRowBeanData(List<? extends AbstractTableRowData> rowDatas, Class<? extends AbstractTableRowData> rowType) {
discardAllDeletedRows();
int deleteCount = 0;
List<ITableRow> newRows = new ArrayList<>(rowDatas.size());
ITableRowDataMapper mapper = createTableRowDataMapper(rowType);
for (AbstractTableRowData rowData : rowDatas) {
if (rowData.getRowState() != AbstractTableRowData.STATUS_DELETED && mapper.acceptImport(rowData)) {
ITableRow newTableRow = new TableRow(getColumnSet());
mapper.importTableRowData(newTableRow, rowData);
newRows.add(newTableRow);
}
else {
deleteCount++;
}
}
replaceRows(newRows);
if (deleteCount > 0) {
try {
setTableChanging(true);
//
for (AbstractTableRowData rowData : rowDatas) {
if (rowData.getRowState() == AbstractTableRowData.STATUS_DELETED && mapper.acceptImport(rowData)) {
ITableRow newTableRow = new TableRow(getColumnSet());
mapper.importTableRowData(newTableRow, rowData);
newTableRow.setStatus(ITableRow.STATUS_NON_CHANGED);
ITableRow addedRow = addRow(newTableRow);
deleteRow(addedRow);
}
}
}
finally {
setTableChanging(false);
}
}
}
@Override
public void setMenus(List<? extends IMenu> menus) {
getContextMenu().setChildActions(menus);
}
@Override
public void addMenu(IMenu menu) {
List<IMenu> menus = getMenus();
menus.add(menu);
setMenus(menus);
}
protected void setContextMenu(ITableContextMenu contextMenu) {
propertySupport.setProperty(PROP_CONTEXT_MENU, contextMenu);
}
@Override
public ITableContextMenu getContextMenu() {
return (ITableContextMenu) propertySupport.getProperty(PROP_CONTEXT_MENU);
}
@Override
public List<IMenu> getMenus() {
return getContextMenu().getChildActions();
}
@Override
public <T extends IMenu> T getMenuByClass(Class<T> menuType) {
return MenuUtility.getMenuByClass(this, menuType);
}
@Override
public boolean runMenu(Class<? extends IMenu> menuType) {
Class<? extends IMenu> c = getReplacingMenuClass(menuType);
for (IMenu m : getMenus()) {
if (m.getClass() == c && ((!m.isInheritAccessibility()) || isEnabled())) {
if (m.isVisible() && m.isEnabled()) {
m.doAction();
return true;
}
else {
return false;
}
}
}
return false;
}
/**
* Checks whether the menu with the given class has been replaced by another menu. If so, the replacing menu's class
* is returned. Otherwise the given class itself.
*
* @param c
* @return Returns the possibly available replacing menu class for the given class.
* @see Replace
* @since 3.8.2
*/
private <T extends IMenu> Class<? extends T> getReplacingMenuClass(Class<T> c) {
if (m_menuReplacementMapping != null) {
@SuppressWarnings("unchecked")
Class<? extends T> replacingMenuClass = (Class<? extends T>) m_menuReplacementMapping.get(c);
if (replacingMenuClass != null) {
return replacingMenuClass;
}
}
return c;
}
/**
* factory to manage user filters
* <p>
* default creates a {@link TableUserFilterManager}
*/
protected TableUserFilterManager createUserFilterManager() {
return new TableUserFilterManager(this);
}
/**
* factory to manage custom columns
* <p>
* default creates null
*/
protected ITableCustomizer createTableCustomizer() {
return BEANS.get(ITableCustomizerProvider.class).createTableCustomizer(this);
}
/*
* Row handling methods. Operate on a Row instance.
*/
@Override
public ITableRow createRow() {
return new P_TableRowBuilder().createRow();
}
@Override
public ITableRow createRow(Object rowValues) {
return new P_TableRowBuilder().createRow(rowValues);
}
@Override
public List<ITableRow> createRowsByArray(Object dataArray) {
return new P_TableRowBuilder().createRowsByArray(dataArray);
}
@Override
public List<ITableRow> createRowsByArray(Object dataArray, int rowStatus) {
return new P_TableRowBuilder().createRowsByArray(dataArray, rowStatus);
}
/**
* Performance note:<br>
* Since the matrix may contain large amount of data, the Object[][] can be passed as new AtomicReference
* <Object>(Object[][]) so that the further processing can set the content of the holder to null while processing.
*/
@Override
public List<ITableRow> createRowsByMatrix(Object dataMatrixOrReference) {
return new P_TableRowBuilder().createRowsByMatrix(dataMatrixOrReference);
}
/**
* Performance note:<br>
* Since the matrix may contain large amount of data, the Object[][] can be passed as new AtomicReference
* <Object>(Object[][]) so that the further processing can set the content of the holder to null while processing.
*/
@Override
public List<ITableRow> createRowsByMatrix(Object dataMatrixOrReference, int rowStatus) {
return new P_TableRowBuilder().createRowsByMatrix(dataMatrixOrReference, rowStatus);
}
@Override
public List<ITableRow> createRowsByCodes(Collection<? extends ICode<?>> codes) {
return new P_TableRowBuilder().createRowsByCodes(codes);
}
/**
* Performance note:<br>
* Since the matrix may contain large amount of data, the Object[][] can be passed as new AtomicReference
* <Object>(Object[][]) so that the further processing can set the content of the holder to null while processing.
*/
@Override
public void replaceRowsByMatrix(Object dataMatrixOrReference) {
replaceRows(createRowsByMatrix(dataMatrixOrReference));
}
@Override
public void replaceRowsByArray(Object dataArray) {
replaceRows(createRowsByArray(dataArray));
}
@Override
public void replaceRows(List<? extends ITableRow> newRows) {
/*
* There are two ways to replace: (1) Completely replace all rows by
* discarding all rows and adding new rows when - autoDiscardOnDelete=true
* (2) Replace rows by applying insert/update/delete on existing rows by
* primary key match when - autoDiscardOnDelete=false
*/
if (isAutoDiscardOnDelete()) {
replaceRowsCase1(newRows);
}
else {
replaceRowsCase2(newRows);
}
}
/**
* Replace rows discarding deleted rows
*/
private void replaceRowsCase1(List<? extends ITableRow> newRows) {
try {
setTableChanging(true);
//
List<CompositeObject> selectedKeys = getSelectedKeys();
discardAllRows();
addRows(newRows, false);
restoreSelection(selectedKeys);
}
finally {
setTableChanging(false);
}
}
private List<CompositeObject> getSelectedKeys() {
List<CompositeObject> selectedKeys = new ArrayList<>();
for (ITableRow r : getSelectedRows()) {
selectedKeys.add(new CompositeObject(getRowKeys(r)));
}
return selectedKeys;
}
private void restoreSelection(List<CompositeObject> selectedKeys) {
List<ITableRow> selectedRows = new ArrayList<>();
if (!selectedKeys.isEmpty()) {
for (ITableRow r : m_rows) {
if (selectedKeys.remove(new CompositeObject(getRowKeys(r)))) {
selectedRows.add(r);
if (selectedKeys.isEmpty()) {
break;
}
}
}
}
selectRows(selectedRows, false);
}
/**
* Replace rows by applying insert/update/delete on existing rows by primary key match
*/
private void replaceRowsCase2(List<? extends ITableRow> newRows) {
try {
setTableChanging(true);
//
int[] oldToNew = new int[getRowCount()];
int[] newToOld = new int[newRows.size()];
Arrays.fill(oldToNew, -1);
Arrays.fill(newToOld, -1);
Map<CompositeObject, Integer> newRowIndexMap = new HashMap<>();
for (int i = newRows.size() - 1; i >= 0; i--) {
newRowIndexMap.put(new CompositeObject(getRowKeys(newRows.get(i))), i);
}
int mappedCount = 0;
for (int i = 0, ni = getRowCount(); i < ni; i++) {
ITableRow existingRow = m_rows.get(i);
Integer newIndex = newRowIndexMap.remove(new CompositeObject(getRowKeys(existingRow)));
if (newIndex != null) {
oldToNew[i] = newIndex.intValue();
newToOld[newIndex.intValue()] = i;
mappedCount++;
}
}
List<ITableRow> updatedRows = new ArrayList<>(mappedCount);
for (int i = 0; i < oldToNew.length; i++) {
if (oldToNew[i] >= 0) {
ITableRow existingRow = getRow(i);
ITableRow newRow = newRows.get(oldToNew[i]);
replaceRowValues(existingRow, newRow);
updatedRows.add(existingRow);
}
}
List<ITableRow> deletedRows = new ArrayList<>(getRowCount() - mappedCount);
for (int i = 0; i < oldToNew.length; i++) {
if (oldToNew[i] < 0) {
deletedRows.add(m_rows.get(i));
}
}
List<ITableRow> insertedRows = new ArrayList<>(newRows.size() - mappedCount);
int[] insertedRowIndexes = new int[newRows.size() - mappedCount];
int index = 0;
for (int i = 0; i < newToOld.length; i++) {
if (newToOld[i] < 0) {
insertedRows.add(newRows.get(i));
insertedRowIndexes[index] = i;
index++;
}
}
//
updateRows(updatedRows);
deleteRows(deletedRows);
addRows(insertedRows, false, insertedRowIndexes);
}
finally {
setTableChanging(false);
}
}
/**
* Update existing row with values from new row
*/
private void replaceRowValues(ITableRow existingRow, ITableRow newRow) {
try {
existingRow.setRowChanging(true);
//
existingRow.setEnabled(newRow.isEnabled());
existingRow.setStatus(newRow.getStatus());
//map values
for (IColumn<?> col : getColumns()) {
int columnIndex = col.getColumnIndex();
Object newValue = null;
if (columnIndex < newRow.getCellCount()) {
newValue = newRow.getCellValue(columnIndex);
}
col.parseValueAndSet(existingRow, newValue);
}
}
finally {
existingRow.setRowPropertiesChanged(false);
existingRow.setRowChanging(false);
}
}
@Override
public void updateRow(ITableRow row) {
if (row != null) {
updateRows(CollectionUtility.arrayList(row));
}
}
@Override
public void updateAllRows() {
updateRows(getRows());
}
@Override
public void setRowState(ITableRow row, int rowState) {
setRowState(CollectionUtility.arrayList(row), rowState);
}
@Override
public void setAllRowState(int rowState) {
setRowState(getRows(), rowState);
}
@Override
public void setRowState(Collection<? extends ITableRow> rows, int rowState) {
try {
setTableChanging(true);
for (ITableRow row : rows) {
row.setStatus(rowState);
}
}
finally {
setTableChanging(false);
}
}
@Override
public void updateRows(Collection<? extends ITableRow> rows) {
try {
setTableChanging(true);
//
List<ITableRow> resolvedRowList = new ArrayList<>(rows.size());
for (ITableRow row : rows) {
ITableRow resolvedRow = resolveRow(row);
if (resolvedRow != null) {
resolvedRowList.add(resolvedRow);
updateRowImpl(resolvedRow);
}
}
if (!resolvedRowList.isEmpty()) {
fireRowsUpdated(resolvedRowList);
}
}
finally {
setTableChanging(false);
}
}
private void updateRowImpl(ITableRow row) {
if (row != null) {
/*
* do NOT use ITableRow#setRowChanging, this might cause a stack overflow
*/
ensureInvalidColumnsVisible(row);
Set<Integer> changedColumnValues = row.getUpdatedColumnIndexes(ICell.VALUE_BIT);
if (CollectionUtility.containsAny(changedColumnValues, IntStream.of(getColumnSet().getKeyColumnIndexes()).boxed().toArray(Integer[]::new))) {
// update primary key
m_rowsByKey.values().remove(row);
m_rowsByKey.put(new CompositeObject(row.getKeyValues()), row);
}
if (CollectionUtility.containsAny(changedColumnValues, getColumnSet().getSortColumns().stream().map(col -> col.getColumnIndex()).collect(Collectors.toSet()))) {
// sort has to be updated
// restore order of rows according to sort criteria
if (isTableChanging()) {
setSortValid(false);
}
else {
sort();
}
}
if (!changedColumnValues.isEmpty()) {
enqueueValueChangeTasks(row, changedColumnValues);
}
enqueueDecorationTasks(row);
}
}
@Override
public void ensureInvalidColumnsVisible() {
List<ITableRow> rows = getRows();
for (ITableRow row : rows) {
ensureInvalidColumnsVisible(row);
}
}
private void ensureInvalidColumnsVisible(ITableRow row) {
for (IColumn<?> col : getColumns()) {
col.ensureVisibileIfInvalid(row);
}
}
@Override
public int getRowCount() {
return m_rows.size();
}
@Override
public int getDeletedRowCount() {
return m_deletedRows.size();
}
@Override
public int getSelectedRowCount() {
return m_selectedRows.size();
}
@Override
public ITableRow getSelectedRow() {
return CollectionUtility.firstElement(m_selectedRows);
}
@Override
public List<ITableRow> getSelectedRows() {
return CollectionUtility.arrayList(m_selectedRows);
}
@Override
public boolean isSelectedRow(ITableRow row) {
row = resolveRow(row);
if (row == null) {
return false;
}
else {
return m_selectedRows.contains(row);
}
}
@Override
public boolean isCheckedRow(ITableRow row) {
row = resolveRow(row);
if (row == null) {
return false;
}
else {
return m_checkedRows.contains(row);
}
}
@Override
public void selectRow(int rowIndex) {
selectRow(getRow(rowIndex));
}
@Override
public void selectRow(ITableRow row) {
selectRow(row, false);
}
@Override
public void selectRow(ITableRow row, boolean append) {
selectRows(CollectionUtility.arrayList(row), append);
}
@Override
public void selectRows(List<? extends ITableRow> rows) {
selectRows(rows, false);
}
@Override
public void selectRows(List<? extends ITableRow> rows, boolean append) {
rows = resolveRows(rows);
TreeSet<ITableRow> newSelection = new TreeSet<>(new RowIndexComparator());
if (append) {
newSelection.addAll(m_selectedRows);
newSelection.addAll(rows);
}
else {
newSelection.addAll(rows);
}
// check selection count with multiselect
if (newSelection.size() > 1 && !isMultiSelect()) {
ITableRow first = newSelection.first();
newSelection.clear();
newSelection.add(first);
}
if (!CollectionUtility.equalsCollection(m_selectedRows, newSelection, true)) {
m_selectedRows = new ArrayList<>(newSelection);
// notify menus
List<ITableRow> notificationCopy = CollectionUtility.arrayList(m_selectedRows);
fireRowsSelected(notificationCopy);
}
}
@Override
public void selectFirstRow() {
selectRow(getRow(0));
}
@Override
public void selectNextRow() {
ITableRow row = getSelectedRow();
if (row != null && row.getRowIndex() + 1 < getRowCount()) {
selectRow(getRow(row.getRowIndex() + 1));
}
else if (row == null && getRowCount() > 0) {
selectRow(0);
}
}
@Override
public void selectPreviousRow() {
ITableRow row = getSelectedRow();
if (row != null && row.getRowIndex() - 1 >= 0) {
selectRow(getRow(row.getRowIndex() - 1));
}
else if (row == null && getRowCount() > 0) {
selectRow(getRowCount() - 1);
}
}
@Override
public void selectLastRow() {
selectRow(getRow(getRowCount() - 1));
}
@Override
public void deselectRow(ITableRow row) {
if (row != null) {
deselectRows(CollectionUtility.arrayList(row));
}
}
@Override
public void deselectRows(List<? extends ITableRow> rows) {
rows = resolveRows(rows);
if (CollectionUtility.hasElements(rows)) {
TreeSet<ITableRow> newSelection = new TreeSet<>(new RowIndexComparator());
newSelection.addAll(m_selectedRows);
if (newSelection.removeAll(rows)) {
m_selectedRows = new ArrayList<>(newSelection);
fireRowsSelected(m_selectedRows);
}
}
}
@Override
public void selectAllRows() {
selectRows(getRows(), false);
}
@Override
public void deselectAllRows() {
selectRow(null, false);
}
@Override
public void selectAllEnabledRows() {
List<ITableRow> newList = new ArrayList<>();
for (int i = 0, ni = getRowCount(); i < ni; i++) {
ITableRow row = getRow(i);
if (row.isEnabled()) {
newList.add(row);
}
else if (isSelectedRow(row)) {
newList.add(row);
}
}
selectRows(newList, false);
}
@Override
public void deselectAllEnabledRows() {
List<ITableRow> selectedRows = getSelectedRows();
List<ITableRow> newList = new ArrayList<>();
for (ITableRow selectedRow : selectedRows) {
if (selectedRow.isEnabled()) {
newList.add(selectedRow);
}
}
deselectRows(newList);
}
@Override
public List<ITableRow> getCheckedRows() {
return CollectionUtility.arrayList(m_checkedRows);
}
@Override
public void checkRow(int row, boolean value) {
checkRow(getRow(row), value);
}
@Override
public void checkRow(ITableRow row, boolean value) {
checkRows(CollectionUtility.arrayList(row), value);
}
@Override
public void checkRows(Collection<? extends ITableRow> rows, boolean value) {
checkRows(rows, value, false);
}
public void checkRows(Collection<? extends ITableRow> rows, boolean value, boolean enabledRowsOnly) {
try {
rows = resolveRows(rows);
// check checked count with multicheck
if (!isMultiCheck() && value) {
ITableRow rowToCheck = null;
for (ITableRow row : rows) {
if (row.isChecked() != value && (!enabledRowsOnly || row.isEnabled())) {
rowToCheck = row;
break;
}
}
if (rowToCheck != null) {
if (!enabledRowsOnly) {
uncheckAllRows();
}
else {
uncheckAllEnabledRows();
}
checkRowImpl(rowToCheck, value);
fireRowsChecked(CollectionUtility.arrayList(rowToCheck));
}
}
else {
List<ITableRow> rowsUpdated = new ArrayList<>();
for (ITableRow row : rows) {
if (row.isChecked() != value && (!enabledRowsOnly || row.isEnabled())) {
checkRowImpl(row, value);
rowsUpdated.add(row);
}
}
if (!rowsUpdated.isEmpty()) {
if (value) {
// sort checked rows if new checked rows have been added (not necessary if checked rows have been removed)
sortCheckedRows();
}
fireRowsChecked(CollectionUtility.arrayList(rowsUpdated));
}
}
}
catch (RuntimeException e) {
BEANS.get(ExceptionHandler.class).handle(e);
}
}
private void checkRowImpl(ITableRow row, boolean value) {
if (!(row instanceof InternalTableRow)) {
return;
}
InternalTableRow internalRow = (InternalTableRow) row;
if (value) {
m_checkedRows.add(internalRow);
}
else {
m_checkedRows.remove(internalRow);
}
if (getCheckableColumn() != null) {
getCheckableColumn().setValue(internalRow, value);
}
else {
// Do not use setStatus() or setStatusUpdated(), because this would trigger unnecessary UPDATED events
internalRow.setStatusInternal(ITableRow.STATUS_UPDATED);
}
}
@Override
public void checkAllRows() {
try {
setTableChanging(true);
checkRows(getRows(), true);
}
finally {
setTableChanging(false);
}
}
public void checkAllEnabledRows() {
try {
setTableChanging(true);
checkRows(getRows(), true, true);
}
finally {
setTableChanging(false);
}
}
@Override
public void uncheckRow(ITableRow row) {
checkRow(row, false);
}
@Override
public void uncheckRows(Collection<? extends ITableRow> rows) {
checkRows(rows, false);
}
@Override
public void uncheckAllEnabledRows() {
try {
setTableChanging(true);
checkRows(getRows(), false, true);
}
finally {
setTableChanging(false);
}
}
@Override
public void uncheckAllRows() {
try {
setTableChanging(true);
checkRows(getRows(), false);
}
finally {
setTableChanging(false);
}
}
@Override
public String getDefaultIconId() {
String iconId = propertySupport.getPropertyString(PROP_DEFAULT_ICON);
if (iconId != null && iconId.isEmpty()) {
iconId = null;
}
return iconId;
}
@Override
public void setDefaultIconId(String iconId) {
propertySupport.setPropertyString(PROP_DEFAULT_ICON, iconId);
}
@Override
public boolean isRowIconVisible() {
return propertySupport.getPropertyBool(PROP_ROW_ICON_VISIBLE);
}
@Override
public void setRowIconVisible(boolean rowIconVisible) {
propertySupport.setPropertyBool(PROP_ROW_ICON_VISIBLE, rowIconVisible);
}
@Override
public int getRowIconColumnWidth() {
return propertySupport.getPropertyInt(PROP_ROW_ICON_COLUMN_WIDTH);
}
@Override
public void setRowIconColumnWidth(int width) {
propertySupport.setPropertyInt(PROP_ROW_ICON_COLUMN_WIDTH, width);
}
@Override
public boolean isEnabled() {
return propertySupport.getPropertyBool(PROP_ENABLED);
}
@Override
public final void setEnabled(boolean enabled) {
setEnabled(enabled, IDimensions.ENABLED);
}
@Override
public void setEnabled(boolean enabled, String dimension) {
m_enabled = ENABLED_BIT_HELPER.changeBit(dimension, enabled, m_enabled);
setEnabledInternal();
}
@Override
public boolean isEnabled(String dimension) {
return ENABLED_BIT_HELPER.isBitSet(dimension, m_enabled);
}
private void setEnabledInternal() {
propertySupport.setPropertyBool(PROP_ENABLED, NamedBitMaskHelper.allBitsSet(m_enabled));
}
@Override
public boolean isScrollToSelection() {
return propertySupport.getPropertyBool(PROP_SCROLL_TO_SELECTION);
}
@Override
public void setScrollToSelection(boolean b) {
propertySupport.setPropertyBool(PROP_SCROLL_TO_SELECTION, b);
}
@Override
public void scrollToSelection() {
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_SCROLL_TO_SELECTION));
}
/**
* @return a copy of a row<br>
* when the row is changed it has to be applied to the table using modifyRow(row);
*/
@Override
public ITableRow getRow(int rowIndex) {
return CollectionUtility.getElement(getRows(), rowIndex);
}
@Override
public List<ITableRow> getRows() {
//lazy create list in getter, make sure to be thread-safe since getters may be called from "wild" threads
synchronized (m_cachedRowsLock) {
if (m_cachedRows == null) {
//this code must be thread-safe
m_cachedRows = CollectionUtility.arrayList(m_rows);
}
return m_cachedRows;
}
}
@Override
public List<ITableRow> getFilteredRows() {
List<ITableRow> rows = getRows();
if (!m_rowFilters.isEmpty()) {
//lazy create list in getter, make sure to be thread-safe since getters may be called from "wild" threads
synchronized (m_cachedFilteredRowsLock) {
if (m_cachedFilteredRows == null) {
//this code must be thread-safe
if (!m_rowFilters.isEmpty()) {
List<ITableRow> filteredRows = new ArrayList<>(getRowCount());
for (ITableRow row : rows) {
if (row != null && row.isFilterAccepted()) {
filteredRows.add(row);
}
}
m_cachedFilteredRows = filteredRows;
}
else {
m_cachedFilteredRows = CollectionUtility.emptyArrayList();
}
}
return m_cachedFilteredRows;
}
}
else {
return rows;
}
}
@Override
public int getFilteredRowCount() {
if (!m_rowFilters.isEmpty()) {
return getFilteredRows().size();
}
else {
return getRowCount();
}
}
@Override
public ITableRow getFilteredRow(int index) {
if (!m_rowFilters.isEmpty()) {
ITableRow row = null;
List<ITableRow> filteredRows = getFilteredRows();
if (index >= 0 && index < filteredRows.size()) {
row = filteredRows.get(index);
}
return row;
}
else {
return getRow(index);
}
}
@Override
public int getFilteredRowIndex(ITableRow row) {
return getFilteredRows().indexOf(row);
}
@Override
public List<ITableRow> getNotDeletedRows() {
List<ITableRow> notDeletedRows = new ArrayList<>();
for (ITableRow row : getRows()) {
if (row.getStatus() != ITableRow.STATUS_DELETED) {
notDeletedRows.add(row);
}
}
return notDeletedRows;
}
@Override
public int getNotDeletedRowCount() {
return getNotDeletedRows().size();
}
@Override
public Object[][] getTableData() {
Object[][] data = new Object[getRowCount()][getColumnCount()];
for (int r = 0; r < getRowCount(); r++) {
for (int c = 0; c < getColumnCount(); c++) {
data[r][c] = getRow(r).getCellValue(c);
}
}
return data;
}
@Override
public Object[][] exportTableRowsAsCSV(List<? extends ITableRow> rows, List<? extends IColumn> columns, boolean includeLineForColumnNames, boolean includeLineForColumnTypes, boolean includeLineForColumnFormat) {
return TableUtility.exportRowsAsCSV(rows, columns, includeLineForColumnNames, includeLineForColumnTypes, includeLineForColumnFormat);
}
@Override
public List<ITableRow> getRows(int[] rowIndexes) {
if (rowIndexes == null) {
return CollectionUtility.emptyArrayList();
}
List<ITableRow> result = new ArrayList<>(rowIndexes.length);
for (int rowIndex : rowIndexes) {
ITableRow row = getRow(rowIndex);
if (row != null) {
result.add(row);
}
}
return result;
}
/**
* @return a copy of a deleted row<br>
* when the row is changed it has to be applied to the table using modifyRow(row);
*/
@Override
public List<ITableRow> getDeletedRows() {
return CollectionUtility.arrayList(m_deletedRows.values());
}
@Override
public int getInsertedRowCount() {
int count = 0;
for (ITableRow row : getRows()) {
if (row.getStatus() == ITableRow.STATUS_INSERTED) {
count++;
}
}
return count;
}
@Override
public List<ITableRow> getInsertedRows() {
List<ITableRow> rowList = new ArrayList<>();
for (ITableRow row : getRows()) {
if (row.getStatus() == ITableRow.STATUS_INSERTED) {
rowList.add(row);
}
}
return rowList;
}
@Override
public int getUpdatedRowCount() {
int count = 0;
for (ITableRow row : getRows()) {
if (row.getStatus() == ITableRow.STATUS_UPDATED) {
count++;
}
}
return count;
}
@Override
public List<ITableRow> getUpdatedRows() {
List<ITableRow> rowList = new ArrayList<>();
for (ITableRow row : getRows()) {
if (row.getStatus() == ITableRow.STATUS_UPDATED) {
rowList.add(row);
}
}
return rowList;
}
/**
* Convenience to add row by data only
*/
@Override
public ITableRow addRowByArray(Object dataArray) {
if (dataArray == null) {
return null;
}
List<ITableRow> result = addRowsByMatrix(new Object[]{dataArray});
return CollectionUtility.firstElement(result);
}
@Override
public List<ITableRow> addRowsByMatrix(Object dataMatrix) {
return addRowsByMatrix(dataMatrix, ITableRow.STATUS_INSERTED);
}
@Override
public List<ITableRow> addRowsByMatrix(Object dataMatrix, int rowStatus) {
return addRows(createRowsByMatrix(dataMatrix, rowStatus));
}
@Override
public List<ITableRow> addRowsByArray(Object dataArray) {
return addRowsByArray(dataArray, ITableRow.STATUS_INSERTED);
}
@Override
public List<ITableRow> addRowsByArray(Object dataArray, int rowStatus) {
return addRows(createRowsByArray(dataArray, rowStatus));
}
@Override
public ITableRow addRow() {
return addRow(true);
}
@Override
public ITableRow addRow(boolean markAsInserted) {
return addRow(createRow(), markAsInserted);
}
@Override
public ITableRow addRow(ITableRow newRow) {
return addRow(newRow, false);
}
@Override
public ITableRow addRow(ITableRow newRow, boolean markAsInserted) {
List<ITableRow> addedRows = addRows(CollectionUtility.arrayList(newRow), markAsInserted);
return CollectionUtility.firstElement(addedRows);
}
@Override
public List<ITableRow> addRows(List<? extends ITableRow> newRows) {
return addRows(newRows, false);
}
@Override
public List<ITableRow> addRows(List<? extends ITableRow> newRows, boolean markAsInserted) {
return addRows(newRows, markAsInserted, null);
}
@Override
public List<ITableRow> addRows(List<? extends ITableRow> newRows, boolean markAsInserted, int[] insertIndexes) {
if (newRows == null) {
return CollectionUtility.emptyArrayList();
}
List<InternalTableRow> newIRows = null;
try {
setTableChanging(true);
//
int oldRowCount = m_rows.size();
initCells(newRows);
if (markAsInserted) {
updateStatus(newRows, ITableRow.STATUS_INSERTED);
}
newIRows = createInternalRows(newRows);
addCellObserver(newIRows);
// Fire ROWS_INSERTED event before really adding the internal rows to the table, because adding might trigger ROWS_UPDATED events (due to validation)
fireRowsInserted(newIRows);
for (int i = 0; i < newIRows.size(); i++) {
ITableRow newIRow = newIRows.get(i);
addInternalRow((InternalTableRow) newIRow);
// copy check status of rows after adding them to the table since InternalTableRow maintains this on the table, not on the row
checkRow(newIRow, newRows.get(i).isChecked());
}
if (getColumnSet().getSortColumnCount() > 0) {
// restore order of rows according to sort criteria
setSortValid(false);
}
else if (insertIndexes != null) {
ITableRow[] sortArray = createSortArray(newIRows, insertIndexes, oldRowCount);
sortInternal(Arrays.asList(sortArray));
}
}
finally {
setTableChanging(false);
}
return new ArrayList<>(newIRows);
}
private ITableRow[] createSortArray(List<InternalTableRow> newIRows, int[] insertIndexes, int oldRowCount) {
ITableRow[] sortArray = new ITableRow[m_rows.size()];
// add new rows that have a given sortIndex
for (int i = 0; i < insertIndexes.length; i++) {
sortArray[insertIndexes[i]] = newIRows.get(i);
}
int sortArrayIndex = 0;
// add existing rows
for (int i = 0; i < oldRowCount; i++) {
// find next empty slot
while (sortArray[sortArrayIndex] != null) {
sortArrayIndex++;
}
sortArray[sortArrayIndex] = m_rows.get(i);
}
// add new rows that have no given sortIndex
for (int i = insertIndexes.length; i < newIRows.size(); i++) {
// find next empty slot
while (sortArray[sortArrayIndex] != null) {
sortArrayIndex++;
}
sortArray[sortArrayIndex] = newIRows.get(i);
}
return sortArray;
}
/**
* Add InternalTableRow as an observer to the cell in order to update the row status on changes
*/
private void addCellObserver(List<InternalTableRow> rows) {
for (InternalTableRow row : rows) {
for (int i = 0; i < row.getCellCount(); i++) {
Cell cell = row.getCellForUpdate(i);
cell.setObserver(row);
}
}
}
/**
* initialize cells with column default values
*/
private void initCells(List<? extends ITableRow> rows) {
for (int i = 0; i < getColumnCount(); i++) {
for (ITableRow row : rows) {
IColumn<?> col = getColumnSet().getColumn(i);
col.initCell(row);
}
}
}
private void updateStatus(List<? extends ITableRow> rows, int status) {
for (ITableRow newRow : rows) {
newRow.setStatus(status);
}
}
private List<InternalTableRow> createInternalRows(List<? extends ITableRow> newRows) {
// make sure rows InternalTableRows are in the same order as the given ITableRows, addRows(...) relies on this
List<InternalTableRow> newIRows = new ArrayList<>(newRows.size());
for (ITableRow newRow : newRows) {
newIRows.add(new InternalTableRow(this, newRow));
}
return newIRows;
}
private ITableRow addInternalRow(InternalTableRow newIRow) {
synchronized (m_cachedRowsLock) {
m_cachedRows = null;
int newIndex = m_rows.size();
newIRow.setRowIndex(newIndex);
newIRow.setTableInternal(this);
m_rows.add(newIRow);
m_rowsByKey.put(new CompositeObject(newIRow.getKeyValues()), newIRow);
}
rebuildTreeStructure();
Set<Integer> indexes = new HashSet<>();
for (int idx : getColumnSet().getAllColumnIndexes()) {
indexes.add(idx);
}
enqueueValueChangeTasks(newIRow, indexes);
enqueueDecorationTasks(newIRow);
return newIRow;
}
private void rebuildTreeStructure() {
if (isTableChanging()) {
m_treeStructureDirty = true;
}
else {
rebuildTreeStructureInternal();
}
}
private void rebuildTreeStructureInternal() {
List<ITableRow> rootNodes = new ArrayList<>();
Map<ITableRow/*parent*/, List<ITableRow> /*child rows*/> parentToChildren = new HashMap<>();
m_rows.forEach(row -> {
List<Object> parentRowKeys = getParentRowKeys(row);
if (parentRowKeys.stream().filter(k -> k != null).findAny().orElse(null) != null) {
ITableRow parentRow = getRowByKey(parentRowKeys);
if (parentRow == null) {
throw new IllegalArgumentException("Could not find the parent row of '" + row + "'. parent keys are defined.");
}
parentToChildren.computeIfAbsent(parentRow, children -> new ArrayList<>())
.add(row);
}
else {
row.setParentRowInternal(null);
rootNodes.add(row);
}
});
m_rootRows = Collections.synchronizedList(rootNodes);
boolean hierarchical = parentToChildren.size() > 0;
setHierarchicalInternal(hierarchical);
if (hierarchical) {
CollectingVisitor<ITableRow> collector = new CollectingVisitor<ITableRow>();
rootNodes.forEach(root -> TreeTraversals.create(collector, node -> {
List<ITableRow> childRows = parentToChildren.getOrDefault(node, Collections.emptyList());
node.setChildRowsInternal(childRows);
childRows.forEach(childRow -> childRow.setParentRowInternal(node));
return childRows;
}).traverse(root));
m_rows = Collections.synchronizedList(collector.getCollection());
}
m_treeStructureDirty = false;
}
@Override
public boolean isHierarchical() {
return propertySupport.getPropertyBool(PROP_HIERARCHICAL_ROWS);
}
protected void setHierarchicalInternal(boolean hierarchical) {
propertySupport.setPropertyBool(PROP_HIERARCHICAL_ROWS, hierarchical);
}
@Override
public void moveRow(int sourceIndex, int targetIndex) {
moveRowImpl(sourceIndex, targetIndex);
}
/**
* move the movingRow to the location just before the target row
*/
@Override
public void moveRowBefore(ITableRow movingRow, ITableRow targetRow) {
movingRow = resolveRow(movingRow);
targetRow = resolveRow(targetRow);
if (movingRow != null && targetRow != null) {
int sourceIndex = movingRow.getRowIndex();
int targetIndex = targetRow.getRowIndex();
if (sourceIndex < targetIndex) {
moveRowImpl(sourceIndex, targetIndex - 1);
}
else {
moveRowImpl(sourceIndex, targetIndex);
}
}
}
/**
* move the movingRow to the location just after the target row
*/
@Override
public void moveRowAfter(ITableRow movingRow, ITableRow targetRow) {
movingRow = resolveRow(movingRow);
targetRow = resolveRow(targetRow);
if (movingRow != null && targetRow != null) {
int sourceIndex = movingRow.getRowIndex();
int targetIndex = targetRow.getRowIndex();
if (sourceIndex > targetIndex) {
moveRowImpl(sourceIndex, targetIndex + 1);
}
else {
moveRowImpl(sourceIndex, targetIndex);
}
}
}
/**
* @see {@link List#add(int, Object)}
* @param sourceIndex
* @param targetIndex
*/
private void moveRowImpl(int sourceIndex, int targetIndex) {
if (sourceIndex < 0) {
sourceIndex = 0;
}
if (sourceIndex >= getRowCount()) {
sourceIndex = getRowCount() - 1;
}
if (targetIndex < 0) {
targetIndex = 0;
}
if (targetIndex >= getRowCount()) {
targetIndex = getRowCount() - 1;
}
if (targetIndex != sourceIndex) {
synchronized (m_cachedRowsLock) {
m_cachedRows = null;
ITableRow row = m_rows.remove(sourceIndex);
m_rows.add(targetIndex, row);
}
// update row indexes
int min = Math.min(sourceIndex, targetIndex);
int max = Math.max(sourceIndex, targetIndex);
ITableRow[] changedRows = new ITableRow[max - min + 1];
for (int i = min; i <= max; i++) {
changedRows[i - min] = getRow(i);
((InternalTableRow) changedRows[i - min]).setRowIndex(i);
}
fireRowOrderChanged();
// rebuild selection and checked rows
selectRows(getSelectedRows(), false);
sortCheckedRows();
}
}
@Override
public void deleteRow(int rowIndex) {
deleteRows(new int[]{rowIndex});
}
@Override
public void deleteRows(int[] rowIndexes) {
List<ITableRow> rowList = new ArrayList<>();
for (int rowIndexe : rowIndexes) {
ITableRow row = getRow(rowIndexe);
if (row != null) {
rowList.add(row);
}
}
deleteRows(rowList);
}
@Override
public void deleteRow(ITableRow row) {
if (row != null) {
deleteRows(CollectionUtility.arrayList(row));
}
}
@Override
public void deleteAllRows() {
deleteRows(getRows());
}
@Override
public void deleteRows(Collection<? extends ITableRow> rows) {
List<ITableRow> existingRows = getRows();
//peformance quick-check
if (rows != existingRows) {
rows = resolveRows(rows);
CollectingVisitor<ITableRow> collector = new CollectingVisitor<ITableRow>();
rows.forEach(parent -> TreeTraversals.create(collector, node -> {
return node.getChildRows();
}).traverse(parent));
rows = collector.getCollection();
}
if (!CollectionUtility.hasElements(rows)) {
return;
}
try {
setTableChanging(true);
//
int rowCountBefore = getRowCount();
int min = getRowCount();
int max = 0;
for (ITableRow row : rows) {
min = Math.min(min, row.getRowIndex());
max = Math.max(max, row.getRowIndex());
}
List<ITableRow> deletedRows = new ArrayList<>(rows);
// remove from selection
deselectRows(deletedRows);
uncheckRows(deletedRows);
//delete impl
//peformance quick-check
if (rows == existingRows) {
//remove all of them
synchronized (m_cachedRowsLock) {
m_rows.clear();
m_rootRows.clear();
m_rowsByKey.clear();
m_cachedRows = null;
}
for (int i = deletedRows.size() - 1; i >= 0; i--) {
ITableRow candidateRow = deletedRows.get(i);
if (candidateRow != null) {
deleteRowImpl(candidateRow);
}
}
}
else {
for (int i = deletedRows.size() - 1; i >= 0; i--) {
ITableRow candidateRow = deletedRows.get(i);
if (candidateRow != null) {
// delete regardless if index is right
boolean removed = false;
synchronized (m_cachedRowsLock) {
removed = m_rows.remove(candidateRow);
m_rowsByKey.remove(new CompositeObject(candidateRow.getKeyValues()));
if (removed) {
m_cachedRows = null;
}
}
if (removed) {
deleteRowImpl(candidateRow);
rebuildTreeStructure();
}
}
}
}
// update index of rows at the bottom of deleted rows
int minAffectedIndex = Math.max(min - 1, 0);
ITableRow[] affectedRows = new ITableRow[getRowCount() - minAffectedIndex];
for (int i = minAffectedIndex; i < getRowCount(); i++) {
affectedRows[i - minAffectedIndex] = getRow(i);
((InternalTableRow) affectedRows[i - minAffectedIndex]).setRowIndex(i);
}
if (rowCountBefore == deletedRows.size()) {
removeUserRowFilters(false);
fireAllRowsDeleted(deletedRows);
}
else {
fireRowsDeleted(deletedRows);
}
}
finally {
setTableChanging(false);
}
}
private void deleteRowImpl(ITableRow row) {
if (!(row instanceof InternalTableRow)) {
return;
}
InternalTableRow internalRow = (InternalTableRow) row;
if (isAutoDiscardOnDelete()) {
internalRow.setTableInternal(null);
// don't manage deleted rows any further
}
else if (internalRow.getStatus() == ITableRow.STATUS_INSERTED) {
internalRow.setTableInternal(null);
// it was new and now it is gone, no further action required
}
else {
internalRow.setStatus(ITableRow.STATUS_DELETED);
m_deletedRows.put(new CompositeObject(getRowKeys(internalRow)), internalRow);
}
}
@Override
public void discardRow(int rowIndex) {
discardRows(new int[]{rowIndex});
}
@Override
public void discardRows(int[] rowIndexes) {
List<ITableRow> rowList = new ArrayList<>();
for (int rIndex : rowIndexes) {
ITableRow row = getRow(rIndex);
if (row != null) {
rowList.add(row);
}
}
discardRows(rowList);
}
@Override
public void discardRow(ITableRow row) {
if (row != null) {
discardRows(CollectionUtility.arrayList(row));
}
}
@Override
public void discardAllRows() {
discardRows(getRows());
}
/**
* discard is the same as delete with the exception that discarded rows are not collected in the deletedRows list
*/
@Override
public void discardRows(Collection<? extends ITableRow> rows) {
try {
setTableChanging(true);
//
for (ITableRow row : rows) {
row.setStatus(ITableRow.STATUS_INSERTED);
}
deleteRows(rows);
}
finally {
setTableChanging(false);
}
}
@Override
public void discardAllDeletedRows() {
for (ITableRow iTableRow : m_deletedRows.values()) {
((InternalTableRow) iTableRow).setTableInternal(null);
}
m_deletedRows.clear();
}
@Override
public void discardDeletedRow(ITableRow deletedRow) {
if (deletedRow != null) {
discardDeletedRows(CollectionUtility.arrayList(deletedRow));
}
}
@Override
public void discardDeletedRows(Collection<? extends ITableRow> deletedRows) {
if (deletedRows != null) {
for (ITableRow row : deletedRows) {
m_deletedRows.remove(new CompositeObject(getRowKeys(row)));
((InternalTableRow) row).setTableInternal(null);
}
}
}
@Override
public void setContextColumn(IColumn<?> col) {
propertySupport.setProperty(PROP_CONTEXT_COLUMN, col);
}
@Override
public IColumn<?> getContextColumn() {
return (IColumn<?>) propertySupport.getProperty(PROP_CONTEXT_COLUMN);
}
@Override
public List<Object> getRowKeys(int rowIndex) {
ITableRow row = getRow(rowIndex);
return getRowKeys(row);
}
@Override
public List<Object> getRowKeys(ITableRow row) {
if (row != null) {
return row.getKeyValues();
}
return CollectionUtility.emptyArrayList();
}
@SuppressWarnings("deprecation")
@Override
public ITableRow findRowByKey(List<?> keys) {
return getRowByKey(keys);
}
@Override
public ITableRow getRowByKey(List<?> keys) {
if (!CollectionUtility.hasElements(keys)) {
return null;
}
return m_rowsByKey.get(new CompositeObject(keys));
}
@Override
public List<Object> getParentRowKeys(int rowIndex) {
ITableRow row = getRow(rowIndex);
return getParentRowKeys(row);
}
@Override
public List<Object> getParentRowKeys(ITableRow row) {
if (row != null) {
return row.getParentKeyValues();
}
return CollectionUtility.emptyArrayList();
}
@Override
public ITableRow findParentRow(ITableRow row) {
return getRowByKey(getParentRowKeys(row));
}
/**
* Gets if the given cell values are equal to the given search values
*
* @param searchValues
* The values to search in the given cells. Must not be <code>null</code>.
* @param keyColumns
* The columns describing the cells to be searched. Must not be <code>null</code>.
* @param row
* The row holding the cells to be searched. Must not be <code>null</code>.
* @return <code>true</code> if the cells described by the given columns and row have the same content as the given
* searchValues. <code>false</code> otherwise. If the number of columns is different than the number of search
* values only the columns are searched for which a search value exists (
* <code>min(searchValues.size(), keyColumns.size()</code>).
*/
protected boolean areCellsEqual(List<?> searchValues, List<IColumn<?>> keyColumns, ITableRow row) {
int keyIndex = 0;
int numKeyColumns = keyColumns.size();
for (Object key : searchValues) {
if (keyIndex >= numKeyColumns) {
break;
}
Object cellValue = keyColumns.get(keyIndex).getValue(row);
if (ObjectUtility.notEquals(key, cellValue)) {
return false;
}
keyIndex++;
}
return true;
}
@Override
public TableUserFilterManager getUserFilterManager() {
return (TableUserFilterManager) propertySupport.getProperty(PROP_USER_FILTER_MANAGER);
}
@Override
public void setUserFilterManager(TableUserFilterManager m) {
propertySupport.setProperty(PROP_USER_FILTER_MANAGER, m);
}
@Override
public ITableCustomizer getTableCustomizer() {
return (ITableCustomizer) propertySupport.getProperty(PROP_TABLE_CUSTOMIZER);
}
@Override
public void setTableCustomizer(ITableCustomizer c) {
propertySupport.setProperty(PROP_TABLE_CUSTOMIZER, c);
}
@Override
public ITypeWithClassId getContainer() {
return (ITypeWithClassId) propertySupport.getProperty(PROP_CONTAINER);
}
/**
* do not use this internal method unless you are implementing a container that holds and controls an {@link ITable}
*/
public void setContainerInternal(ITypeWithClassId container) {
propertySupport.setProperty(PROP_CONTAINER, container);
}
@Override
public boolean isSortEnabled() {
return propertySupport.getPropertyBool(PROP_SORT_ENABLED);
}
@Override
public void setSortEnabled(boolean b) {
propertySupport.setPropertyBool(PROP_SORT_ENABLED, b);
}
@Override
public boolean isUiSortPossible() {
return propertySupport.getPropertyBool(PROP_UI_SORT_POSSIBLE);
}
@Override
public void setUiSortPossible(boolean b) {
propertySupport.setPropertyBool(PROP_UI_SORT_POSSIBLE, b);
}
public void onGroupedColumnInvisible(IColumn<?> col) {
if (isTableChanging()) {
setSortValid(false);
}
else {
sort();
}
}
@Override
public void sort() {
try {
if (isSortEnabled()) {
// Consider any active sort-column, not only explicit ones.
// This is to support reverse (implicit) sorting of columns, meaning that multiple column sort is done
// without CTRL-key held. In contrast to explicit multiple column sort, the first clicked column
// is the least significant sort column.
if (!getRows().isEmpty()) {//NOSONAR
Comparator<ITableRow> comparator = null;
LinkedHashSet<IColumn<?>> sortCols = new LinkedHashSet<>(getColumnSet().getSortColumns());
if (!sortCols.isEmpty()) {
// add all visible columns (not already added, thus LinkedHashSet)
// as fallback sorting to guarantee same sorting as in JS.
sortCols.addAll(getColumnSet().getVisibleColumns());
comparator = new TableRowComparator(sortCols);
}
// first make sure decorations and lookups are up-to-date
processDecorationBuffer();
sortInternal(sortRows(getRows(), comparator));
}
}
}
finally {
setSortValid(true);
}
}
protected List<ITableRow> sortRows(List<? extends ITableRow> rows, Comparator<ITableRow> comparator) {
List<ITableRow> rootNodes = new ArrayList<>();
Map<ITableRow/*parent*/, List<ITableRow> /*child rows*/> parentToChildren = new HashMap<>();
rows.forEach(row -> {
ITableRow parentRow = findParentRow(row);
if (parentRow == null) {
rootNodes.add(row);
}
else {
parentToChildren.computeIfAbsent(parentRow, children -> new ArrayList<>())
.add(row);
}
});
CollectingVisitor<ITableRow> collector = new CollectingVisitor<ITableRow>();
if (comparator != null) {
rootNodes.sort(comparator);
}
rootNodes.forEach(root -> TreeTraversals.create(collector, node -> {
List<ITableRow> childRows = parentToChildren.get(node);
if (comparator != null && CollectionUtility.hasElements(childRows)) {
childRows.sort(comparator);
}
return childRows;
}).traverse(root));
return collector.getCollection();
}
@Override
public void sort(List<? extends ITableRow> rowsInNewOrder) {
List<ITableRow> resolvedRows = resolveRows(rowsInNewOrder);
if (resolvedRows.size() == rowsInNewOrder.size()) {
sortInternal(resolvedRows);
}
else {
// check which rows could not be mapped
List<ITableRow> list = new ArrayList<>(m_rows);
list.removeAll(resolvedRows);
List<ITableRow> sortedList = new ArrayList<>();
sortedList.addAll(resolvedRows);
sortedList.addAll(list);
sortInternal(sortedList);
}
}
private void sortInternal(List<? extends ITableRow> resolvedRows) {
int i = 0;
for (ITableRow row : resolvedRows) {
((InternalTableRow) row).setRowIndex(i);
i++;
}
synchronized (m_cachedRowsLock) {
m_cachedRows = null;
m_rows.clear();
m_rows.addAll(resolvedRows);
}
//sort selection and checked rows without firing an event
if (m_selectedRows != null && !m_selectedRows.isEmpty()) {
Set<ITableRow> newSelection = new TreeSet<>(new RowIndexComparator());
newSelection.addAll(m_selectedRows);
m_selectedRows = new ArrayList<>(newSelection);
}
sortCheckedRows();
fireRowOrderChanged();
}
private void sortCheckedRows() {
if (m_checkedRows == null || m_checkedRows.isEmpty()) {
return;
}
TreeSet<ITableRow> newCheckedRows = new TreeSet<>(new RowIndexComparator());
newCheckedRows.addAll(m_checkedRows);
m_checkedRows = new LinkedHashSet<>(newCheckedRows);
}
@Override
public void resetColumnConfiguration() {
discardAllRows();
//
try {
setTableChanging(true);
// save displayable state
HashMap<String, Boolean> displayableState = new HashMap<>();
for (IColumn<?> col : getColumns()) {
displayableState.put(col.getColumnId(), col.isDisplayable());
}
// reset columns
disposeColumnsInternal();
m_objectExtensions.runInExtensionContext(() -> {
// enforce recreation of the contributed columns so column indexes etc. will be reset
m_contributionHolder.resetContributionsByClass(AbstractTable.this, IColumn.class);
// runs within extension context, so that extensions and contributions can be created
createColumnsInternal();
});
initColumnsInternal();
// re-apply displayable
for (IColumn<?> col : getColumns()) {
if (displayableState.get(col.getColumnId()) != null) {
col.setDisplayable(displayableState.get(col.getColumnId()));
}
}
// re link existing filters to new columns
linkColumnFilters();
// reset context column (disposed column must not be used anymore)
setContextColumn(null);
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_COLUMN_STRUCTURE_CHANGED));
}
finally {
setTableChanging(false);
}
}
private void linkColumnFilters() {
TableUserFilterManager filterManager = getUserFilterManager();
if (filterManager == null) {
return;
}
for (IColumn<?> col : getColumns()) {
getUserFilterManager().getFilters().stream()
.filter(IColumnAwareUserFilterState.class::isInstance)
.map(IColumnAwareUserFilterState.class::cast)
.forEach(filter -> filter.replaceColumn(col));
}
}
@Override
public void resetColumnVisibilities() {
resetColumns(CollectionUtility.hashSet(IResetColumnsOption.VISIBILITY));
}
@Override
public void resetColumnOrder() {
resetColumns(CollectionUtility.hashSet(IResetColumnsOption.ORDER));
}
@Override
public void resetColumnSortOrder() {
resetColumns(CollectionUtility.hashSet(IResetColumnsOption.SORTING));
}
@Override
public void resetColumnWidths() {
resetColumns(CollectionUtility.hashSet(IResetColumnsOption.WIDTHS));
}
@Override
public void resetColumnBackgroundEffects() {
resetColumns(CollectionUtility.hashSet(IResetColumnsOption.BACKGROUND_EFFECTS));
}
@Override
public void resetColumnFilters() {
resetColumns(CollectionUtility.hashSet(IResetColumnsOption.FILTERS));
}
@Override
public void resetColumns() {
resetColumns(CollectionUtility.hashSet(
IResetColumnsOption.VISIBILITY,
IResetColumnsOption.ORDER,
IResetColumnsOption.SORTING,
IResetColumnsOption.WIDTHS,
IResetColumnsOption.BACKGROUND_EFFECTS,
IResetColumnsOption.FILTERS));
}
protected void resetColumns(Set<String> options) {
try {
setTableChanging(true);
resetColumnsInternal(options);
interceptResetColumns(options);
}
finally {
setTableChanging(false);
}
}
private void resetColumnsInternal(Set<String> options) {
if (options.contains(IResetColumnsOption.SORTING)) {
setSortValid(false);
}
if (options.contains(IResetColumnsOption.ORDER)) {
SortedMap<CompositeObject, IColumn<?>> orderMap = new TreeMap<>();
int index = 0;
for (IColumn<?> col : getColumns()) {
if (col.isDisplayable()) {
orderMap.put(new CompositeObject(col.getOrder(), index), col);
index++;
}
}
getColumnSet().setVisibleColumns(orderMap.values());
}
if (options.contains(IResetColumnsOption.VISIBILITY)) {
List<IColumn<?>> list = new ArrayList<>();
for (IColumn<?> col : getColumnSet().getAllColumnsInUserOrder()) {
if (col.isDisplayable()) {
boolean configuredVisible = ((IColumn<?>) col).isInitialVisible();
if (configuredVisible) {
list.add(col);
}
}
}
getColumnSet().setVisibleColumns(list);
}
if (options.contains(IResetColumnsOption.SORTING)) {
getColumnSet().resetSortingAndGrouping();
}
if (options.contains(IResetColumnsOption.WIDTHS)) {
for (IColumn<?> col : getColumns()) {
if (col.isDisplayable()) {
col.setWidth(col.getInitialWidth());
}
}
}
if (options.contains(IResetColumnsOption.BACKGROUND_EFFECTS)) {
for (IColumn<?> col : getColumns()) {
if (col instanceof INumberColumn) {
((INumberColumn) col).setBackgroundEffect(((INumberColumn) col).getInitialBackgroundEffect());
}
}
}
if (options.contains(IResetColumnsOption.FILTERS)) {
removeUserRowFilters();
}
}
/**
* Affects columns with lookup calls or code types<br>
* cells that have changed values fetch new texts/decorations from the lookup service in one single batch call lookup
* (performance optimization)
*/
private void processDecorationBuffer() {
/*
* update row decorations
*/
Map<Integer, Set<ITableRow>> changes = m_rowValueChangeBuffer;
m_rowValueChangeBuffer = new HashMap<>();
applyRowValueChanges(changes);
Set<ITableRow> set = m_rowDecorationBuffer;
m_rowDecorationBuffer = new HashSet<>();
applyRowDecorations(set);
/*
* check row filters
*/
if (!m_rowFilters.isEmpty()) {
boolean filterChanged = false;
for (ITableRow row : set) {
if (row.getTable() == AbstractTable.this && row instanceof InternalTableRow) {
InternalTableRow irow = (InternalTableRow) row;
boolean oldFlag = irow.isFilterAccepted();
applyRowFiltersInternal(irow);
boolean newFlag = irow.isFilterAccepted();
filterChanged = filterChanged || (oldFlag != newFlag);
}
}
if (filterChanged) {
fireRowFilterChanged();
}
}
}
private void applyRowValueChanges(Map<Integer, Set<ITableRow>> changes) {
try {
for (ITableRow tableRow : getRows()) {
tableRow.setRowChanging(true);
}
Set<Entry<Integer, Set<ITableRow>>> entrySet = changes.entrySet();
for (Entry<Integer, Set<ITableRow>> e : entrySet) {
IColumn<?> col = getColumnSet().getColumn(e.getKey());
col.updateDisplayTexts(CollectionUtility.arrayList(e.getValue()));
}
}
finally {
for (ITableRow tableRow : getRows()) {
tableRow.setRowPropertiesChanged(false);
tableRow.setRowChanging(false);
}
}
}
@SuppressWarnings("unchecked")
private void applyRowDecorations(Set<ITableRow> rows) {
try {
for (ITableRow tableRow : rows) {
tableRow.setRowChanging(true);
this.decorateRow(tableRow);
}
for (IColumn col : getColumns()) {
col.decorateCells(CollectionUtility.arrayList(rows));
// cell decorator on table
for (ITableRow row : rows) {
this.decorateCell(row, col);
}
}
}
catch (Exception ex) {
LOG.error("Error occured while applying row decoration", ex);
}
finally {
for (ITableRow tableRow : rows) {
tableRow.setRowPropertiesChanged(false);
tableRow.setRowChanging(false);
}
}
}
/**
* Fires events in form in of one batch <br>
* Unnecessary events are removed or merged.
*/
private void processEventBuffer() {
//loop detection
try {
m_eventBufferLoopDetection++;
if (m_eventBufferLoopDetection > 100) {
LOG.error("LOOP DETECTION in {}. see stack trace for more details.", getClass(), new Exception("LOOP DETECTION"));
return;
}
//
if (!getEventBuffer().isEmpty()) {
List<TableEvent> list = getEventBuffer().consumeAndCoalesceEvents();
// fire the batch and set tree to changing, otherwise a listener might trigger another events that
// then are processed before all other listeners received that batch
try {
setTableChanging(true);
//
m_listeners.fireEvents(list);
}
finally {
setTableChanging(false);
}
}
}
finally {
m_eventBufferLoopDetection--;
}
}
/**
* do decoration and filtering later
*/
private void enqueueDecorationTasks(ITableRow row) {
if (row != null) {
m_rowDecorationBuffer.add(row);
}
}
private void enqueueValueChangeTasks(ITableRow row, Set<Integer> valueChangedColumns) {
for (Integer colIndex : valueChangedColumns) {
Set<ITableRow> rows = m_rowValueChangeBuffer.get(colIndex);
if (rows == null) {
rows = new HashSet<>();
}
rows.add(row);
m_rowValueChangeBuffer.put(colIndex, rows);
}
}
@Override
public ITableRow resolveRow(ITableRow row) {
if (row == null) {
return null;
}
if (!(row instanceof InternalTableRow)) {
throw new IllegalArgumentException("only accept InternalTableRow, not " + row.getClass());
}
// check owner
if (row.getTable() == this) {
return row;
}
else {
return null;
}
}
@Override
public List<ITableRow> resolveRows(Collection<? extends ITableRow> rows) {
if (rows == null) {
rows = CollectionUtility.emptyArrayList();
}
List<ITableRow> resolvedRows = new ArrayList<>(rows.size());
for (ITableRow row : rows) {
if (resolveRow(row) == row) {
resolvedRows.add(row);
}
else {
LOG.warn("could not resolve row {}", row);
}
}
return resolvedRows;
}
@Override
public boolean isHeaderVisible() {
return propertySupport.getPropertyBool(PROP_HEADER_VISIBLE);
}
@Override
public void setHeaderVisible(boolean b) {
propertySupport.setPropertyBool(PROP_HEADER_VISIBLE, b);
}
@Override
public boolean isHeaderEnabled() {
return propertySupport.getPropertyBool(PROP_HEADER_ENABLED);
}
@Override
public void setHeaderEnabled(boolean headerEnabled) {
propertySupport.setPropertyBool(PROP_HEADER_ENABLED, headerEnabled);
}
@Override
public final void decorateCell(ITableRow row, IColumn<?> col) {
Cell cell = row.getCellForUpdate(col.getColumnIndex());
decorateCellInternal(cell, row, col);
try {
interceptDecorateCell(cell, row, col);
}
catch (Exception e) {
BEANS.get(ExceptionHandler.class).handle(e);
}
}
protected void decorateCellInternal(Cell view, ITableRow row, IColumn<?> col) {
}
@Override
public final void decorateRow(ITableRow row) {
decorateRowInternal(row);
try {
interceptDecorateRow(row);
}
catch (Exception e) {
BEANS.get(ExceptionHandler.class).handle(e);
}
}
protected void decorateRowInternal(ITableRow row) {
// icon
if (row.getIconId() == null) {
String s = getDefaultIconId();
if (s != null) {
row.setIconId(s);
}
}
}
/**
* Called when the columns are reset.
* <p>
* Subclasses can override this method. The default does nothing.
*
* @param options
* Set of constants of {@link IResetColumnsOption}
*/
@ConfigOperation
@Order(90)
protected void execResetColumns(Set<String> options) {
}
@Override
public TableListeners tableListeners() {
return m_listeners;
}
protected IEventHistory<TableEvent> createEventHistory() {
return new DefaultTableEventHistory(5000L);
}
@Override
public IEventHistory<TableEvent> getEventHistory() {
return m_eventHistory;
}
private void fireRowsInserted(List<? extends ITableRow> rows) {
synchronized (m_cachedFilteredRowsLock) {
m_cachedFilteredRows = null;
}
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_ROWS_INSERTED, rows));
}
private void fireRowsUpdated(List<? extends ITableRow> rows) {
synchronized (m_cachedFilteredRowsLock) {
m_cachedFilteredRows = null;
}
LOG.debug("fire rows updated {}", rows);
TableEvent e = new TableEvent(this, TableEvent.TYPE_ROWS_UPDATED, rows);
// For each row, add information about updated columns to the event. (A row may also be updated if
// not specific column was changed, e.g. when a row's enabled state changes.)
for (ITableRow row : rows) {
// Convert column indexes to IColumns
Set<Integer> columnIndexes = row.getUpdatedColumnIndexes();
if (!columnIndexes.isEmpty()) {
Set<IColumn<?>> columns = new HashSet<>();
for (Integer columnIndex : columnIndexes) {
IColumn<?> column = getColumns().get(columnIndex);
if (column != null) {
columns.add(column);
}
}
// Put updated columns into event
e.setUpdatedColumns(row, columns);
}
}
fireTableEventInternal(e);
}
/**
* Request to reload/replace table data with refreshed data
*/
private void fireRowsDeleted(List<? extends ITableRow> rows) {
synchronized (m_cachedFilteredRowsLock) {
m_cachedFilteredRows = null;
}
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_ROWS_DELETED, rows));
}
private void fireAllRowsDeleted(List<? extends ITableRow> rows) {
synchronized (m_cachedFilteredRowsLock) {
m_cachedFilteredRows = null;
}
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_ALL_ROWS_DELETED, rows));
}
private void fireRowsSelected(List<? extends ITableRow> rows) {
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_ROWS_SELECTED, rows));
}
private void fireRowsChecked(List<? extends ITableRow> rows) {
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_ROWS_CHECKED, rows));
}
private void fireRowsExpanded(List<? extends ITableRow> rows) {
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_ROWS_EXPANDED, rows));
}
private void fireRowClick(ITableRow row, MouseButton mouseButton) {
if (row != null) {
try {
interceptRowClickSingleObserver(row, mouseButton);
interceptRowClick(row, mouseButton);
}
catch (Exception ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
}
}
protected void interceptRowClickSingleObserver(ITableRow row, MouseButton mouseButton) {
// Only toggle checked state if the table and row are enabled.
if (!row.isEnabled() || !isEnabled()) {
return;
}
// Only toggle checked state if being fired by the left mousebutton (https://bugs.eclipse.org/bugs/show_bug.cgi?id=453543).
if (mouseButton != MouseButton.Left) {
return;
}
IColumn<?> ctxCol = getContextColumn();
if (isCellEditable(row, ctxCol)) {
//cell-level checkbox
if (ctxCol instanceof IBooleanColumn) {
//editable boolean columns consume this click
IFormField field = ctxCol.prepareEdit(row);
if (field instanceof IBooleanField) {
IBooleanField bfield = (IBooleanField) field;
bfield.toggleValue();
ctxCol.completeEdit(row, field);
}
}
else {
//other editable columns have no effect HERE, the ui will open an editor
}
}
}
private void fireRowAction(ITableRow row) {
if (isActionRunning() || row == null) {
return;
}
try {
setActionRunning(true);
interceptRowAction(row);
}
catch (Exception ex) {
BEANS.get(ExceptionHandler.class).handle(ex);
}
finally {
setActionRunning(false);
}
}
private void fireRowOrderChanged() {
synchronized (m_cachedFilteredRowsLock) {
m_cachedFilteredRows = null;
}
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_ROW_ORDER_CHANGED, getRows()));
}
private void fireRequestFocus() {
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_REQUEST_FOCUS));
}
private void fireRequestFocusInCell(IColumn<?> column, ITableRow row) {
TableEvent e = new TableEvent(this, TableEvent.TYPE_REQUEST_FOCUS_IN_CELL);
e.setColumns(CollectionUtility.hashSet(column));
e.setRows(CollectionUtility.arrayList(row));
fireTableEventInternal(e);
}
private void fireRowFilterChanged() {
synchronized (m_cachedFilteredRowsLock) {
m_cachedFilteredRows = null;
}
fireTableEventInternal(new TableEvent(this, TableEvent.TYPE_ROW_FILTER_CHANGED));
}
private TransferObject fireRowsDragRequest() {
List<ITableRow> rows = getSelectedRows();
if (CollectionUtility.hasElements(rows)) {
TableEvent e = new TableEvent(this, TableEvent.TYPE_ROWS_DRAG_REQUEST, rows);
fireTableEventInternal(e);
return e.getDragObject();
}
else {
return null;
}
}
private void fireRowDropAction(ITableRow row, TransferObject dropData) {
List<ITableRow> rows = null;
if (row != null) {
rows = CollectionUtility.arrayList(row);
}
TableEvent e = new TableEvent(this, TableEvent.TYPE_ROW_DROP_ACTION, rows);
e.setDropObject(dropData);
fireTableEventInternal(e);
}
private TransferObject fireRowsCopyRequest() {
List<ITableRow> rows = getSelectedRows();
if (CollectionUtility.hasElements(rows)) {
TableEvent e = new TableEvent(this, TableEvent.TYPE_ROWS_COPY_REQUEST, rows);
fireTableEventInternal(e);
return e.getCopyObject();
}
else {
return null;
}
}
// main handler
public void fireTableEventInternal(TableEvent e) {
if (isTableChanging()) {
// buffer the event for later batch firing
getEventBuffer().add(e);
}
else {
doFireTableEvent(e);
}
}
protected void doFireTableEvent(TableEvent e) {
m_listeners.fireEvent(e);
}
@Override
public ITableUIFacade getUIFacade() {
return m_uiFacade;
}
@Override
public void setReloadHandler(IReloadHandler reloadHandler) {
m_reloadHandler = reloadHandler;
}
@Override
public IReloadHandler getReloadHandler() {
return m_reloadHandler;
}
@Override
public List<ITableControl> getTableControls() {
return CollectionUtility.arrayList(m_tableControls);
}
@Override
public void addTableControl(ITableControl control) {
m_tableControls.add(control);
addTableControlInternal(control);
}
private void addTableControlInternal(ITableControl control) {
((AbstractTableControl) control).setTable(this);
m_tableControls.sort(new OrderedComparator());
propertySupport.firePropertyChange(PROP_TABLE_CONTROLS, null, getTableControls());
}
@Override
public void removeTableControl(ITableControl control) {
m_tableControls.remove(control);
((AbstractTableControl) control).setTable(null);
propertySupport.firePropertyChange(PROP_TABLE_CONTROLS, null, getTableControls());
}
@Override
public <T extends ITableControl> T getTableControl(Class<T> controlClass) {
for (ITableControl control : m_tableControls) {
if (controlClass.isAssignableFrom(control.getClass())) {
return controlClass.cast(control);
}
}
return null;
}
/**
* Configures the visibility of the table status.
* <p>
* Subclasses can override this method. Default is {@code false}.
*
* @return {@code true} if the table status is visible, {@code false} otherwise.
*/
@ConfigProperty(ConfigProperty.BOOLEAN)
@Order(200)
protected boolean getConfiguredTableStatusVisible() {
return false;
}
@Override
public boolean isTableStatusVisible() {
return propertySupport.getPropertyBool(PROP_TABLE_STATUS_VISIBLE);
}
@Override
public void setTableStatusVisible(boolean visible) {
propertySupport.setPropertyBool(PROP_TABLE_STATUS_VISIBLE, visible);
}
@Override
public IStatus getTableStatus() {
return (IStatus) propertySupport.getProperty(PROP_TABLE_STATUS);
}
@Override
public void setTableStatus(IStatus status) {
propertySupport.setProperty(PROP_TABLE_STATUS, status);
}
/**
* Check if this column would prevent an ui sort for table. If it prevents an ui sort,
* {@link ITable#setUiSortPossible(boolean)} is set to <code>false</code> for all columns of the table.
*/
protected void checkIfColumnPreventsUiSortForTable() {
for (IColumn<?> column : m_columnSet.getColumns()) {
if (!column.isVisible() && column.getSortIndex() != -1) {
setUiSortPossible(false);
return;
}
}
setUiSortPossible(true);
}
/**
* Checks if the context column is visible. If not, the context column is set to <code>null</code>.
*/
protected void checkIfContextColumnIsVisible() {
IColumn<?> contextColumn = getContextColumn();
if (contextColumn != null && !contextColumn.isVisible()) {
setContextColumn(null);
}
}
/*
* UI Notifications
*/
protected class P_TableUIFacade implements ITableUIFacade {
private int m_uiProcessorCount = 0;
protected void pushUIProcessor() {
m_uiProcessorCount++;
}
protected void popUIProcessor() {
m_uiProcessorCount--;
}
@Override
public boolean isUIProcessing() {
return m_uiProcessorCount > 0;
}
@Override
public void fireRowClickFromUI(ITableRow row, MouseButton mouseButton) {
try {
pushUIProcessor();
//
row = resolveRow(row);
if (row != null) {
fireRowClick(resolveRow(row), mouseButton);
}
}
finally {
popUIProcessor();
}
}
@Override
public void fireRowActionFromUI(ITableRow row) {
try {
pushUIProcessor();
//
row = resolveRow(row);
if (row != null) {
fireRowAction(row);
}
}
finally {
popUIProcessor();
}
}
@Override
public void fireVisibleColumnsChangedFromUI(Collection<IColumn<?>> visibleColumns) {
try {
pushUIProcessor();
//
getColumnSet().setVisibleColumns(visibleColumns);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
}
finally {
popUIProcessor();
}
}
@Override
public void fireColumnMovedFromUI(IColumn<?> c, int toViewIndex) {
try {
pushUIProcessor();
//
c = getColumnSet().resolveColumn(c);
if (c != null) {
getColumnSet().moveColumnToVisibleIndex(c.getColumnIndex(), toViewIndex);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
}
}
finally {
popUIProcessor();
}
}
@Override
public void setColumnWidthFromUI(IColumn<?> c, int newWidth) {
try {
pushUIProcessor();
//
c = getColumnSet().resolveColumn(c);
if (c != null) {
c.setWidthInternal(newWidth);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
}
}
finally {
popUIProcessor();
}
}
@Override
public void fireHeaderSortFromUI(IColumn<?> c, boolean multiSort, boolean ascending) {
try {
pushUIProcessor();
//
if (isSortEnabled()) {
c = getColumnSet().resolveColumn(c);
if (c != null) {
getColumnSet().handleSortEvent(c, multiSort, ascending);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
sort();
}
}
}
finally {
popUIProcessor();
}
}
@Override
public void fireHeaderGroupFromUI(IColumn<?> c, boolean multiGroup, boolean ascending) {
try {
pushUIProcessor();
//
if (isSortEnabled()) {
c = getColumnSet().resolveColumn(c);
if (c != null) {
getColumnSet().handleGroupingEvent(c, multiGroup, ascending);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
sort();
}
}
}
finally {
popUIProcessor();
}
}
@Override
public void fireAggregationFunctionChanged(INumberColumn<?> c, String function) {
try {
pushUIProcessor();
getColumnSet().setAggregationFunction(c, function);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
}
finally {
popUIProcessor();
}
}
@Override
public void setColumnBackgroundEffect(INumberColumn<?> column, String effect) {
try {
pushUIProcessor();
column.setBackgroundEffect(effect);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
}
finally {
popUIProcessor();
}
}
@Override
public void setCheckedRowsFromUI(List<? extends ITableRow> rows, boolean checked) {
if (!isEnabled()) {
return;
}
try {
pushUIProcessor();
checkRows(rows, checked, true);
}
finally {
popUIProcessor();
}
}
@Override
public void setExpandedRowsFromUI(List<? extends ITableRow> rows, boolean expanded) {
if (CollectionUtility.isEmpty(rows)) {
return;
}
try {
pushUIProcessor();
expandRowsInternal(rows, expanded);
}
finally {
popUIProcessor();
}
}
@Override
public void setSelectedRowsFromUI(List<? extends ITableRow> rows) {
try {
pushUIProcessor();
selectRows(rows, false);
}
finally {
popUIProcessor();
}
}
@Override
public TransferObject fireRowsDragRequestFromUI() {
try {
pushUIProcessor();
return fireRowsDragRequest();
}
finally {
popUIProcessor();
}
}
@Override
public void fireRowDropActionFromUI(ITableRow row, TransferObject dropData) {
try {
pushUIProcessor();
row = resolveRow(row);
fireRowDropAction(row, dropData);
}
finally {
popUIProcessor();
}
}
@Override
public TransferObject fireRowsCopyRequestFromUI() {
try {
pushUIProcessor();
return fireRowsCopyRequest();
}
finally {
popUIProcessor();
}
}
@Override
public void fireAppLinkActionFromUI(String ref) {
try {
pushUIProcessor();
//
doAppLinkAction(ref);
}
catch (RuntimeException e) {
BEANS.get(ExceptionHandler.class).handle(e);
}
finally {
popUIProcessor();
}
}
@Override
public void setContextColumnFromUI(IColumn<?> col) {
try {
pushUIProcessor();
//
if (col != null && col.getTable() != AbstractTable.this) {
col = null;
}
setContextColumn(col);
}
finally {
popUIProcessor();
}
}
@Override
public IFormField prepareCellEditFromUI(ITableRow row, IColumn<?> col) {
if (!isEnabled()) {
return null;
}
try {
pushUIProcessor();
//
m_editContext = null;
row = resolveRow(row);
if (row != null && col != null) {
// ensure the editable row to be selected.
// This is crucial if the cell's value is changed right away in IColumn#prepareEdit(ITableRow), e.g. in AbstractBooleanColumn
row.getTable().selectRow(row);
IFormField f = col.prepareEdit(row);
if (f != null) {
m_editContext = new P_CellEditorContext(row, col, f);
}
return f;
}
}
finally {
popUIProcessor();
}
return null;
}
@Override
public void completeCellEditFromUI() {
if (!isEnabled()) {
return;
}
try {
pushUIProcessor();
//
if (m_editContext != null) {
try {
m_editContext.getColumn().completeEdit(m_editContext.getRow(), m_editContext.getFormField());
}
finally {
m_editContext = null;
}
}
}
finally {
popUIProcessor();
}
}
@Override
public void cancelCellEditFromUI() {
try {
pushUIProcessor();
//
m_editContext = null;
}
finally {
popUIProcessor();
}
}
@Override
public void fireTableReloadFromUI() {
if (m_reloadHandler != null) {
try {
pushUIProcessor();
//
m_reloadHandler.reload();
}
finally {
popUIProcessor();
}
}
}
@Override
public void fireTableResetFromUI() {
try {
setTableChanging(true);
resetColumns();
TableUserFilterManager m = getUserFilterManager();
if (m != null) {
m.reset();
}
ITableCustomizer cst = getTableCustomizer();
if (cst != null) {
cst.removeAllColumns();
}
}
finally {
setTableChanging(false);
}
}
@Override
public void fireSortColumnRemovedFromUI(IColumn<?> column) {
try {
pushUIProcessor();
//
getColumnSet().removeSortColumn(column);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
sort();
}
finally {
popUIProcessor();
}
}
@Override
public void fireGroupColumnRemovedFromUI(IColumn<?> column) {
try {
pushUIProcessor();
//
getColumnSet().removeGroupColumn(column);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
sort();
}
finally {
popUIProcessor();
}
}
@Override
public void fireOrganizeColumnAddFromUI(IColumn<?> column) {
try {
pushUIProcessor();
//
getTableOrganizer().addColumn(column);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
}
finally {
popUIProcessor();
}
}
@Override
public void fireOrganizeColumnRemoveFromUI(IColumn<?> column) {
try {
pushUIProcessor();
//
getTableOrganizer().removeColumn(column);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
}
finally {
popUIProcessor();
}
}
@Override
public void fireOrganizeColumnModifyFromUI(IColumn<?> column) {
try {
pushUIProcessor();
//
getTableOrganizer().modifyColumn(column);
ClientUIPreferences.getInstance().setAllTableColumnPreferences(AbstractTable.this);
}
finally {
popUIProcessor();
}
}
@Override
public void fireFilterAddedFromUI(IUserFilterState filter) {
try {
pushUIProcessor();
//
getUserFilterManager().addFilter(filter);
}
finally {
popUIProcessor();
}
}
@Override
public void fireFilterRemovedFromUI(IUserFilterState filter) {
try {
pushUIProcessor();
//
getUserFilterManager().removeFilter(filter);
}
finally {
popUIProcessor();
}
}
@Override
public void setFilteredRowsFromUI(List<? extends ITableRow> rows) {
try {
pushUIProcessor();
// Remove existing filter first, so that only one UserTableRowFilter is active
removeUserRowFilters(false);
// Create and add a new filter
UserTableRowFilter filter = new UserTableRowFilter(rows);
// Do not use addRowFilter to prevent applyRowFilters
m_rowFilters.add(filter);
applyRowFilters();
}
finally {
popUIProcessor();
}
}
@Override
public void removeFilteredRowsFromUI() {
try {
pushUIProcessor();
removeUserRowFilters();
}
finally {
popUIProcessor();
}
}
}
private class P_TableRowBuilder extends AbstractTableRowBuilder<Object> {
@Override
protected ITableRow createEmptyTableRow() {
return new TableRow(getColumnSet());
}
}
private static class P_CellEditorContext {
private final ITableRow m_row;
private final IColumn<?> m_column;
private final IFormField m_formField;
public P_CellEditorContext(ITableRow row, IColumn<?> col, IFormField f) {
m_row = row;
m_column = col;
m_formField = f;
}
public ITableRow getRow() {
return m_row;
}
public IColumn<?> getColumn() {
return m_column;
}
public IFormField getFormField() {
return m_formField;
}
}
protected static class LocalTableExtension<TABLE extends AbstractTable> extends AbstractExtension<TABLE> implements ITableExtension<TABLE> {
public LocalTableExtension(TABLE owner) {
super(owner);
}
@Override
public void execAppLinkAction(TableAppLinkActionChain chain, String ref) {
getOwner().execAppLinkAction(ref);
}
@Override
public void execRowAction(TableRowActionChain chain, ITableRow row) {
getOwner().execRowAction(row);
}
@Override
public void execContentChanged(TableContentChangedChain chain) {
getOwner().execContentChanged();
}
@Override
public ITableRowDataMapper execCreateTableRowDataMapper(TableCreateTableRowDataMapperChain chain, Class<? extends AbstractTableRowData> rowType) {
return getOwner().execCreateTableRowDataMapper(rowType);
}
@Override
public void execInitTable(TableInitTableChain chain) {
getOwner().execInitTable();
}
@Override
public void execResetColumns(TableResetColumnsChain chain, Set<String> options) {
getOwner().execResetColumns(options);
}
@Override
public void execDecorateCell(TableDecorateCellChain chain, Cell view, ITableRow row, IColumn<?> col) {
getOwner().execDecorateCell(view, row, col);
}
@Override
public void execDrop(TableDropChain chain, ITableRow row, TransferObject t) {
getOwner().execDrop(row, t);
}
@Override
public void execDisposeTable(TableDisposeTableChain chain) {
getOwner().execDisposeTable();
}
@Override
public void execRowClick(TableRowClickChain chain, ITableRow row, MouseButton mouseButton) {
getOwner().execRowClick(row, mouseButton);
}
@Override
public void execDecorateRow(TableDecorateRowChain chain, ITableRow row) {
getOwner().execDecorateRow(row);
}
@Override
public TransferObject execCopy(TableCopyChain chain, List<? extends ITableRow> rows) {
return getOwner().execCopy(rows);
}
@Override
public void execRowsSelected(TableRowsSelectedChain chain, List<? extends ITableRow> rows) {
getOwner().execRowsSelected(rows);
}
@Override
public TransferObject execDrag(TableDragChain chain, List<ITableRow> rows) {
return getOwner().execDrag(rows);
}
@Override
public void execRowsChecked(TableRowsCheckedChain chain, List<? extends ITableRow> row) {
getOwner().execRowsChecked(row);
}
}
protected final void interceptAppLinkAction(String ref) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableAppLinkActionChain chain = new TableAppLinkActionChain(extensions);
chain.execAppLinkAction(ref);
}
protected final void interceptRowAction(ITableRow row) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableRowActionChain chain = new TableRowActionChain(extensions);
chain.execRowAction(row);
}
protected final void interceptContentChanged() {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableContentChangedChain chain = new TableContentChangedChain(extensions);
chain.execContentChanged();
}
protected final ITableRowDataMapper interceptCreateTableRowDataMapper(Class<? extends AbstractTableRowData> rowType) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableCreateTableRowDataMapperChain chain = new TableCreateTableRowDataMapperChain(extensions);
return chain.execCreateTableRowDataMapper(rowType);
}
protected final void interceptInitTable() {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableInitTableChain chain = new TableInitTableChain(extensions);
chain.execInitTable();
}
protected final void interceptResetColumns(Set<String> options) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableResetColumnsChain chain = new TableResetColumnsChain(extensions);
chain.execResetColumns(options);
}
protected final void interceptDecorateCell(Cell view, ITableRow row, IColumn<?> col) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableDecorateCellChain chain = new TableDecorateCellChain(extensions);
chain.execDecorateCell(view, row, col);
}
protected final void interceptDrop(ITableRow row, TransferObject t) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableDropChain chain = new TableDropChain(extensions);
chain.execDrop(row, t);
}
protected final void interceptDisposeTable() {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableDisposeTableChain chain = new TableDisposeTableChain(extensions);
chain.execDisposeTable();
}
protected final void interceptRowClick(ITableRow row, MouseButton mouseButton) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableRowClickChain chain = new TableRowClickChain(extensions);
chain.execRowClick(row, mouseButton);
}
protected final void interceptDecorateRow(ITableRow row) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableDecorateRowChain chain = new TableDecorateRowChain(extensions);
chain.execDecorateRow(row);
}
protected final TransferObject interceptCopy(List<? extends ITableRow> rows) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableCopyChain chain = new TableCopyChain(extensions);
return chain.execCopy(rows);
}
protected final void interceptRowsSelected(List<? extends ITableRow> rows) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableRowsSelectedChain chain = new TableRowsSelectedChain(extensions);
chain.execRowsSelected(rows);
}
protected final void interceptRowsChecked(List<? extends ITableRow> rows) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableRowsCheckedChain chain = new TableRowsCheckedChain(extensions);
chain.execRowsChecked(rows);
}
protected final TransferObject interceptDrag(List<ITableRow> rows) {
List<? extends ITableExtension<? extends AbstractTable>> extensions = getAllExtensions();
TableDragChain chain = new TableDragChain(extensions);
return chain.execDrag(rows);
}
@Override
public boolean isValueChangeTriggerEnabled() {
return m_valueChangeTriggerEnabled >= 1;
}
@Override
public void setValueChangeTriggerEnabled(boolean b) {
if (b) {
m_valueChangeTriggerEnabled++;
}
else {
m_valueChangeTriggerEnabled--;
}
}
@Override
public ITableOrganizer getTableOrganizer() {
return m_tableOrganizer;
}
@Override
public boolean isCustomizable() {
return getTableCustomizer() != null;
}
@Override
public void setLoading(boolean loading) {
propertySupport.setPropertyBool(PROP_LOADING, loading);
}
@Override
public boolean isLoading() {
return propertySupport.getPropertyBool(PROP_LOADING);
}
@Override
public GroupingStyle getGroupingStyle() {
return (GroupingStyle) propertySupport.getProperty(PROP_GROUPING_STYLE);
}
@Override
public void setGroupingStyle(GroupingStyle groupingStyle) {
propertySupport.setProperty(PROP_GROUPING_STYLE, groupingStyle);
}
@Override
public HierarchicalStyle getHierarchicalStyle() {
return (HierarchicalStyle) propertySupport.getProperty(PROP_HIERARCHICAL_STYLE);
}
@Override
public void setHierarchicalStyle(HierarchicalStyle hierarchicalStyle) {
propertySupport.setProperty(PROP_HIERARCHICAL_STYLE, hierarchicalStyle);
}
public List<? extends IColumn<?>> getDynamicColumns() {
return m_dynamicColumns;
}
protected void setDynamicColumnsInternal(List<? extends IColumn<?>> dynamicColumns) {
m_dynamicColumns = dynamicColumns;
}
/**
* Adds the given dynamic columns to the table, imports the table data and tries to restore the selected rows.
* Previously injected dynamic columns are reset.
*/
public void updateDynamicColumns(List<? extends IColumn<?>> dynamicColumns, Runnable tablePopulator) {
try {
setTableChanging(true);
// Normally, the current selection is restored automatically during replacement of
// rows (see AbstractTable#replaceRows). Unfortunately, resetColumnConfiguraiton()
// discards all rows, causing the current selection to be lost. Therefore, we have
// to save and restore it manually.
Set<List<Object>> selectedKeys = getSelectedRows().stream()
.map(row -> row.getKeyValues())
.collect(Collectors.toSet());
// Update columns
setDynamicColumnsInternal(dynamicColumns);
resetColumnConfiguration();
// Insert data (rows)
if (tablePopulator != null) {
tablePopulator.run();
}
// Restore selection
selectRows(selectedKeys.stream()
.map(key -> findRowByKey(key))
.filter(Objects::nonNull)
.collect(Collectors.toList()));
}
finally {
setTableChanging(false);
}
}
}