| /*=============================================================================# |
| # Copyright (c) 2012, 2021 Original NatTable authors and others. |
| # |
| # This program and the accompanying materials are made available under the |
| # terms of the Eclipse Public License 2.0 which is available at |
| # https://www.eclipse.org/legal/epl-2.0. |
| # |
| # SPDX-License-Identifier: EPL-2.0 |
| # |
| # Contributors: |
| # Original NatTable authors and others - initial API and implementation |
| #=============================================================================*/ |
| |
| package org.eclipse.statet.ecommons.waltable; |
| |
| import static org.eclipse.statet.ecommons.waltable.coordinate.Orientation.HORIZONTAL; |
| import static org.eclipse.statet.ecommons.waltable.coordinate.Orientation.VERTICAL; |
| import static org.eclipse.statet.ecommons.waltable.painter.cell.GraphicsUtils.safe; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Properties; |
| |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.swt.SWT; |
| import org.eclipse.swt.custom.BusyIndicator; |
| import org.eclipse.swt.dnd.DragSource; |
| import org.eclipse.swt.dnd.DragSourceListener; |
| import org.eclipse.swt.dnd.DropTarget; |
| import org.eclipse.swt.dnd.DropTargetListener; |
| import org.eclipse.swt.dnd.Transfer; |
| import org.eclipse.swt.events.DisposeEvent; |
| import org.eclipse.swt.events.DisposeListener; |
| import org.eclipse.swt.events.FocusEvent; |
| import org.eclipse.swt.events.FocusListener; |
| import org.eclipse.swt.events.PaintEvent; |
| import org.eclipse.swt.events.PaintListener; |
| import org.eclipse.swt.graphics.Rectangle; |
| import org.eclipse.swt.widgets.Canvas; |
| import org.eclipse.swt.widgets.Composite; |
| import org.eclipse.swt.widgets.Event; |
| import org.eclipse.swt.widgets.Listener; |
| import org.eclipse.swt.widgets.ScrollBar; |
| |
| import org.eclipse.statet.ecommons.waltable.command.DisposeResourcesCommand; |
| import org.eclipse.statet.ecommons.waltable.command.ILayerCommand; |
| import org.eclipse.statet.ecommons.waltable.command.ILayerCommandHandler; |
| import org.eclipse.statet.ecommons.waltable.command.StructuralRefreshCommand; |
| import org.eclipse.statet.ecommons.waltable.config.ConfigRegistry; |
| import org.eclipse.statet.ecommons.waltable.config.DefaultNatTableStyleConfiguration; |
| import org.eclipse.statet.ecommons.waltable.config.IConfigRegistry; |
| import org.eclipse.statet.ecommons.waltable.config.IConfiguration; |
| import org.eclipse.statet.ecommons.waltable.conflation.EventConflaterChain; |
| import org.eclipse.statet.ecommons.waltable.conflation.IEventConflater; |
| import org.eclipse.statet.ecommons.waltable.conflation.VisualChangeEventConflater; |
| import org.eclipse.statet.ecommons.waltable.coordinate.LRectangle; |
| import org.eclipse.statet.ecommons.waltable.coordinate.Orientation; |
| import org.eclipse.statet.ecommons.waltable.coordinate.PixelOutOfBoundsException; |
| import org.eclipse.statet.ecommons.waltable.edit.ActiveCellEditorRegistry; |
| import org.eclipse.statet.ecommons.waltable.grid.ClientAreaResizeCommand; |
| import org.eclipse.statet.ecommons.waltable.grid.InitializeGridCommand; |
| import org.eclipse.statet.ecommons.waltable.layer.ILayer; |
| import org.eclipse.statet.ecommons.waltable.layer.ILayerListener; |
| import org.eclipse.statet.ecommons.waltable.layer.LabelStack; |
| import org.eclipse.statet.ecommons.waltable.layer.cell.ILayerCell; |
| import org.eclipse.statet.ecommons.waltable.layer.event.ILayerEvent; |
| import org.eclipse.statet.ecommons.waltable.layer.event.IVisualChangeEvent; |
| import org.eclipse.statet.ecommons.waltable.painter.IOverlayPainter; |
| import org.eclipse.statet.ecommons.waltable.painter.layer.ILayerPainter; |
| import org.eclipse.statet.ecommons.waltable.painter.layer.NatLayerPainter; |
| import org.eclipse.statet.ecommons.waltable.persistence.IPersistable; |
| import org.eclipse.statet.ecommons.waltable.selection.CellSelectionEvent; |
| import org.eclipse.statet.ecommons.waltable.swt.SWTUtil; |
| import org.eclipse.statet.ecommons.waltable.ui.IClientAreaProvider; |
| import org.eclipse.statet.ecommons.waltable.ui.binding.UiBindingRegistry; |
| import org.eclipse.statet.ecommons.waltable.ui.mode.ConfigurableModeEventHandler; |
| import org.eclipse.statet.ecommons.waltable.ui.mode.Mode; |
| import org.eclipse.statet.ecommons.waltable.ui.mode.ModeSupport; |
| import org.eclipse.statet.ecommons.waltable.util.GUIHelper; |
| import org.eclipse.statet.ecommons.waltable.viewport.RecalculateScrollBarsCommand; |
| import org.eclipse.statet.internal.ecommons.waltable.LayerListenerList; |
| import org.eclipse.statet.internal.ecommons.waltable.WaLTablePlugin; |
| |
| |
| public class NatTable extends Canvas implements ILayer, PaintListener, ILayerListener, IPersistable { |
| |
| public static final int DEFAULT_STYLE_OPTIONS= SWT.NO_BACKGROUND | SWT.NO_REDRAW_RESIZE | SWT.DOUBLE_BUFFERED | SWT.V_SCROLL | SWT.H_SCROLL; |
| |
| |
| private final NatTableDim hDim; |
| private final NatTableDim vDim; |
| |
| private IClientAreaProvider clientAreaProvider= new IClientAreaProvider() { |
| @Override |
| public LRectangle getClientArea() { |
| if (!isDisposed()) { |
| return SWTUtil.toNatTable(NatTable.this.getClientArea()); |
| } else { |
| return new LRectangle(0, 0, 0, 0); |
| } |
| } |
| |
| }; |
| |
| |
| private UiBindingRegistry uiBindingRegistry; |
| |
| private ModeSupport modeSupport; |
| |
| private final EventConflaterChain conflaterChain= new EventConflaterChain(); |
| |
| private final List<IOverlayPainter> overlayPainters= new ArrayList<>(); |
| |
| private final List<IPersistable> persistables= new LinkedList<>(); |
| |
| private ILayer underlyingLayer; |
| |
| private IConfigRegistry configRegistry; |
| |
| protected final Collection<IConfiguration> configurations= new LinkedList<>(); |
| |
| protected String id= GUIHelper.getSequenceNumber(); |
| |
| private ILayerPainter layerPainter= new NatLayerPainter(this); |
| |
| private final boolean autoconfigure; |
| |
| |
| public NatTable(final Composite parent, final ILayer layer) { |
| this(parent, DEFAULT_STYLE_OPTIONS, layer); |
| } |
| |
| public NatTable(final Composite parent, final ILayer layer, final boolean autoconfigure) { |
| this(parent, DEFAULT_STYLE_OPTIONS, layer, autoconfigure); |
| } |
| |
| public NatTable(final Composite parent, final int style, final ILayer layer) { |
| this(parent, style, layer, true); |
| } |
| |
| public NatTable(final Composite parent, final int style, final ILayer layer, final boolean autoconfigure) { |
| super(parent, style); |
| |
| this.hDim= new NatTableDim(this, layer.getDim(HORIZONTAL)); |
| this.vDim= new NatTableDim(this, layer.getDim(VERTICAL)); |
| |
| // Disable scroll bars by default; if a Viewport is available, it will enable the scroll bars |
| disableScrollBar(getHorizontalBar()); |
| disableScrollBar(getVerticalBar()); |
| |
| initInternalListener(); |
| |
| internalSetLayer(layer); |
| |
| this.autoconfigure= autoconfigure; |
| if (autoconfigure) { |
| this.configurations.add(new DefaultNatTableStyleConfiguration()); |
| configure(); |
| } |
| |
| this.conflaterChain.add(getVisualChangeEventConflater()); |
| this.conflaterChain.start(); |
| |
| addDisposeListener(new DisposeListener() { |
| |
| @Override |
| public void widgetDisposed(final DisposeEvent e) { |
| doCommand(new DisposeResourcesCommand()); |
| NatTable.this.conflaterChain.stop(); |
| ActiveCellEditorRegistry.unregisterActiveCellEditor(); |
| layer.dispose(); |
| } |
| |
| }); |
| } |
| |
| |
| @Override |
| public NatTableDim getDim(final Orientation orientation) { |
| if (orientation == null) { |
| throw new NullPointerException("orientation"); //$NON-NLS-1$ |
| } |
| |
| return (orientation == HORIZONTAL) ? this.hDim : this.vDim; |
| } |
| |
| |
| protected IEventConflater getVisualChangeEventConflater() { |
| return new VisualChangeEventConflater(this); |
| } |
| |
| private void disableScrollBar(final ScrollBar scrollBar) { |
| if (scrollBar != null) { |
| scrollBar.setMinimum(0); |
| scrollBar.setMaximum(1); |
| scrollBar.setThumb(1); |
| scrollBar.setEnabled(false); |
| } |
| } |
| |
| public ILayer getLayer() { |
| return this.underlyingLayer; |
| } |
| |
| private void internalSetLayer(final ILayer layer) { |
| if (layer != null) { |
| this.underlyingLayer= layer; |
| this.underlyingLayer.setClientAreaProvider(getClientAreaProvider()); |
| this.underlyingLayer.addLayerListener(this); |
| } |
| } |
| |
| /** |
| * Adds a configuration to the table. |
| * <p> |
| * Configurations are processed when the {@link #configure()} method is invoked. |
| * Each configuration object then has a chance to configure the |
| * <ol> |
| * <li>ILayer</li> |
| * <li>ConfigRegistry</li> |
| * <li>UiBindingRegistry</li> |
| * </ol> |
| */ |
| public void addConfiguration(final IConfiguration configuration) { |
| if (this.autoconfigure) { |
| throw new IllegalStateException("May only add configurations post construction if autoconfigure is turned off"); //$NON-NLS-1$ |
| } |
| |
| this.configurations.add(configuration); |
| } |
| |
| /** |
| * @return {@link IConfigRegistry} used to hold the configuration bindings |
| * by Layer, DisplayMode and Config labels. |
| */ |
| public IConfigRegistry getConfigRegistry() { |
| if (this.configRegistry == null) { |
| this.configRegistry= new ConfigRegistry(); |
| } |
| return this.configRegistry; |
| } |
| |
| public void setConfigRegistry(final IConfigRegistry configRegistry) { |
| if (this.autoconfigure) { |
| throw new IllegalStateException("May only set config registry post construction if autoconfigure is turned off"); //$NON-NLS-1$ |
| } |
| |
| this.configRegistry= configRegistry; |
| } |
| |
| /** |
| * @return Registry holding all the UIBindings contributed by the underlying layers |
| */ |
| public UiBindingRegistry getUiBindingRegistry() { |
| if (this.uiBindingRegistry == null) { |
| this.uiBindingRegistry= new UiBindingRegistry(this); |
| } |
| return this.uiBindingRegistry; |
| } |
| |
| public void setUiBindingRegistry(final UiBindingRegistry uiBindingRegistry) { |
| if (this.autoconfigure) { |
| throw new IllegalStateException("May only set UI binding registry post construction if autoconfigure is turned off"); //$NON-NLS-1$ |
| } |
| |
| this.uiBindingRegistry= uiBindingRegistry; |
| } |
| |
| public String getID() { |
| return this.id; |
| } |
| |
| @Override |
| protected void checkSubclass() { |
| } |
| |
| protected void initInternalListener() { |
| this.modeSupport= new ModeSupport(this); |
| this.modeSupport.registerModeEventHandler(Mode.NORMAL_MODE, new ConfigurableModeEventHandler(this.modeSupport, this)); |
| this.modeSupport.switchMode(Mode.NORMAL_MODE); |
| |
| addPaintListener(this); |
| |
| addFocusListener(new FocusListener() { |
| |
| @Override |
| public void focusLost(final FocusEvent arg0) { |
| redraw(); |
| } |
| |
| @Override |
| public void focusGained(final FocusEvent arg0) { |
| redraw(); |
| } |
| |
| }); |
| |
| addListener(SWT.Resize, new Listener() { |
| @Override |
| public void handleEvent(final Event e) { |
| doCommand(new ClientAreaResizeCommand(NatTable.this)); |
| } |
| }); |
| } |
| |
| @Override |
| public boolean forceFocus() { |
| return super.forceFocus(); |
| } |
| |
| // Painting /////////////////////////////////////////////////////////////// |
| |
| public List<IOverlayPainter> getOverlayPainters() { |
| return this.overlayPainters; |
| } |
| |
| public void addOverlayPainter(final IOverlayPainter overlayPainter) { |
| this.overlayPainters.add(overlayPainter); |
| } |
| |
| public void removeOverlayPainter(final IOverlayPainter overlayPainter) { |
| this.overlayPainters.remove(overlayPainter); |
| } |
| |
| @Override |
| public void paintControl(final PaintEvent event) { |
| paintNatTable(event); |
| } |
| |
| private void paintNatTable(final PaintEvent event) { |
| final Rectangle eventRectangle= new Rectangle(event.x, event.y, event.width, event.height); |
| |
| if (!eventRectangle.isEmpty()) { |
| getLayerPainter().paintLayer(this, event.gc, 0, 0, eventRectangle, getConfigRegistry()); |
| } |
| } |
| |
| @Override |
| public ILayerPainter getLayerPainter() { |
| return this.layerPainter; |
| } |
| |
| public void setLayerPainter(final ILayerPainter layerPainter) { |
| this.layerPainter= layerPainter; |
| } |
| |
| /** |
| * Repaint only a specific column in the grid. This method is optimized so that only the specific column is |
| * repainted and nothing else. |
| * |
| * @param columnPosition column of the grid to repaint |
| */ |
| public void repaintColumn(final long columnPosition) { |
| this.hDim.repaintPosition(columnPosition); |
| } |
| |
| /** |
| * Repaint only a specific row in the grid. This method is optimized so that only the specific row is repainted and |
| * nothing else. |
| * |
| * @param rowPosition row of the grid to repaint |
| */ |
| public void repaintRow(final long rowPosition) { |
| this.vDim.repaintPosition(rowPosition); |
| } |
| |
| protected void repaint(final Orientation orientation, final int start, final int size) { |
| if (orientation == HORIZONTAL) { |
| redraw(start, 0, size, safe(getHeight()), true); |
| } |
| else { |
| redraw(0, start, safe(getWidth()), size, true); |
| } |
| } |
| |
| |
| public void updateResize() { |
| updateResize(true); |
| } |
| |
| /** |
| * Update the table screen by re-calculating everything again. It should not |
| * be called too frequently. |
| * |
| * @param redraw |
| * true to redraw the table |
| */ |
| private void updateResize(final boolean redraw) { |
| if (isDisposed()) { |
| return; |
| } |
| doCommand(new RecalculateScrollBarsCommand()); |
| if (redraw) { |
| redraw(); |
| } |
| } |
| |
| /** |
| * Refreshes the entire NatTable as every layer will be refreshed. |
| */ |
| public void refresh() { |
| doCommand(new StructuralRefreshCommand()); |
| } |
| |
| @Override |
| public void configure(final ConfigRegistry configRegistry, final UiBindingRegistry uiBindingRegistry) { |
| throw new UnsupportedOperationException("Cannot use this method to configure NatTable. Use no-argument configure() instead."); //$NON-NLS-1$ |
| } |
| |
| /** |
| * Processes all the registered {@link IConfiguration} (s). |
| * All the underlying layers are walked and given a chance to configure. |
| * Note: all desired configuration tweaks must be done <i>before</i> this method is invoked. |
| */ |
| public void configure() { |
| if (this.underlyingLayer == null) { |
| throw new IllegalStateException("Layer must be set before configure is called"); //$NON-NLS-1$ |
| } |
| |
| if (this.underlyingLayer != null) { |
| this.underlyingLayer.configure((ConfigRegistry) getConfigRegistry(), getUiBindingRegistry()); |
| } |
| |
| for (final IConfiguration configuration : this.configurations) { |
| configuration.configureLayer(this); |
| configuration.configureRegistry(getConfigRegistry()); |
| configuration.configureUiBindings(getUiBindingRegistry()); |
| } |
| |
| // Once everything is initialized and properly configured we will |
| // now formally initialize the grid |
| doCommand(new InitializeGridCommand(this)); |
| } |
| |
| // Events ///////////////////////////////////////////////////////////////// |
| |
| @Override |
| public void handleLayerEvent(final ILayerEvent event) { |
| for (final ILayerListener layerListener : this.listeners.getListeners()) { |
| layerListener.handleLayerEvent(event); |
| } |
| |
| if (event instanceof IVisualChangeEvent) { |
| this.conflaterChain.addEvent(event); |
| } |
| |
| if (event instanceof CellSelectionEvent) { |
| final Event e= new Event(); |
| e.widget= this; |
| try { |
| notifyListeners(SWT.Selection, e); |
| } catch (final RuntimeException re) { |
| WaLTablePlugin.log(new Status(IStatus.ERROR, WaLTablePlugin.BUNDLE_ID, |
| "An error occurred when fireing SWT selection event.", re )); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| |
| // ILayer ///////////////////////////////////////////////////////////////// |
| |
| // Persistence |
| |
| /** |
| * Save the state of the table to the properties object. |
| * {@link ILayer#saveState(String, Properties)} is invoked on all the underlying layers. |
| * This properties object will be populated with the settings of all underlying layers |
| * and any {@link IPersistable} registered with those layers. |
| */ |
| @Override |
| public void saveState(final String prefix, final Properties properties) { |
| BusyIndicator.showWhile(null, new Runnable() { |
| |
| @Override |
| public void run() { |
| NatTable.this.underlyingLayer.saveState(prefix, properties); |
| } |
| }); |
| } |
| |
| /** |
| * Restore the state of the underlying layers from the values in the properties object. |
| * @see #saveState(String, Properties) |
| */ |
| @Override |
| public void loadState(final String prefix, final Properties properties) { |
| BusyIndicator.showWhile(null, new Runnable() { |
| |
| @Override |
| public void run() { |
| NatTable.this.underlyingLayer.loadState(prefix, properties); |
| } |
| }); |
| } |
| |
| /** |
| * @see ILayer#registerPersistable(IPersistable) |
| */ |
| @Override |
| public void registerPersistable(final IPersistable persistable) { |
| this.persistables.add(persistable); |
| } |
| |
| @Override |
| public void unregisterPersistable(final IPersistable persistable) { |
| this.persistables.remove(persistable); |
| } |
| |
| // Command |
| |
| @Override |
| public boolean doCommand(final ILayerCommand command) { |
| return this.underlyingLayer.doCommand(command); |
| } |
| |
| @Override |
| public void registerCommandHandler(final ILayerCommandHandler<?> commandHandler) { |
| this.underlyingLayer.registerCommandHandler(commandHandler); |
| } |
| |
| @Override |
| public void unregisterCommandHandler(final Class<? extends ILayerCommand> commandClass) { |
| this.underlyingLayer.unregisterCommandHandler(commandClass); |
| } |
| |
| // Events |
| |
| private final LayerListenerList listeners= new LayerListenerList(); |
| |
| @Override |
| public void fireLayerEvent(final ILayerEvent event) { |
| this.underlyingLayer.fireLayerEvent(event); |
| } |
| |
| @Override |
| public void addLayerListener(final ILayerListener listener) { |
| this.listeners.add(listener); |
| } |
| |
| @Override |
| public void removeLayerListener(final ILayerListener listener) { |
| this.listeners.remove(listener); |
| } |
| |
| // Columns/Horizontal |
| |
| @Override |
| public long getColumnCount() { |
| return this.hDim.getPositionCount(); |
| } |
| |
| @Override |
| public long getWidth() { |
| return this.hDim.getSize(); |
| } |
| |
| @Override |
| public long getColumnPositionByX(final long x) { |
| try { |
| return this.hDim.getPositionByPixel(x); |
| } |
| catch (final PixelOutOfBoundsException e) { |
| return Long.MIN_VALUE; |
| } |
| } |
| |
| |
| // Rows/Vertical |
| |
| @Override |
| public long getRowCount() { |
| return this.vDim.getPositionCount(); |
| } |
| |
| @Override |
| public long getHeight() { |
| return this.vDim.getSize(); |
| } |
| |
| @Override |
| public long getRowPositionByY(final long y) { |
| try { |
| return this.vDim.getPositionByPixel(y); |
| } |
| catch (final PixelOutOfBoundsException e) { |
| return Long.MIN_VALUE; |
| } |
| } |
| |
| // Y |
| |
| // Cell features |
| |
| @Override |
| public ILayerCell getCellByPosition(final long columnPosition, final long rowPosition) { |
| return this.underlyingLayer.getCellByPosition(columnPosition, rowPosition); |
| } |
| |
| // IRegionResolver |
| |
| @Override |
| public LabelStack getRegionLabelsByXY(final long x, final long y) { |
| return this.underlyingLayer.getRegionLabelsByXY(x, y); |
| } |
| |
| @Override |
| public ILayer getUnderlyingLayerByPosition(final long columnPosition, final long rowPosition) { |
| return this.underlyingLayer; |
| } |
| |
| @Override |
| public IClientAreaProvider getClientAreaProvider() { |
| return this.clientAreaProvider; |
| } |
| |
| @Override |
| public void setClientAreaProvider(final IClientAreaProvider clientAreaProvider) { |
| this.clientAreaProvider= clientAreaProvider; |
| this.underlyingLayer.setClientAreaProvider(clientAreaProvider); |
| } |
| |
| |
| // DND ///////////////////////////////////////////////////////////////// |
| |
| /** |
| * Adds support for dragging items out of this control via a user |
| * drag-and-drop operation. |
| * |
| * @param operations |
| * a bitwise OR of the supported drag and drop operation types ( |
| * <code>DROP_COPY</code>,<code>DROP_LINK</code>, and |
| * <code>DROP_MOVE</code>) |
| * @param transferTypes |
| * the transfer types that are supported by the drag operation |
| * @param listener |
| * the callback that will be invoked to set the drag data and to |
| * cleanup after the drag and drop operation finishes |
| * @see org.eclipse.swt.dnd.DND |
| */ |
| public void addDragSupport(final int operations, final Transfer[] transferTypes, final DragSourceListener listener) { |
| final DragSource dragSource= new DragSource(this, operations); |
| dragSource.setTransfer(transferTypes); |
| dragSource.addDragListener(listener); |
| } |
| |
| /** |
| * Adds support for dropping items into this control via a user drag-and-drop |
| * operation. |
| * |
| * @param operations |
| * a bitwise OR of the supported drag and drop operation types ( |
| * <code>DROP_COPY</code>,<code>DROP_LINK</code>, and |
| * <code>DROP_MOVE</code>) |
| * @param transferTypes |
| * the transfer types that are supported by the drop operation |
| * @param listener |
| * the callback that will be invoked after the drag and drop |
| * operation finishes |
| * @see org.eclipse.swt.dnd.DND |
| */ |
| public void addDropSupport(final int operations, final Transfer[] transferTypes, final DropTargetListener listener) { |
| final DropTarget dropTarget= new DropTarget(this, operations); |
| dropTarget.setTransfer(transferTypes); |
| dropTarget.addDropListener(listener); |
| } |
| |
| } |