package org.simantics.district.network.ui.techtype.table; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.nebula.widgets.nattable.NatTable; import org.eclipse.nebula.widgets.nattable.config.AbstractRegistryConfiguration; import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes; import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration; import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry; import org.eclipse.nebula.widgets.nattable.config.IEditableRule; import org.eclipse.nebula.widgets.nattable.copy.command.CopyDataCommandHandler; import org.eclipse.nebula.widgets.nattable.data.IDataProvider; import org.eclipse.nebula.widgets.nattable.edit.EditConfigAttributes; import org.eclipse.nebula.widgets.nattable.edit.editor.CheckBoxCellEditor; import org.eclipse.nebula.widgets.nattable.freeze.CompositeFreezeLayer; import org.eclipse.nebula.widgets.nattable.freeze.FreezeLayer; import org.eclipse.nebula.widgets.nattable.grid.GridRegion; import org.eclipse.nebula.widgets.nattable.grid.data.DefaultCornerDataProvider; import org.eclipse.nebula.widgets.nattable.grid.layer.ColumnHeaderLayer; import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer; import org.eclipse.nebula.widgets.nattable.grid.layer.DefaultColumnHeaderDataLayer; import org.eclipse.nebula.widgets.nattable.grid.layer.DefaultRowHeaderDataLayer; import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer; import org.eclipse.nebula.widgets.nattable.grid.layer.RowHeaderLayer; import org.eclipse.nebula.widgets.nattable.group.ColumnGroupHeaderLayer; import org.eclipse.nebula.widgets.nattable.group.ColumnGroupModel; import org.eclipse.nebula.widgets.nattable.hideshow.ColumnHideShowLayer; import org.eclipse.nebula.widgets.nattable.layer.DataLayer; import org.eclipse.nebula.widgets.nattable.layer.ILayer; import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer; import org.eclipse.nebula.widgets.nattable.layer.LabelStack; import org.eclipse.nebula.widgets.nattable.layer.cell.IConfigLabelAccumulator; import org.eclipse.nebula.widgets.nattable.painter.cell.CheckBoxPainter; import org.eclipse.nebula.widgets.nattable.reorder.RowReorderLayer; import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer; import org.eclipse.nebula.widgets.nattable.sort.SortHeaderLayer; import org.eclipse.nebula.widgets.nattable.sort.config.SingleClickSortConfiguration; import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes; import org.eclipse.nebula.widgets.nattable.style.DisplayMode; import org.eclipse.nebula.widgets.nattable.style.Style; import org.eclipse.nebula.widgets.nattable.util.GUIHelper; import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer; import org.eclipse.swt.SWT; import org.eclipse.swt.dnd.Clipboard; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Text; import org.simantics.Simantics; import org.simantics.db.ReadGraph; import org.simantics.db.Resource; import org.simantics.db.WriteGraph; import org.simantics.db.common.request.ResourceRead; import org.simantics.db.common.request.WriteRequest; import org.simantics.db.exception.DatabaseException; import org.simantics.db.layer0.request.PossibleActiveModel; import org.simantics.db.procedure.Listener; import org.simantics.district.network.ontology.DistrictNetworkResource; import org.simantics.district.network.techtype.requests.EnableTechTypeItem; import org.simantics.district.network.techtype.requests.PossibleTechTypeKeyName; import org.simantics.district.network.techtype.requests.PossibleTechTypeTable; import org.simantics.district.network.techtype.requests.WriteTechTypeTable; import org.simantics.utils.datastructures.Triple; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TechTypeTable extends Composite { private static final CheckBoxCellEditor CHECK_BOX_CELL_EDITOR = new CheckBoxCellEditor(); private static final CheckBoxPainter CHECK_BOX_PAINTER = new CheckBoxPainter(); private static final String INVALID_LABEL = "INVALID"; protected static final String CHECK_BOX_LABEL = "CHECK_BOX"; private final static Logger LOGGER = LoggerFactory.getLogger(TechTypeTable.class); NatTable table; private TechTypeTableDataProvider bodyDataProvider; DataLayer bodyDataLayer; private IConfigRegistry summaryConfigRegistry; private IUniqueIndexLayer summaryRowLayer; private ViewportLayer viewportLayer; private CompositeFreezeLayer compositeFreezeLayer; private FreezeLayer freezeLayer; // private TableDataSortModel sortModel; private ColumnHideShowLayer columnHideShowLayer; private ColumnGroupModel columnGroupModel = new ColumnGroupModel(); private TechTypeColumnHeaderTableDataProvider columnHeaderDataProvider; Clipboard cpb; public SelectionLayer selectionLayer; private TechTypeTableSortModel sortModel; private Resource componentType; private Resource tableResource; private String keyName; protected Set validationResult; public TechTypeTable(Composite parent, int style, Resource componentType) { super(parent, style); this.tableResource = null; this.componentType = componentType; defaultInitializeUI(); } private void defaultInitializeUI() { GridDataFactory.fillDefaults().grab(true, true).applyTo(this); GridLayoutFactory.fillDefaults().numColumns(1).applyTo(this); Composite filterComposite = new Composite(this, SWT.NONE); GridDataFactory.fillDefaults().grab(true, false).applyTo(filterComposite); GridLayoutFactory.fillDefaults().numColumns(1).applyTo(filterComposite); createFilterBar(filterComposite); Composite tableComposite = new Composite(this, SWT.NONE); GridDataFactory.fillDefaults().grab(true, true).applyTo(tableComposite); GridLayoutFactory.fillDefaults().numColumns(1).applyTo(tableComposite); createTable(tableComposite); } private void createFilterBar(Composite filterComposite) { Text filterText = new Text(filterComposite, SWT.BORDER); GridDataFactory.fillDefaults().grab(true, true).applyTo(filterText); filterText.addModifyListener(new ModifyListener() { @Override public void modifyText(ModifyEvent e) { System.out.println("text modified"); bodyDataProvider.setFilter(filterText.getText()); table.refresh(true); } }); } private void createTable(Composite parent) { Triple> tableData; String data = null; int[] enabled = null; if (componentType != null) { try { tableData = Simantics.getSession().syncRequest(new TableDataRequest(componentType), new TableDataListener(componentType)); if (tableData != null) { this.tableResource = tableData.first; data = tableData.second; enabled = tableData.third.stream().mapToInt(Integer::intValue).toArray(); } else { this.tableResource = null; } } catch (DatabaseException e) { LOGGER.error("Failed to read tech type table data for {}", componentType, e); } } // build the body layer stack // Usually you would create a new layer stack by extending // AbstractIndexLayerTransform and // setting the ViewportLayer as underlying layer. But in this case using the // ViewportLayer // directly as body layer is also working. bodyDataProvider = new TechTypeTableDataProvider(data, enabled); bodyDataLayer = new DataLayer(bodyDataProvider); bodyDataLayer.setConfigLabelAccumulator(new IConfigLabelAccumulator() { @Override public void accumulateConfigLabels(LabelStack configLabels, int columnPosition, int rowPosition) { if (columnPosition == 0) { configLabels.addLabel(CHECK_BOX_LABEL); } else if (validationResult != null && keyName != null) { int keyColumn = bodyDataProvider.getVariableIndex(keyName); if (keyColumn >= 0) { String key = (String) bodyDataProvider.getDataValue(keyColumn, rowPosition); String columnName = bodyDataProvider.getVariableName(columnPosition); if (validationResult.contains(key + "/" + columnName)) { configLabels.addLabel(INVALID_LABEL); } } } } }); bodyDataProvider.addEnableListener((rowIndex, enable) -> { if (this.tableResource != null) { try { Simantics.getSession().syncRequest( new EnableTechTypeItem(this.tableResource, rowIndex, enable) ); } catch (DatabaseException e) { LOGGER.error("Failed to update enable state for {}", tableResource, e); } } }); RowReorderLayer rowReorderLayer = new RowReorderLayer( columnHideShowLayer = new ColumnHideShowLayer(bodyDataLayer)); // HoverLayer hoverLayer = new HoverLayer(rowReorderLayer, false); // // we need to ensure that the hover styling is removed when the mouse // // cursor moves out of the cell area // hoverLayer.addConfiguration(new BodyHoverStylingBindings(hoverLayer)); // // selectionLayer = new SelectionLayer(hoverLayer); selectionLayer = new SelectionLayer(rowReorderLayer); viewportLayer = new ViewportLayer(selectionLayer); viewportLayer.setRegionName(GridRegion.BODY); freezeLayer = new FreezeLayer(selectionLayer); compositeFreezeLayer = new CompositeFreezeLayer(freezeLayer, viewportLayer, selectionLayer); // build the column header layer columnHeaderDataProvider = new TechTypeColumnHeaderTableDataProvider(bodyDataProvider); DataLayer columnHeaderDataLayer = new DefaultColumnHeaderDataLayer(columnHeaderDataProvider); columnHeaderDataLayer.setRowsResizableByDefault(false); columnHeaderDataLayer.setColumnsResizableByDefault(true); ColumnHeaderLayer columnHeaderLayer = new ColumnHeaderLayer(columnHeaderDataLayer, compositeFreezeLayer, selectionLayer); ColumnGroupHeaderLayer columnGroupHeaderLayer = new ColumnGroupHeaderLayer(columnHeaderLayer, selectionLayer, columnGroupModel); columnGroupHeaderLayer.setCalculateHeight(true); SortHeaderLayer columnSortHeaderLayer = new SortHeaderLayer<>(columnGroupHeaderLayer, sortModel = new TechTypeTableSortModel(bodyDataProvider)); // build the row header layer IDataProvider rowHeaderDataProvider = new TechTypeRowHeaderTableDataProvider(bodyDataProvider); DataLayer rowHeaderDataLayer = new DefaultRowHeaderDataLayer(rowHeaderDataProvider); rowHeaderDataLayer.setRowsResizableByDefault(false); rowHeaderDataLayer.setColumnsResizableByDefault(false); RowHeaderLayer rowHeaderLayer = new RowHeaderLayer(rowHeaderDataLayer, compositeFreezeLayer, selectionLayer); // build the corner layer IDataProvider cornerDataProvider = new DefaultCornerDataProvider(columnHeaderDataProvider, rowHeaderDataProvider); DataLayer cornerDataLayer = new DataLayer(cornerDataProvider); ILayer cornerLayer = new CornerLayer(cornerDataLayer, rowHeaderLayer, columnSortHeaderLayer); // build the grid layer GridLayer gridLayer = new GridLayer(compositeFreezeLayer, columnSortHeaderLayer, rowHeaderLayer, cornerLayer); table = new NatTable(parent, NatTable.DEFAULT_STYLE_OPTIONS | SWT.BORDER, gridLayer, false); GridDataFactory.fillDefaults().grab(true, true).applyTo(table); table.addConfiguration(new SingleClickSortConfiguration()); // Show entries labeled "INVALID" with red text table.addConfiguration(new AbstractRegistryConfiguration() { @Override public void configureRegistry(IConfigRegistry configRegistry) { Style cellStyle = new Style(); cellStyle.setAttributeValue(CellStyleAttributes.BACKGROUND_COLOR, GUIHelper.COLOR_RED); configRegistry.registerConfigAttribute(EditConfigAttributes.CELL_EDITABLE_RULE, IEditableRule.ALWAYS_EDITABLE); configRegistry.registerConfigAttribute( CellConfigAttributes.CELL_STYLE, cellStyle, DisplayMode.NORMAL, INVALID_LABEL ); configRegistry.registerConfigAttribute( EditConfigAttributes.CELL_EDITOR, CHECK_BOX_CELL_EDITOR, DisplayMode.EDIT, CHECK_BOX_LABEL ); configRegistry.registerConfigAttribute( CellConfigAttributes.CELL_PAINTER, CHECK_BOX_PAINTER, DisplayMode.NORMAL, CHECK_BOX_LABEL ); } }); // Register a CopyDataCommandHandler that also copies the headers and // uses the configured IDisplayConverters CopyDataCommandHandler copyHandler = new CopyDataCommandHandler(selectionLayer, columnHeaderDataLayer, rowHeaderDataLayer); copyHandler.setCopyFormattedText(true); gridLayer.registerCommandHandler(copyHandler); // initialize paste handler with SWT clipboard cpb = new Clipboard(getDisplay()); // PasteDataCommandHandler pasteHandler = new // PasteDataCommandHandler(bodyDataProvider, bodyDataLayer, selectionLayer, // cpb); // bodyDataLayer.registerCommandHandler(pasteHandler); table.addConfiguration(new DefaultNatTableStyleConfiguration()); // table.addConfiguration(new EditingSupportConfiguration(bodyDataProvider)); table.configure(); } private static String getKeyColumnName(Resource componentType) { String keyName = null; if (componentType != null) { try { keyName = Simantics.getSession().syncRequest(new PossibleTechTypeKeyName(componentType)); } catch (DatabaseException e) { LOGGER.error("Failed to read possible tech type key name for {}", componentType, e); } } return keyName.startsWith("_") ? keyName.substring(1) : keyName; } @Override public void dispose() { cpb.dispose(); super.dispose(); } public void setComponentType(Resource componentType) { if (Objects.equals(this.componentType, componentType)) return; this.componentType = componentType; this.keyName = getKeyColumnName(componentType); Simantics.getSession().asyncRequest(new TableDataRequest(componentType), new TableDataListener(componentType)); } private final class TableDataListener implements Listener>> { private final Resource componentType; private TableDataListener(Resource componentType) { this.componentType = componentType; } @Override public void execute(Triple> result) { TechTypeTable.this.getDisplay().asyncExec(() -> { TechTypeTable.this.tableResource = result.first; String data = result.second; int[] enabled = result.third.stream().mapToInt(Integer::intValue).toArray(); setTechTypeData(data, enabled); setValidationResult(null); }); } @Override public void exception(Throwable t) { LOGGER.error("Error updating tech type table data for {}", componentType, t); } @Override public boolean isDisposed() { return TechTypeTable.this.table != null && TechTypeTable.this.table.isDisposed() || !Objects.equals(TechTypeTable.this.componentType, componentType); } } private final class TableDataRequest extends ResourceRead>> { private TableDataRequest(Resource componentType) { super(componentType); } @Override public Triple> perform(ReadGraph graph) throws DatabaseException { Resource model = graph.syncRequest(new PossibleActiveModel(Simantics.getProjectResource())); if (model == null) return null; Resource tableResource = graph.syncRequest(new PossibleTechTypeTable(model, this.resource)); if (tableResource == null) return null; String data = graph.getRelatedValue2(tableResource, DistrictNetworkResource.getInstance(graph).TechType_TechTypeTable_HasData); int[] enabled = graph.getRelatedValue2(tableResource, DistrictNetworkResource.getInstance(graph).TechType_TechTypeTable_HasEnabledItems); List list = Arrays.stream(enabled).mapToObj(Integer::valueOf).collect(Collectors.toList()); return Triple.make(tableResource, data, list); } } public void setTechTypePath(String path) { String data; try { data = Files.lines(Paths.get(path), Charset.defaultCharset()).collect(Collectors.joining("\n")); } catch (IOException e) { LOGGER.error("Failed to read contents of file '{}' as {}", path, Charset.defaultCharset(), e); return; } try { Simantics.getSession().syncRequest(new WriteRequest() { @Override public void perform(WriteGraph graph) throws DatabaseException { graph.syncRequest(new WriteTechTypeTable(componentType, data)); } }); } catch (DatabaseException e) { LOGGER.error("Failed to write tech type table data to model", e); } setComponentType(componentType); } public void setTechTypeData(String data, int[] enabled) { bodyDataProvider.setData(data); bodyDataProvider.setEnabledFlags(enabled); table.refresh(true); } /** * Set results of a validation operation * * Invalid entries are designated by a string of the form "/". * * This method must be called in the SWT thread. * * @param result A set of strings representing invalid entries */ public void setValidationResult(Set result) { if (result != null && result.isEmpty()) result = null; this.validationResult = result; if (result != null) { keyName = getKeyColumnName(componentType); } table.refresh(); } /** * Get a resource representation of the currently open table, or null if * table is not stored in the model. */ public Resource getCurrentTable() { return tableResource; } }