plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/layers/CustomGridLayer.java
public class CustomGridLayer extends GridLayer {

	/**
	 * Constructor that passes `false` to the respective constructor, 
	 * {@link GridLayer#GridLayer(ILayer, ILayer, ILayer, ILayer, boolean)}.
	 * 
	 * @param bodyLayer
	 * @param columnHeaderLayer
	 * @param rowHeaderLayer
	 * @param cornerLayer
	 */
	public CustomGridLayer(ILayer bodyLayer, ILayer columnHeaderLayer, ILayer rowHeaderLayer, ILayer cornerLayer) {
		super(bodyLayer, columnHeaderLayer, rowHeaderLayer, cornerLayer, false);
	}
	
	@Override
    protected void init(boolean useDefaultConfiguration) {
        super.init(false);
        addConfiguration(new CustomGridLayerConfiguration(this));    }

	/**
	 * A customized {@link DefaultGridLayerConfiguration}.
	 *
	 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
	 * 
	 */
	public class CustomGridLayerConfiguration extends DefaultGridLayerConfiguration {

		/**
		 * Constructor simply calling the super constructor
		 * {@link DefaultGridLayerConfiguration#DefaultGridLayerConfiguration(CompositeLayer)}.
		 * 
		 * @param gridLayer
		 */
		public CustomGridLayerConfiguration(CompositeLayer gridLayer) {
			super(gridLayer);
		}
		
		/**
		 * Adds a custom configuration for edit bindings
		 * instead of using the default one.
		 */
		@Override
        protected void addEditingUIConfig() {
            addConfiguration(new GridEditorEditBindings());
        }

	}

}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/configuration/GridSpanningDataProvider.java
public class GridSpanningDataProvider extends AutomaticSpanningDataProvider {

	public GridSpanningDataProvider(IDataProvider underlyingDataProvider, boolean autoColumnSpan, boolean autoRowSpan) {
		super(underlyingDataProvider, autoColumnSpan, autoRowSpan);
	}
	
	@Override
	protected boolean valuesNotEqual(Object value1, Object value2) {
		if (value1 == null && value2 == null) {
			return true;
		}
		else if (value1 == value2) {
            return false;
        }
        return ((value1 == null && value2 != null)
                || (value1 != null && value2 == null)
                || !value1.equals(value2));
    }

}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/menu/GridPopupMenuConfiguration.java
public class GridPopupMenuConfiguration extends AbstractUiBindingConfiguration {

	private static final String MENU_NEW_COLUMN = "New annotation column";
	private static final String MENU_CREATE_SPAN = "Create span";
	private static final String MENU_DELETE_CLICKED_SPAN_ANNOTATION = "Delete clicked on span annotation";
	private static final String MENU_DELETE_SELECTED_SPAN_ANNOTATION = "Delete span annotation(s) in selection";
	private static final String MENU_SPLIT_CLICKED_SPAN_ANNOTATION = "Split clicked-on multi-cell span";
	private static final String MENU_MERGE_TO_SPAN = "Merge annotations into span";
	private static final String MENU_CREATE_TOKEN_AFTER_THIS = "Create token after the clicked one";
	private static final String MENU_CREATE_TOKEN_BEFORE_FIRST = "Create token before first";
	private static final String MENU_DELETE_TOKEN = "Delete token";
	private static final String MENU_PURGE_TOKEN = "Purge token";
	private static final String MENU_MERGE_TOKEN = "Merge token";
	private static final String MENU_SPLIT_TOKEN = "Split token";
	private final Menu menu;
	private final SelectionLayer selectionLayer;
	private final AnnotationGrid grid;
	private ILayerCell clickedCell;
	private Collection<ILayerCell> selectedCells = null;

    public GridPopupMenuConfiguration(final NatTable natTable, final AnnotationGrid grid, final SelectionLayer selectionLayer) {
    	this.grid = grid;
    	this.selectionLayer = selectionLayer;
        this.menu = new PopupMenuBuilder(natTable)
        		// Tokens
        		.withMenuItemProvider(MENU_CREATE_TOKEN_AFTER_THIS, new CreateTokenMenuItemProvider(false))
                .withVisibleState(MENU_CREATE_TOKEN_AFTER_THIS, new TokenMenuItemState())
                .withMenuItemProvider(MENU_CREATE_TOKEN_BEFORE_FIRST, new CreateTokenMenuItemProvider(true))
                .withVisibleState(MENU_CREATE_TOKEN_BEFORE_FIRST, new TokenMenuBeforeFirstItemState())
                .withMenuItemProvider(MENU_DELETE_TOKEN, new DeleteTokenMenuItemProvider())
                .withVisibleState(MENU_DELETE_TOKEN, new TokenMenuItemState())
                .withMenuItemProvider(MENU_PURGE_TOKEN, new PurgeTokenMenuItemProvider())
                .withVisibleState(MENU_PURGE_TOKEN, new TokenMenuItemState())
                .withMenuItemProvider(MENU_MERGE_TOKEN, new MergeTokenMenuItemProvider())
                .withVisibleState(MENU_MERGE_TOKEN, new MultiTokenSelectionMenuItemState())
                .withMenuItemProvider(MENU_SPLIT_TOKEN, new SplitTokenMenuItemProvider())
                .withVisibleState(MENU_SPLIT_TOKEN, new TokenMenuItemState())
        		// Spans
        		.withMenuItemProvider(MENU_NEW_COLUMN, new NewAnnotationColumnMenuItemProvider())
                .withMenuItemProvider(MENU_CREATE_SPAN, new CreateSpanMenuItemProvider())
                .withVisibleState(MENU_CREATE_SPAN, new MultiEmptyCellSelectionInOneColumnMenuItemState())
                .withMenuItemProvider(MENU_MERGE_TO_SPAN, new MergeCellsToSpanMenuItemProvider())
                .withVisibleState(MENU_MERGE_TO_SPAN, new MultiSpanCellSelectionMenuItemState())
                .withMenuItemProvider(MENU_SPLIT_CLICKED_SPAN_ANNOTATION, new SplitClickedSpanAnnotationMenuItemProvider())
                .withVisibleState(MENU_SPLIT_CLICKED_SPAN_ANNOTATION, new ClickedOnAnnotatedMultiCellSpanMenuItemState())
                .withMenuItemProvider(MENU_DELETE_CLICKED_SPAN_ANNOTATION, new DeleteClickedSpanAnnotationMenuItemProvider())
                .withVisibleState(MENU_DELETE_CLICKED_SPAN_ANNOTATION, new ClickedOnAnnotatedSpanMenuItemState())
                .withMenuItemProvider(MENU_DELETE_SELECTED_SPAN_ANNOTATION, new DeleteSelectedSpanAnnotationMenuItemProvider())
                .withVisibleState(MENU_DELETE_SELECTED_SPAN_ANNOTATION, new MultiSpanCellSelectionMenuItemState())
                .build();
    }

    @Override
    public void configureUiBindings(UiBindingRegistry uiBindingRegistry) {
        uiBindingRegistry.registerMouseDownBinding(
                new MouseEventMatcher(SWT.NONE, null, 3),
                new PopupMenuAction(this.menu));
    }

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class CreateTokenMenuItemProvider implements IMenuItemProvider {
	
		private final boolean isFirstToken;

		public CreateTokenMenuItemProvider(boolean isLastToken) {
			this.isFirstToken = isLastToken;
		}

		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
			MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
			if (!isFirstToken) {
				menuItem.setText("Create new token");
			}
			else {
				menuItem.setText("Create new first token");
			}
            menuItem.setEnabled(true);
            menuItem.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent event) {
                    executeCreateAnnotationColumn();
                }

				private void executeCreateAnnotationColumn() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getService(IHandlerService.class);
					
					Event event = new Event();
					event.data = new Object[]{clickedCell, grid, isFirstToken};
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.newToken", event);
					}
					catch (Exception e1) {
						throw new RuntimeException("Command org.corpus_tools.atomic.grideditor.commands.newToken not found!", e1);
					}
				}
            });
		}
	
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class DeleteTokenMenuItemProvider implements IMenuItemProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
			MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
            menuItem.setText("Delete token");
            menuItem.setEnabled(true);
            menuItem.addSelectionListener(new SelectionAdapter() {
            	@Override
            	public void widgetSelected(SelectionEvent event) {
            		executeDeleteTokenCommand();
            	}

				private void executeDeleteTokenCommand() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getService(IHandlerService.class);
					
					Event event = new Event();
					event.data = new Object[]{clickedCell, grid};
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.deleteToken", event);
					}
					catch (Exception e1) {
						throw new RuntimeException("Command org.corpus_tools.atomic.grideditor.commands.deleteToken not found!", e1);
					}
				}
            });
		}
	
	}
	
	/**
	 * // TODO Add description
	 *
	 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
	 * 
	 */
public class PurgeTokenMenuItemProvider implements IMenuItemProvider {

	/* (non-Javadoc)
	 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
	 */
	@Override
	public void addMenuItem(NatTable natTable, Menu popupMenu) {
		MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
        menuItem.setText("Purge token");
        menuItem.setEnabled(true);
        menuItem.addSelectionListener(new SelectionAdapter() {
        	@Override
        	public void widgetSelected(SelectionEvent event) {
        		executePurgeTokenCommand();
        	}

			private void executePurgeTokenCommand() {
				IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getService(IHandlerService.class);
				
				Event event = new Event();
				event.data = new Object[]{clickedCell, grid};
				event.widget = natTable;
				try {
					handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.purgeToken", event);
				}
				catch (Exception e1) {
					throw new RuntimeException("Command org.corpus_tools.atomic.grideditor.commands.purgeToken not found!", e1);
				}
			}
        });
	}

}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class MergeTokenMenuItemProvider implements IMenuItemProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
			MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
            menuItem.setText("Merge token");
            menuItem.setEnabled(true);

            menuItem.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent event) {
                    executeMergeTokenCommand();
                }

				private void executeMergeTokenCommand() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getService(IHandlerService.class);
					
					Event event = new Event();
					event.data = new Object[]{selectedCells, grid};
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.mergeTokens", event);
					}
					catch (Exception e1) {
						throw new RuntimeException("Command org.corpus_tools.atomic.grideditor.commands.mergeTokens not found!", e1);
					}
				}
            });
		}
	
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class SplitTokenMenuItemProvider implements IMenuItemProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
			MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
            menuItem.setText("Split token");
            menuItem.setEnabled(true);

            menuItem.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent event) {
                    executeSplitTokenCommand();
                }

				private void executeSplitTokenCommand() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getService(IHandlerService.class);
					
					Event event = new Event();
					event.data = new Object[]{clickedCell, grid};
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.splitToken", event);
					}
					catch (Exception e1) {
						throw new RuntimeException("Command org.corpus_tools.atomic.grideditor.commands.splitToken not found!", e1);
					}
				}
            });
		}
	
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class NewAnnotationColumnMenuItemProvider implements IMenuItemProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
			MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
            menuItem.setText("New annotation column");
            menuItem.setEnabled(true);

            menuItem.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent event) {
                    executeCreateAnnotationColumn();
                }

				private void executeCreateAnnotationColumn() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getService(IHandlerService.class);
					
					Event event = new Event();
					event.data = grid;
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.newAnnotationColumn", event);
					}
					catch (Exception e1) {
						throw new RuntimeException("Command org.corpus_tools.atomic.grideditor.commands.newAnnotationColumn not found!", e1);
					}
				}
            });
		}
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class CreateSpanMenuItemProvider implements IMenuItemProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
            MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
            menuItem.setText("Create new span");
            menuItem.setEnabled(true);

			menuItem.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent event) {
					executeCreateSpanCommand();
				}

				private void executeCreateSpanCommand() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
							.getService(IHandlerService.class);

					Event event = new Event();
//					if (selectedCells == null) {
//						throw new RuntimeException("The collection of selected cells is null which shouldn't be the case!"); // FIME Use log, provide better info, etc.
//					}
					event.data = new Object[] {selectedCells, grid};
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.createSpan", event);
					}
					catch (Exception e1) {
						throw new RuntimeException(
								"Command org.corpus_tools.atomic.grideditor.commands.createSpan not found!", e1);
					}
				}
			});
		}

	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class MergeCellsToSpanMenuItemProvider implements IMenuItemProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
			MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
            menuItem.setText("Merge to single span");
            menuItem.setEnabled(true);

			menuItem.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent event) {
					executeMergeCellsToSpanCommand();
				}

				private void executeMergeCellsToSpanCommand() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
							.getService(IHandlerService.class);

					Event event = new Event();
//					if (selectedCells == null) {
//						throw new RuntimeException("The collection of selected cells is null which shouldn't be the case!"); // FIME Use log, provide better info, etc.
//					}
					event.data = new Object[] {selectedCells, grid};
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.mergeCellsToSpan", event);
					}
					catch (Exception e1) {
						throw new RuntimeException(
								"Command org.corpus_tools.atomic.grideditor.commands.mergeCellsToSpan not found!", e1);
					}
				}
			});
		}
	
	}

	/**
		 * // TODO Add description
		 * 
		 * TODO Check if this can be refactored to be used for token annotations as well
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class DeleteClickedSpanAnnotationMenuItemProvider implements IMenuItemProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
			MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
            menuItem.setText("Delete cell annotation");
            menuItem.setEnabled(true);

			menuItem.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent event) {
					executeDeleteSpanAnnotationCommand();
				}

				private void executeDeleteSpanAnnotationCommand() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
							.getService(IHandlerService.class);

					Event event = new Event();
					if (clickedCell == null) {
						throw new RuntimeException("The clicked on object is null which shouldn't be the case!"); // FIME Use log, provide better info, etc.
					}
					event.data = new Object[]{clickedCell, grid};
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.deleteClickedSpanAnnotation", event);
					}
					catch (Exception e1) {
						throw new RuntimeException(
								"Command org.corpus_tools.atomic.grideditor.commands.deleteClickedSpanAnnotation not found!", e1);
					}
				}
			});
		}
	
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class DeleteSelectedSpanAnnotationMenuItemProvider implements IMenuItemProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
			MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
            menuItem.setText("Delete selected");
            menuItem.setEnabled(true);

			menuItem.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent event) {
					executeDeleteSelectedSpanCommand();
				}

				private void executeDeleteSelectedSpanCommand() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
							.getService(IHandlerService.class);

					Event event = new Event();
					if (selectedCells == null) {
						throw new RuntimeException("The collection of selected cells is null which shouldn't be the case!"); // FIME Use log, provide better info, etc.
					}
					event.data = new Object[]{selectedCells, grid};
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.deleteSelectedSpanAnnotation", event);
					}
					catch (Exception e1) {
						throw new RuntimeException(
								"Command org.corpus_tools.atomic.grideditor.commands.deleteSelectedSpanAnnotation not found!", e1);
					}
				}
			});
		}
	
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class SplitClickedSpanAnnotationMenuItemProvider implements IMenuItemProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemProvider#addMenuItem(org.eclipse.nebula.widgets.nattable.NatTable, org.eclipse.swt.widgets.Menu)
		 */
		@Override
		public void addMenuItem(NatTable natTable, Menu popupMenu) {
			MenuItem menuItem = new MenuItem(popupMenu, SWT.PUSH);
            menuItem.setText("Split span");
            menuItem.setEnabled(true);

			menuItem.addSelectionListener(new SelectionAdapter() {
				@Override
				public void widgetSelected(SelectionEvent event) {
					executeSplitSpanCommand();
				}

				private void executeSplitSpanCommand() {
					IHandlerService handlerService = PlatformUI.getWorkbench().getActiveWorkbenchWindow()
							.getService(IHandlerService.class);

					Event event = new Event();
					if (clickedCell == null) {
						throw new RuntimeException("The clicked on object is null which shouldn't be the case!"); // FIME Use log, provide better info, etc.
					}
					event.data = new Object[]{clickedCell, grid};
					event.widget = natTable;
					try {
						handlerService.executeCommand("org.corpus_tools.atomic.grideditor.commands.splitClickedSpanAnnotation", event);
					}
					catch (Exception e1) {
						throw new RuntimeException(
								"Command org.corpus_tools.atomic.grideditor.commands.splitClickedSpanAnnotation not found!", e1);
					}
				}
			});
		}
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class TokenMenuItemState implements IMenuItemState {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemState#isActive(org.eclipse.nebula.widgets.nattable.ui.NatEventData)
		 */
		@Override
		public boolean isActive(NatEventData natEventData) {
			GridPopupMenuConfiguration.this.clickedCell = natEventData.getNatTable().getCellByPosition(natEventData.getColumnPosition(), natEventData.getRowPosition());
			Object value = clickedCell.getDataValue();
			return value instanceof SToken;
		}
	
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class TokenMenuBeforeFirstItemState implements IMenuItemState {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemState#isActive(org.eclipse.nebula.widgets.nattable.ui.NatEventData)
		 */
		@Override
		public boolean isActive(NatEventData natEventData) {
			GridPopupMenuConfiguration.this.clickedCell = natEventData.getNatTable().getCellByPosition(natEventData.getColumnPosition(), natEventData.getRowPosition());
			Object value = clickedCell.getDataValue();
			return value instanceof SToken && clickedCell.getRowIndex() == 0;
		}
	
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class MultiTokenSelectionMenuItemState implements IMenuItemState {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemState#isActive(org.eclipse.nebula.widgets.nattable.ui.NatEventData)
		 */
		@Override
		public boolean isActive(NatEventData natEventData) {
			Collection<ILayerCell> selectedCells = selectionLayer.getSelectedCells();
			GridPopupMenuConfiguration.this.selectedCells = selectedCells;
			int noOfSelCells = selectedCells.size();
			int noOfSelCols = selectionLayer.getSelectedColumnPositions().length;
			return noOfSelCells > 1 && noOfSelCols == 1 && selectedCells.stream().allMatch(c -> {
				return c.getColumnIndex() == 0;
			});
		}
	
	}

	/**
	 * An {@link IMenuItemState} which is active only when
	 * more than one cells in the {@link NatTable} are
	 * selected, and all of the selected cells are in a
	 * single column.
	 *
	 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
	 * 
	 */
	public class MultiEmptyCellSelectionInOneColumnMenuItemState implements IMenuItemState {

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemState#isActive(
		 * org.eclipse.nebula.widgets.nattable.ui.NatEventData)
		 */
		@Override
		public boolean isActive(NatEventData natEventData) {
			Collection<ILayerCell> selectedCells = selectionLayer.getSelectedCells();
			int noOfSelCells = selectedCells.size();
			int noOfSelCols = selectionLayer.getSelectedColumnPositions().length;
			final boolean allCellsEmpty = selectedCells.stream().allMatch(c -> {
				Object val = c.getDataValue();
				/*
				 * Check whether cell data is either `null`, or if it isn't,
				 * then if it is an SAnnnotation, and if it is,
				 * then if the annotation value is either `null` or an empty String.
				 */
				if (val == null || (val instanceof SAnnotation && (((SAnnotation) val).getValue() == null) || (val instanceof SAnnotation && ((SAnnotation) val).getValue_STEXT().isEmpty()))) {
					return true;
				}
				return false;
			});
			return noOfSelCells > 1 && noOfSelCols == 1 && allCellsEmpty;
		}

	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class MultiSpanCellSelectionMenuItemState implements IMenuItemState {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemState#isActive(org.eclipse.nebula.widgets.nattable.ui.NatEventData)
		 */
		@Override
		public boolean isActive(NatEventData natEventData) {
			Collection<ILayerCell> selectedCells = selectionLayer.getSelectedCells();
			/*
			 * Check whether at least one cell is selected, and whether the
			 * data value is not null and is an instance of SAnnotation and
			 * whether the annotation value is not null and not the empty String.
			 */
			if (selectedCells.size() > 0) {
				for (ILayerCell cell : selectedCells) {
					Object val = cell.getDataValue();
					if (val != null && (val instanceof SAnnotation && (((SAnnotation) val).getValue() != null) && !((SAnnotation) val).getValue_STEXT().isEmpty())) {
						GridPopupMenuConfiguration.this.selectedCells = selectedCells;
						return true;
					} 
				}
			}
			return false;
		}
	
	}

	/**
		 * // TODO Add description
		 * 
		 * Works only on the clicked on cell, not the selection
		 * 
		 * TODO implement for selection as extra menu item
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class ClickedOnAnnotatedSpanMenuItemState implements IMenuItemState {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemState#isActive(org.eclipse.nebula.widgets.nattable.ui.NatEventData)
		 */
		@Override
		public boolean isActive(NatEventData natEventData) {
			GridPopupMenuConfiguration.this.clickedCell = natEventData.getNatTable().getCellByPosition(natEventData.getColumnPosition(), natEventData.getRowPosition());
			Object value = clickedCell.getDataValue();
			if (value != null && value instanceof SAnnotation && ((SAnnotation) value).getContainer() instanceof SSpan) {
				return true;
			}
			return false;
		}
	
	}
	
	public class ClickedOnAnnotatedMultiCellSpanMenuItemState implements IMenuItemState {
		
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemState#isActive(org.eclipse.nebula.widgets.nattable.ui.NatEventData)
		 */
		@Override
		public boolean isActive(NatEventData natEventData) {
			GridPopupMenuConfiguration.this.clickedCell = natEventData.getNatTable().getCellByPosition(natEventData.getColumnPosition(), natEventData.getRowPosition());
			Object value = clickedCell.getDataValue();
			if (value != null && value instanceof SAnnotation && ((SAnnotation) value).getContainer() instanceof SSpan && clickedCell.isSpannedCell()) {
				return true;
			}
			return false;
		}
	
	}
	

	// TODO Use for checking for cell selection
//	/* (non-Javadoc)
//	 * @see org.eclipse.nebula.widgets.nattable.ui.menu.IMenuItemState#isActive(org.eclipse.nebula.widgets.nattable.ui.NatEventData)
//	 */
//	@Override
//	public boolean isActive(NatEventData natEventData) {
//		boolean selectedCellsIncludeAnnotatedSpan = selectionLayer.getSelectedCells().stream().anyMatch(c -> {
//			Object val = c.getDataValue();
//			/*
//			 * Check whether any of the selected cells includes a non-empty
//			 * cell, and if they do,
//			 * then if its value is an SAnnotation, and if it is,
//			 * then whether the annotation container is a span.
//			 */
//			if (val != null && val instanceof SAnnotation && ((SAnnotation) val).getContainer() instanceof SSpan){// && (((SAnnotation) val).getValue() != null || !((SAnnotation) val).getValue_STEXT().isEmpty())) {
//				return true;
//			}
//			return false;
//		});
//		return selectedCellsIncludeAnnotatedSpan;
//	}


}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/data/AnnotationGridDataProvider.java
public class AnnotationGridDataProvider implements IDataProvider {

	private AnnotationGrid annotationGrid;

	public AnnotationGridDataProvider(AnnotationGrid annotationGrid) {
		this.annotationGrid = annotationGrid;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.nebula.widgets.nattable.data.IDataProvider#getDataValue(int, int)
	 */
	@Override
	public Object getDataValue(int columnIndex, int rowIndex) {
		return annotationGrid.get(rowIndex, columnIndex);
	}

//	private Object getColumnIndex() {
//		colKeySet = annotationGrid.columnKeySet()
//		return null;
//	}

	/* (non-Javadoc)
	 * @see org.eclipse.nebula.widgets.nattable.data.IDataProvider#setDataValue(int, int, java.lang.Object)
	 */
	@Override
	public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
		Object previous = getDataValue(columnIndex, rowIndex); 
		SDocumentGraph graph = annotationGrid.getGraph();
		if (previous instanceof SAnnotation) {
			((SAnnotation) previous).setValue(newValue);
			annotationGrid.record(rowIndex, columnIndex, annotationGrid.getColumnHeaderMap().get(columnIndex), previous);
		}
		else if (previous == null) {
			SToken token = graph.getSortedTokenByText().get(rowIndex);
			SSpan span = graph.createSpan(token);
			String namespace = null;
			String name = null;
			String[] headerSplit = annotationGrid.getColumnHeaderMap().get(columnIndex).split("::");
			if (headerSplit.length == 2) {
				namespace = headerSplit[0].equals("null") ? null : headerSplit[0];
				name = headerSplit[1];
			}
			else if (headerSplit.length == 1) {
				name = headerSplit[0];
			}
			SAnnotation newAnno = span.createAnnotation(namespace, name, newValue);
			annotationGrid.record(rowIndex, columnIndex, annotationGrid.getColumnHeaderMap().get(columnIndex), newAnno);
		}
		else {
			// Not null, not an SAnnotation
			throw new UnsupportedOperationException("Not supported yet!");
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.nebula.widgets.nattable.data.IDataProvider#getColumnCount()
	 */
	@Override
	public int getColumnCount() {
		return annotationGrid != null ? annotationGrid.getColumnHeaderMap().size() : 0;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.nebula.widgets.nattable.data.IDataProvider#getRowCount()
	 */
	@Override
	public int getRowCount() {
		return annotationGrid != null ? annotationGrid.getRowMap().size() : 0;
	}

//	/* (non-Javadoc)
//	 * @see org.eclipse.nebula.widgets.nattable.data.ISpanningDataProvider#getCellByPosition(int, int)
//	 */
//	@Override
//	public DataCell getCellByPosition(int columnPosition, int rowPosition) {
//		// TODO Auto-generated method stub
//		return null;
//	}

	/**
	 * @return the annotationGrid
	 */
	public AnnotationGrid getAnnotationTable() {
		return annotationGrid;
	}

	/**
	 * @param annotationGrid the annotationGrid to set
	 */
	public void setAnnotationTable(AnnotationGrid annotationTable) {
		this.annotationGrid = annotationTable;
	}

}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/data/GridRowHeaderDataProvider.java
public class GridRowHeaderDataProvider extends DefaultRowHeaderDataProvider {

	private AnnotationGrid annotationTable = null;

	public GridRowHeaderDataProvider(AnnotationGrid annotationTable) {
		super(null);
		this.annotationTable = annotationTable;
	}
	
	@Override
    public Object getDataValue(int columnIndex, int rowIndex) {
        return Integer.valueOf(rowIndex);
    }
	
	@Override
	public int getRowCount() {
		return annotationTable.getRowMap().size();
	}

}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/data/GridColumnHeaderDataProvider.java
public class GridColumnHeaderDataProvider extends DefaultColumnHeaderDataProvider {

	private AnnotationGrid annotationTable = null;

	public GridColumnHeaderDataProvider(AnnotationGrid annotationTable) {
		super(null);
		this.annotationTable = annotationTable;
	}
	
	@Override
	public String getColumnHeaderLabel(int columnIndex) {
		String label = annotationTable.getColumnHeaderMap().get(columnIndex);//FluentIterable.from(annotationTable.columnKeySet()).skip(columnIndex).limit(columnIndex + 1).toList().get(0);
		return label;
	}
	
	@Override
    public int getColumnCount() {
        return annotationTable.getColumnHeaderMap().size();//columnKeySet().size();
    }

}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/configuration/GridEditorSelectionConfiguration.java
public class GridEditorSelectionConfiguration extends DefaultSelectionLayerConfiguration {

	private AnnotationGrid annotationGrid;

	public GridEditorSelectionConfiguration(AnnotationGrid annotationGrid) {
		this.annotationGrid = annotationGrid;
	}
	
	@Override
	protected void addSelectionUIBindings() {
		addConfiguration(new GridEditorSelectionBindings());
	}

}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/selection/SingleCellSelection.java
public class SingleCellSelection implements ISelection {

	private final ILayerCell cell;

	public SingleCellSelection(ILayerCell cell) {
		this.cell = cell;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jface.viewers.ISelection#isEmpty()
	 */
	@Override
	public boolean isEmpty() {
		return cell == null;
	}
	
	public int getRowIndex() {
		return cell.getRowIndex();
	}
	
	public int getColumnIndex() {
		return cell.getColumnIndex();
	}
	
	public Object getValue() {
		return cell.getDataValue();
	}
	
	public boolean singleCell() {
		return true;
	}

}
plugins/org.corpus-tools.atomic/src/main/java/org/corpus_tools/atomic/api/editors/DocumentGraphEditor.java
public abstract class DocumentGraphEditor extends EditorPart {
	
	private static final Logger log = LogManager.getLogger(DocumentGraphEditor.class);


	/**
	 * The default name for {@link SaltProject} serialization files.
	 */
	public static final String SALT_PROJECT_FILE_NAME = "saltProject.salt";
	
	
	protected SDocumentGraph graph = null;


	protected boolean dirty;


	private boolean isValidInput = true;
	
	
	/* (non-Javadoc)
	 * @see org.eclipse.ui.part.EditorPart#doSave(org.eclipse.core.runtime.IProgressMonitor)
	 */
	@Override
	public void doSave(IProgressMonitor monitor) {
		IPath resPath = ((FileEditorInput) getEditorInput()).getPath();
		SaltUtil.saveDocumentGraph(getGraph(), URI.createFileURI(resPath.toOSString()));
		
		this.dirty = false;
		firePropertyChange(EditorPart.PROP_DIRTY);

	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.part.EditorPart#doSaveAs()
	 */
	@Override
	public void doSaveAs() {	
//		IPath origResPath = ((FileEditorInput) getEditorInput()).getPath();
//		FileDialog dlg = new FileDialog(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell());
//		// use the folder of the current file as argument
//		dlg.setFilterPath(origResPath.toFile().getParent());
//		dlg.setFilterExtensions(new String[] {"*.salt", "*.*"});
//		String path = dlg.open();
//		if(path != null) {
//			SaltUtil.saveDocumentGraph(getGraph(), URI.createFileURI(path));
//			
//			this.dirty = false;
//			firePropertyChange(EditorPart.PROP_DIRTY);
//		}
	}

	/** 
	 * Handles initialization of the editor.
	 * 
	 * Clients should check against {@link #isInputValid()} after calling
	 * `super.init(site, input)` in order to catch any exceptions that
	 * may occur when the initialization is aborted (e.g., due to the
	 * user trying to open a Salt project file with a document editor
	 * extending this class).
	 * 
	 * @see org.eclipse.ui.part.EditorPart#init(org.eclipse.ui.IEditorSite, org.eclipse.ui.IEditorInput)
	 */
	@Override
	public void init(IEditorSite site, IEditorInput input) throws PartInitException {
		// Site and input must be set in init
		setSite(site);
		setInput(input);
		
		if (!(input instanceof FileEditorInput)) {
			log.error("Input for editors of type {} must be of type {}, but the provided input is of type {}!", this.getClass().getName(), FileEditorInput.class.getName(), input.getClass().getName(), new AtomicGeneralException());
		}
		else {
			String filePath = ((FileEditorInput) input).getPath().makeAbsolute().toOSString();
			log.trace("Input for editor {} is file '{}'.", this.getClass().getName(), filePath);
			if (filePath.endsWith(SALT_PROJECT_FILE_NAME)) {
				MessageDialog.openInformation(Display.getDefault().getActiveShell(), "Cannot open project file", "This editor does work only on Salt document files, but you have tried to open a Salt project file (\"" + SALT_PROJECT_FILE_NAME + "\").\n\nClosing editor.");
				isValidInput = false;
				Display.getDefault().asyncExec(new Runnable() {
					@Override
					public void run() {
						getSite().getPage().closeEditor(DocumentGraphEditor.this, false); 
					}
				});
				log.info("Attempt to open editor for Salt documents (" + this.getClass().getSimpleName() + ") with Salt project file aborted.");
				return;
			}
			IProject iProject = ((FileEditorInput) input).getFile().getProject();
			IFile saltProjectResource = null;
			try {
				for (IResource member : iProject.members()) {
					if (member instanceof IFile && member.getName().equalsIgnoreCase(SALT_PROJECT_FILE_NAME)) { // FIXME Externalize and treat for case, etc
						saltProjectResource = (IFile) member;
					}
				}
			}
			catch (CoreException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			String projectFilePath = saltProjectResource.getRawLocation().toOSString();
			SaltProject saltProject = SaltFactory.createSaltProject();
			saltProject.loadCorpusStructure(URI.createFileURI(projectFilePath));
			for (SDocument doc : saltProject.getCorpusGraphs().get(0).getDocuments()) {
				if (doc.getDocumentGraphLocation().equals(URI.createFileURI(filePath))) {
					doc.loadDocumentGraph();
					graph = doc.getDocumentGraph();
				}
			}
			log.trace("Loaded document graph {}.", graph);
			
			// Set name of editor
			setPartName(((FileEditorInput) input).getPath().lastSegment());
			// Set up editor for automatic context switches on activation/deactivation 
			PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage().addPartListener(new PartContextListener(site.getId(), site.getPluginId()));
		}
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.part.EditorPart#isDirty()
	 */
	@Override
	public boolean isDirty() {
		return dirty;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.ui.part.EditorPart#isSaveAsAllowed()
	 */
	@Override
	public boolean isSaveAsAllowed() {
		// TODO Auto-generated method stub
		return false;
	}

	/** 
	 * Simply passes the parent argument to an abstract method which
	 * clients need to override to create a control for this
	 * editor part.
	 * 
	 * **Note:** Clients should not override this method, albeit its
	 * visibility. Instead, they should override the method 
	 * {@link #createEditorPartControl(Composite)}!
	 * 
	 * @see org.eclipse.ui.part.WorkbenchPart#createPartControl(org.eclipse.swt.widgets.Composite)
	 */
	@Override
	public void createPartControl(Composite parent) {
		createEditorPartControl(parent);
	}

	/**
	 * TODO: Description
	 *
	 * @param parent
	 */
	public abstract void createEditorPartControl(Composite parent);

//	/* (non-Javadoc)
//	 * @see org.eclipse.ui.part.WorkbenchPart#setFocus()
//	 */
//	@Override
//	public void setFocus() {
//		// TODO Auto-generated method stub
//
//	}

	/**
	 * Returns the single instance of {@link SDocumentGraph} that
	 * the editor can work on. The graph is initially set in {@link #init(IEditorSite, IEditorInput)}
	 * and the referenced object itself should not be changed anywhere 
	 * in the editor.
	 * 
	 * @return the graph
	 */
	public final SDocumentGraph getGraph() {
		return graph;
	}

	/**
	 * @param dirty the dirty to set
	 */
	public final void setDirty(boolean dirty) {
		this.dirty = dirty;
		firePropertyChange(PROP_DIRTY);
	}

	/**
	 * @return the isValidInput
	 */
	public final boolean isInputValid() {
		return isValidInput;
	}
	
	@Override
	public void setFocus() {
		/*
		 * Empty implementation here prevents forced
		 * implementation down the inheritance line
		 * but can still be overridden to provide
		 * customized implementations.
		 */
	}

}
plugins/org.corpus-tools.atomic.tagset/src/main/java/org/corpus_tools/atomic/tagset/api/Tagset.java
@JsonDeserialize(using = TagsetDeserializer.class)
public interface Tagset extends Serializable {

	boolean addValue(TagsetValue value);
	
	boolean addValue(int index, TagsetValue value);
	
	boolean removeValue(TagsetValue value);
	
	void setValues(List<TagsetValue> values);
	
	/**
	 * // TODO Add description
	 * 
	 * @return the list of **valid** values for this tagset's corpus
	 */
	List<TagsetValue> getValues();
	
	/**
	 * Gets the complete set of **valid** values for the given parameters,
	 * i.e., all annotation values for which 
	 * 
	 * - the tagset layer name equals the given layer name (i.e., the name of 
	 * one of the layers the annotation's container is added to, and those
	 * for which the tagset layer name is `null`, i.e., not restricted
	 * - the tagset element type equals the given element type (i.e.,
	 * the {@link SALT_TYPE} of the annotation's container, and those for
	 * which the tagset element type is `null`, i.e., not restricted
	 * - the tagset annotation namespace equals the namespace of the
	 * annotation, and those for which the namespace is `null`, i.e., 
	 * not restricted
	 * - for which the tagset annotation name equals the annotations's
	 * name.  
	 * 
	 * @param layer The name of one of the layers the annotation's container is added to
	 * @param elementType The {@link SALT_TYPE} of the annotation's container
	 * @param namespace The annotation's namespace
	 * @param name The annotation's name
	 * @return the complete set of **valid** values for the combination of given parameters 
	 */
	Set<TagsetValue> getValuesForParameters(String layer, SALT_TYPE elementType, String namespace, String name);
	
	/**
	 * Gets a complete set of **valid** annotation names for the given
	 * parameters.
	 * 
	 * @param layer
	 * @param elementType
	 * @param namespace
	 * @return the complete set of **valid** annotation names for the given parameters
	 */
	Set<String> getAnnotationNamesForParameters(String layer, SALT_TYPE elementType, String namespace);
	
	/**
	 * Saves the tagset at the given URI
	 * 
	 * @param uri The URI where the tagset should be saved
	 * @return `true` if the tagset has been successfully save at the given URI
	 */
	boolean save(URI uri);
	
	/**
	 * @return the name of the tagset
	 */
	String getName();
	
	/**
	 * Sets the name of the tagset
	 * 
	 * @param name The tagset's name
	 */
	void setName(String name);
	
	/**
	 * Sets the identifier id of the corpus for which this
	 * tagset provides valid annotation values.
	 * 
	 * @param corpusId The identifier id of corpus this tagset is for
	 */
	void setCorpusId(String corpusId);
	
	/**
	 * @return the identifier id of the corpus this tagset is for
	 */
	String getCorpusId();
	
}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/data/annotationgrid/AnnotationGrid.java
public class AnnotationGrid {

	Map<Integer, Row> rowMap = new HashMap<>();
	BiMap<Integer, String> columnHeaderMap = HashBiMap.create();
	private final SDocumentGraph graph;

	public AnnotationGrid(SDocumentGraph graph) {
		this.graph = graph;
	}

	/**
	 * Top-level method for adding content to the grid.
	 * 
	 * Takes row and column indices, a header for the
	 * column in which the cell to be constructed down
	 * the line should be placed, and the value object.
	 * 
	 * If no {@link Row} exists at the respective row
	 * index, a new one is initialized, otherwise, the
	 * existing {@link Row} is used. The arguments
	 * `colIndex`, `header` and `value` are then passed
	 * to {@link Row#put(int, String, Object)}, and the
	 * row put (back) in the {@link #rowMap}.
	 * 
	 * @param rowIndex The row index for the passed value.
	 * @param colIndex The column index for the passed value.
	 * @param header The column header for the passed value.
	 * @param value The value object.
	 */
	public void record(int rowIndex, int colIndex, String header, Object value) {
		Row row = rowMap.get(rowIndex);
		if (row == null) {
			row = new Row();
		}
		row.put(colIndex, header, value);
		rowMap.put(rowIndex, row);
	}

	/**
	 * Retrieves the value object from the cell
	 * located at the give row index and column
	 * index.
	 * 
	 * @param rowIndex The row index for the value to be retrieved.
	 * @param colIndex The column index for the value to be retrieved.
	 * @return The value of the cell which is located at rowIndex:colIndex in the grid.
	 */
	public Object get(int rowIndex, int colIndex) {
		Row row = rowMap.get(rowIndex);
		if (row == null) {
			return null;
		}
		else if (row.get(colIndex) == null) {
			return null;
		}
		return row.get(colIndex).getValue();
	}

	/**
	 * @return the columnHeaderMap
	 */
	public BiMap<Integer, String> getColumnHeaderMap() {
		return columnHeaderMap;
	}

	/**
	 * @return the rowMap
	 */
	public Map<Integer, Row> getRowMap() {
		return rowMap;
	}
	
	/**
	 * // TODO Add description FIXME
	 * Remove col:
	 * - Remove col in each row
	 * - Decrement cell indices in all cells with index > colindexToDelete by 1
	 * - Remove header from header map 
	 * - Decrement header indices in all headers with index > colindexToDelete by 1
	 * 
	 * @param colIndex
	 */
	public void removeColumn(Integer colIndex) {
		int colIndexInt = colIndex.intValue();
		Map<Integer, Row> tempRowMap = new HashMap<>();
		BiMap<Integer, String> tempColumnHeaderMap = HashBiMap.create();
		
		for (Row row : rowMap.values()) {
			row.removeCell(colIndexInt);
		}
		columnHeaderMap.remove(colIndexInt);
		columnHeaderMap.forEach((i,s) -> {
			int iInt = i.intValue();
			if (iInt < colIndexInt) {
				tempColumnHeaderMap.put(i, s);
			}
			else if (iInt > colIndexInt) {
				tempColumnHeaderMap.put(i - 1, s);
			}
		});
		columnHeaderMap = tempColumnHeaderMap;
		rowMap.entrySet().forEach(e -> {
			Integer rowIndex = e.getKey();
			Row row = e.getValue();
			Map<Integer, Cell> tempCells = new HashMap<>();
			row.getCells().forEach((i,c) -> {
				int intI = i.intValue();
				if (intI < colIndexInt) {
					tempCells.put(i, c);
				}
				else if (intI > colIndexInt) {
					tempCells.put(i - 1, c);
				}
			});
			row.setCells(tempCells);
			tempRowMap.put(rowIndex, row);
		});
		rowMap = tempRowMap;
	}

	/**
	 * Lays out the annotation grid so that it can be used
	 * for display.
	 * 
	 * Before this method is run for the first time, there
	 * may be {@link Row}s in the annotation grid which
	 * contain less cells than others. This is due to the
	 * fact the the grid is constructed row by row, and
	 * the process will not know about the contents of
	 * any other row. Hence it is necessary to evaluate
	 * whether all rows contain cells for all existing
	 * column headers across the grid.
	 * 
	 * The method iterates over the {@link #rowMap} and
	 * checks whether for each row, there is a cell for
	 * each column header (using the {@link #columnHeaderMap},
	 * which at this point must be complete, i.e., this
	 * method should only be run after all values have 
	 * been added to the grid, and re-run after each 
	 * change to the grid's contents; also using a
	 * newly streamed map from headers to cell values
	 * for each {@link Row}).
	 * 
	 * If there is no cell for a column header, a new
	 * cell is added to the {@link Row}, using the
	 * respective index for the column header from the
	 * {@link #columnHeaderMap}, the value from the created
	 * header-to-cell-value map, and the header from
	 * the {@link #columnHeaderMap}. 
	 */
	public void layout() {
		Iterator<Entry<Integer, Row>> it = rowMap.entrySet().iterator();
		while (it.hasNext()) {
			Entry<Integer, Row> current = it.next();
			Row currentRow = current.getValue();
			if (columnHeaderMap.size() != currentRow.getCells().size()) {
				Map<String, Object> cellHeaderMap = currentRow.getCells().entrySet().stream().collect(Collectors.toMap(e -> e.getValue().getColumnHeader(), e -> e.getValue().getValue()));
				for (Entry<String, Integer> headerEntry : columnHeaderMap.inverse().entrySet()) {
					currentRow.getCells().put(headerEntry.getValue(), new Cell(cellHeaderMap.get(headerEntry.getKey()), headerEntry.getKey()));
				}
			}
		}
	}

	/**
	 * @author Stephan Druskat
	 * 
	 * Objects of this type represent rows in an
	 * annotation grid of type {@link AnnotationGrid}.
	 * 
	 * They are backed by a map of cells ({@link #cells}),
	 * which is keyed on the position of the row in the grid.
	 */
	public class Row {
	
		private Map<Integer, Cell> cells = new HashMap<>();
	
		/**
		 * Adds cells to this row.
		 * 
		 * First, the method performs a check whether the given
		 * column header already exists in the global header map.
		 * If so, `colIndex` is overwritten with the column
		 * index from the global header map for pre-sorting.
		 * Otherwise, the passed column header is added to the
		 * header map with the passed column index as key.
		 * 
		 * Subsequently, a new {@link Cell} is constructed
		 * with the passed column index and the passed
		 * column header, and keyed with the column index.
		 * 
		 * @param colIndex The index of the column where the cell should be located in the grid (this may change during layout).
		 * @param colHeader The header of the column the constructed cell should be in.
		 * @param value The cell value.
		 */
		public void put(int colIndex, String colHeader, Object value) {
			// Check if columnHeader already exists
			if (columnHeaderMap.inverse().containsKey(colHeader)) {
				colIndex = columnHeaderMap.inverse().get(colHeader);
			} 
			else { 
				columnHeaderMap.put(colIndex, colHeader);
			}
			cells.put(colIndex, new Cell(value, colHeader));
		}
		
		/**
		 * // TODO Add description FIXME
		 * 
		 * @param colIndex
		 * @return
		 */
		public Row removeCell(int colIndex) {
			Row row = this;
			row.getCells().remove(colIndex);
			return row;
		}
	
		/**
		 * @param colIndex A column index.
		 * @return The {@link Cell} at the respective column index.
		 */
		public Cell get(int colIndex) {
			return cells.get(colIndex);
		}
		
		/**
		 * @param colIndex A column index.
		 * @return The header of the {@link Cell} at the respective column index.
		 */
		public String getHeader(int colIndex) {
			return cells.get(colIndex).getColumnHeader();
		}
		
		/**
		 * @param colIndex A column index.
		 * @return The value object of the {@link Cell} at the respective column index.
		 */
		public Object getValue(int colIndex) {
			return cells.get(colIndex).getValue();
		}

		/**
		 * @return The map of cells for this row, keyed by index.
		 */
		public Map<Integer, Cell> getCells() {
			return cells;
		}
		
		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			for (Entry<Integer, Cell> c : getCells().entrySet()) {
				sb.append(c.getKey() + ":" + c.getValue() + " | ");
			}
			return sb.toString();
		}

		/**
		 * @param cells the cells to set
		 */
		public final void setCells(Map<Integer, Cell> cells) {
			this.cells = cells;
		}
	
	}

	/**
	 * @author Stephan Druskat
	 * 
	 * A bean representing a cell in an annotation grid of
	 * type {@link AnnotationGrid}.
	 * 
	 * A cell contains a value and is sorted under
	 * a column header.
	 */
	public class Cell {
	
		private Object value;
		private String columnHeader;
	
		/**
		 * // TODO Add description
		 * 
		 * @param value
		 * @param columnHeader
		 */
		public Cell(Object value, String columnHeader) {
			this.value = value;
			this.columnHeader = columnHeader;
		}
	
		/**
		 * @return the value
		 */
		public Object getValue() {
			return value;
		}
	
		/**
		 * @return the columnHeader
		 */
		public String getColumnHeader() {
			return columnHeader;
		}
	
	}

	/**
	 * @return the graph
	 */
	public SDocumentGraph getGraph() {
		return graph;
	}

}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/selection/MultiCellSelection.java
public class MultiCellSelection implements ISelection {
	
	// FIXME Implement

	private final ArrayList<ILayerCell> cells;

	public MultiCellSelection(ArrayList<ILayerCell> cells) {
		this.cells = cells;
	}

	/* (non-Javadoc)
	 * @see org.eclipse.jface.viewers.ISelection#isEmpty()
	 */
	@Override
	public boolean isEmpty() {
		return this.cells == null;
	}

}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/configuration/GridEditorConfiguration.java
public class GridEditorConfiguration extends AbstractRegistryConfiguration {

	private static final String TOKEN_TEXT_COLUMN_LABEL = "Token";
	private static final String INVALID = "INVALID";
	private final static Image ERROR_IMAGE = FieldDecorationRegistry.getDefault().getFieldDecoration(FieldDecorationRegistry.DEC_ERROR).getImage();
	private final GridEditor editor;
	private final ColumnOverrideLabelAccumulator accumulator;

	public GridEditorConfiguration(GridEditor gridEditor, ColumnOverrideLabelAccumulator columnLabelAccumulator) {
		this.editor = gridEditor;
		this.accumulator = columnLabelAccumulator; 
	}

	/* (non-Javadoc)
	 * @see org.eclipse.nebula.widgets.nattable.config.IConfiguration#configureRegistry(org.eclipse.nebula.widgets.nattable.config.IConfigRegistry)
	 */
	@Override
	public void configureRegistry(IConfigRegistry configRegistry) {
		// Register configuration labels on (some) columns
		registerConfigLabelsOnColumns();
		// Make cells editable
		configRegistry.registerConfigAttribute(EditConfigAttributes.CELL_EDITABLE_RULE, IEditableRule.ALWAYS_EDITABLE);
		// Configure what to show in the grid
		configRegistry.registerConfigAttribute(CellConfigAttributes.DISPLAY_CONVERTER, getAnnotationValueConverter(), DisplayMode.NORMAL);
		// Configure cell editing
		ICellEditor cellEditor = new AnnotationCellEditor();
		cellEditor = editor.hasTagset ? new AnnotationComboEditor(new TagsetValuesDataProvider()) : new AnnotationCellEditor();
		configRegistry.registerConfigAttribute(EditConfigAttributes.CELL_EDITOR, cellEditor, DisplayMode.EDIT, null);
		// Automatically open the editor for the cell below FIXME Make this configurable
//		configRegistry.registerConfigAttribute(EditConfigAttributes.OPEN_ADJACENT_EDITOR, true);
		// Switch off out-of-the-box multi-cell editing
		configRegistry.registerConfigAttribute(EditConfigAttributes.SUPPORT_MULTI_EDIT, false);
		// Disable editing of token text
		configRegistry.registerConfigAttribute(EditConfigAttributes.CELL_EDITABLE_RULE, IEditableRule.NEVER_EDITABLE, DisplayMode.EDIT, TOKEN_TEXT_COLUMN_LABEL);
		
		
		
		
		CellPainterDecorator errorDecoration = new CellPainterDecorator(new TextPainter(), CellEdgeEnum.TOP_LEFT,
				new ImagePainter(ERROR_IMAGE), false);
		configRegistry.registerConfigAttribute(CellConfigAttributes.CELL_PAINTER, errorDecoration, DisplayMode.NORMAL,
				INVALID);
		IStyle validationErrorStyle = new Style();
		validationErrorStyle.setAttributeValue(CellStyleAttributes.FOREGROUND_COLOR, GUIHelper.COLOR_RED);

		configRegistry.registerConfigAttribute(CellConfigAttributes.CELL_STYLE, validationErrorStyle,
				DisplayMode.NORMAL, INVALID);
	}

	private void registerConfigLabelsOnColumns() {
		accumulator.registerColumnOverrides(0, TOKEN_TEXT_COLUMN_LABEL);
		
	}

	private IDisplayConverter getAnnotationValueConverter() {
		return new DisplayConverter() {
			
			@Override
			public Object displayToCanonicalValue(Object displayValue) {
				if (displayValue.equals("")) {
					return null;
				}
				else {
					return displayValue;
				}

			}
			
			@Override
			public Object canonicalToDisplayValue(Object canonicalValue) {
				if (canonicalValue == null) {
					return "";
				}
				else if (canonicalValue instanceof SAnnotation) {
					return ((SAnnotation) canonicalValue).getValue();
				}
				else if (canonicalValue instanceof SToken) {
					if (((SToken) canonicalValue).getGraph().getText((SNode) canonicalValue).isEmpty()) {
						return "∅";
						// FIXME: Use different colouring or similar
					}
					return ((SToken) canonicalValue).getGraph().getText((SNode) canonicalValue);
				}
				else {
					return canonicalValue;
				}
			}
		};
	}

	public class AnnotationCellEditor extends TextCellEditor {
		
		private Object originalCanonicalValue = null;

		@Override
	    protected Control activateCell(final Composite parent, Object originalCanonicalValue) {
			this.originalCanonicalValue = GridEditorConfiguration.this.getAnnotationValueConverter().canonicalToDisplayValue(originalCanonicalValue);
			return super.activateCell(parent, originalCanonicalValue);
		}

		@Override
		public boolean commit(MoveDirectionEnum direction, boolean closeAfterCommit, boolean skipValidation) {
			if (getCanonicalValue() != null) {
				if (!getCanonicalValue().equals(originalCanonicalValue)) {
					GridEditorConfiguration.this.editor.setDirty(true);
				}
			}
			else if (originalCanonicalValue != null) {
				GridEditorConfiguration.this.editor.setDirty(true);
			}
			return super.commit(MoveDirectionEnum.DOWN, true, true);

		}
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class AnnotationComboEditor extends ComboBoxCellEditor {
		
		public AnnotationComboEditor(IComboBoxDataProvider provider) {
			super(provider);
			this.freeEdit = true;
		}
		
		private Object originalCanonicalValue = null;

		@Override
	    protected Control activateCell(final Composite parent, Object originalCanonicalValue) {
			this.originalCanonicalValue = GridEditorConfiguration.this.getAnnotationValueConverter().canonicalToDisplayValue(originalCanonicalValue);
			if (this.originalCanonicalValue == null) {
				return super.activateCell(parent, this.originalCanonicalValue);
			}
			return super.activateCell(parent, this.originalCanonicalValue);
		}

		@Override
		public boolean commit(MoveDirectionEnum direction, boolean closeAfterCommit, boolean skipValidation) {
			if (getCanonicalValue() != null) {
				if (!getCanonicalValue().equals(originalCanonicalValue)) {
					GridEditorConfiguration.this.editor.setDirty(true);
				}
			}
			else if (originalCanonicalValue != null) {
				GridEditorConfiguration.this.editor.setDirty(true);
			}
			return super.commit(MoveDirectionEnum.RIGHT, true, true);
		}

	
	}

	/**
		 * // TODO Add description
		 *
		 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
		 * 
		 */
	public class TagsetValuesDataProvider implements IComboBoxDataProvider {
	
		/* (non-Javadoc)
		 * @see org.eclipse.nebula.widgets.nattable.edit.editor.IComboBoxDataProvider#getValues(int, int)
		 */
		@Override
		public List<?> getValues(int columnIndex, int rowIndex) {
			List<String> validValues = new ArrayList<>();
			Tagset tagset = GridEditorConfiguration.this.editor.getTagset();
			AnnotationGrid grid = GridEditorConfiguration.this.editor.getAnnotationGrid();
			Object gridObject = grid.get(rowIndex, columnIndex);
			if (gridObject != null) {
				if (gridObject instanceof SAnnotation) {
					SAnnotation annotation = (SAnnotation) gridObject;
					LabelableElement container = annotation.getContainer();
					if (container instanceof SStructuredNode) {
						/*
						 * TODO In this iteration of the editor, no element types
						 * are queried.
						 */
						String namespace = annotation.getNamespace();
						String name = annotation.getName();
//						SStructuredNode node = (SStructuredNode) container;
//						Set<SLayer> layers = node.getLayers();
//						if (layers.size() != 0) {
//							for (SLayer l : layers) {
//								tagset.getValuesForParameters(l.getName(), null, namespace, name).forEach(v -> validValues.add(v.getValue()));
//							}
//						}
//						else {
							// FIXME: Document that GridEditor does not support layers and element types yet!
							tagset.getValuesForParameters(null, null, namespace, name).forEach(v -> validValues.add(v.getValue()));
//						}
					}
				}
			}
			else {
				// gridObject is null
				/* 
				 * TODO: INTRODUCE LAYER IN DISPLAY (in grid column header, add layer name)
				 * THEN ADD LAYER TO NEWLY CREATED ANNOTATION!
				 */
				String header = grid.getColumnHeaderMap().get(columnIndex);
				String namespace = null;
				String name = null;
				String[] headerSplit = header.split("::");
				if (headerSplit.length == 2) {
					namespace = headerSplit[0].equals("null") ? null : headerSplit[0];
					name = headerSplit[1];
				}
				else if (headerSplit.length == 1) {
					name = headerSplit[0];
				}
				// FIXME: Document that GridEditor does not support layers and element types yet!
				Set<TagsetValue> values = tagset.getValuesForParameters(null, null, namespace, name);
				values.stream().forEach(v -> validValues.add(v.getValue()));
			}
			return validValues;
		}
	
	}

}
plugins/org.corpus-tools.atomic.grideditor/src/main/java/org/corpus_tools/atomic/grideditor/configuration/CustomGridLabelAccumulator.java
public class CustomGridLabelAccumulator extends ColumnOverrideLabelAccumulator {

	private GridSpanningDataProvider rowDataProvider;
	private Tagset tagset;
	private AnnotationGrid grid;

	public CustomGridLabelAccumulator(ILayer layer, GridSpanningDataProvider rowDataProvider, Tagset tagset, AnnotationGrid annotationGrid) {
		super(layer);
		this.rowDataProvider = rowDataProvider;
		this.tagset = tagset;
		this.grid = annotationGrid;
	}
	
	@Override
	public void accumulateConfigLabels(LabelStack configLabels, int columnPosition, int rowPosition) {
		super.accumulateConfigLabels(configLabels, columnPosition, rowPosition);

		if (tagset != null) {
			// Get the row object via data provider
			Object rowObject = rowDataProvider.getDataValue(columnPosition, rowPosition);
			if (rowObject instanceof SAnnotation) {
				String annoVal = ((SAnnotation) rowObject).getValue_STEXT();
				String namespace = null;
				String name = null;
				String[] headerSplit = grid.getColumnHeaderMap().get(columnPosition).split("::");
				if (headerSplit.length == 2) {
					namespace = headerSplit[0].equals("null") ? null : headerSplit[0];
					name = headerSplit[1];
				}
				else if (headerSplit.length == 1) {
					name = headerSplit[0];
				}
				Set<TagsetValue> values = tagset.getValuesForParameters(null, null, namespace, name);
				Collection<String> valueStrings = values.stream().map(TagsetValue::getValue)
						.collect(Collectors.toCollection(ArrayList::new));
				// Check if annotation value validifies
				if (!valueStrings.contains(annoVal)) {
					configLabels.addLabel("INVALID");
				}
			}
		}
	}

}
plugins/org.corpus-tools.atomic.tagset/src/main/java/org/corpus_tools/atomic/tagset/api/TagsetAwareEditor.java
public interface TagsetAwareEditor extends IEditorPart {

	static final Logger log = LogManager.getLogger(TagsetAwareEditor.class);

	default public Tagset loadTagset(IEditorInput input) {
		Tagset tagset = null;
		IFileEditorInput fileEditorInput = null;
		if (input instanceof IFileEditorInput) {
			fileEditorInput = (IFileEditorInput) input;
		}
		else {
			log.info("Cannot retrieve project from editor input that is not based on a file.");
			return null;
		}
		IProject iProject = fileEditorInput.getFile().getProject();

		IFile tagsetFileResource = null;
		File tagsetFile = null;
		try {
			for (IResource member : iProject.members()) {
				if (member instanceof IFile && member.getFileExtension().equals("ats")) {
					tagsetFileResource = (IFile) member;
				}
			}
		}
		catch (CoreException e) {
			log.error("An error occurred getting the tagset file.", e);
		}
		if (tagsetFileResource != null) {
			IPath location = tagsetFileResource.getLocation();
			if (location != null)
				tagsetFile = location.toFile();
		}
		else {
			log.info("There is no tagset file in project \"{}\".", iProject.getName());
			return null;
		}
		if (tagsetFile != null) {
			String tagsetFilePath = tagsetFile.getAbsolutePath();
			long tagsetFileSize = 0;
			try {
				tagsetFileSize = Files.size(Paths.get(tagsetFile.getAbsolutePath()));
				log.trace("Tagset file {} has size {}.", tagsetFilePath, String.valueOf(tagsetFileSize));
			}
			catch (IOException e) {
				log.warn("Failed to load tagset file {} in order to calculate its size.", tagsetFilePath, e);
				return null;
			}
			if (tagsetFileSize != 0) {
				tagset = TagsetFactory.load(URI.createFileURI(tagsetFilePath));
			}
			else {
				log.info("Tagset file \"{}\" is empty.", tagsetFile.getName());
				return null;
			}
			if (tagset == null) {
				log.error("Could not read tagset from tagset file {}.", tagsetFilePath);
				return null;
			}
			log.info("Loaded tagset {} ({}) from {}.", tagset, tagset.getName(), tagsetFilePath);
			return tagset;
		}
		else {
			log.info("There is no actual file backing the IResource \"{}\".", tagsetFileResource);
			return null;
		}
	}

}


Question: package org.corpus_tools.atomic.grideditor;
import java.lang.reflect.InvocationTargetException; 
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.corpus_tools.atomic.api.editors.DocumentGraphEditor;
import org.corpus_tools.atomic.grideditor.configuration.CustomGridLabelAccumulator;
import org.corpus_tools.atomic.grideditor.configuration.GridEditorConfiguration;
import org.corpus_tools.atomic.grideditor.configuration.GridEditorSelectionConfiguration;
import org.corpus_tools.atomic.grideditor.configuration.GridSpanningDataProvider;
import org.corpus_tools.atomic.grideditor.selection.MultiCellSelection;
import org.corpus_tools.atomic.grideditor.selection.SingleCellSelection;
import org.corpus_tools.atomic.tagset.api.Tagset;
import org.corpus_tools.atomic.tagset.api.TagsetAwareEditor;
import org.corpus_tools.atomic.grideditor.data.AnnotationGridDataProvider;
import org.corpus_tools.atomic.grideditor.data.GridColumnHeaderDataProvider;
import org.corpus_tools.atomic.grideditor.data.GridRowHeaderDataProvider;
import org.corpus_tools.atomic.grideditor.data.annotationgrid.AnnotationGrid;
import org.corpus_tools.atomic.grideditor.layers.CustomGridLayer;
import org.corpus_tools.atomic.grideditor.menu.GridPopupMenuConfiguration;
import org.corpus_tools.salt.common.SDocumentGraph;
import org.corpus_tools.salt.common.SSpan;
import org.corpus_tools.salt.common.SToken;
import org.corpus_tools.salt.core.SAnnotation;
import org.corpus_tools.salt.core.SNode;
import org.corpus_tools.salt.core.SRelation;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.PartInitException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
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.GridLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.RowHeaderLayer;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
import org.eclipse.nebula.widgets.nattable.layer.SpanningDataLayer;
import org.eclipse.nebula.widgets.nattable.layer.cell.ColumnOverrideLabelAccumulator;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.command.SelectCellCommand;
import org.eclipse.nebula.widgets.nattable.selection.event.ISelectionEvent;
import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;



/**
 * // TODO Add description
 *
 * @author Stephan Druskat <[mail@sdruskat.net](mailto:mail@sdruskat.net)>
 * 
 */
public class GridEditor extends DocumentGraphEditor implements ISelectionProvider, TagsetAwareEditor {

	private static final Logger log = LogManager.getLogger(GridEditor.class);
	private AnnotationGridDataProvider dataProvider = null;
	private AnnotationGrid annotationGrid;
	
	private ListenerList<ISelectionChangedListener> selectionListeners = new ListenerList<>();
	private NatTable natTable;
	public Tagset tagset = null;
	public boolean hasTagset = false;
	
	
	public GridEditor() {
		super();
//		colorManager = new ColorManager();
	}
	
	@Override
	public void init(IEditorSite site, IEditorInput input) throws PartInitException {
		super.init(site, input);
		if (isInputValid()) {
			annotationGrid = compileAnnotationGrid(graph);
		}
		this.tagset = loadTagset(input);
		if (tagset != null) {
			log.info("Set tagset for editor to {}.", tagset.getName());
			hasTagset = true;
		}
		else {
			log.info("No tagset found for this document.");
		}
	}
	
	@Override
	public void dispose() {
		super.dispose();
		getSite().setSelectionProvider(null);
	}
	
	@Override
	public void createEditorPartControl(Composite parent) {
		parent.setLayout(new GridLayout());
		
		/* ############################################
		 * Grid
		 * ############################################
		 */
		dataProvider = createDataProvider();
		GridSpanningDataProvider spanningProvider = new GridSpanningDataProvider(dataProvider, false, true);
		SpanningDataLayer bodyDataLayer = new SpanningDataLayer(spanningProvider);
		/* 
		 * Register the accumulator on the body data layer.
		 * This ensures that the labels are bound to the column index and are
		 * unaffected by column order.
		 * The Accumulator registers label constants for all columns,
		 * which makes them addressable for column-based customizations.
		 */
		final ColumnOverrideLabelAccumulator columnLabelAccumulator = new CustomGridLabelAccumulator(bodyDataLayer, spanningProvider, tagset, annotationGrid);
        bodyDataLayer.setConfigLabelAccumulator(columnLabelAccumulator);
//        final ColumnOverrideLabelAccumulator columnLabelAccumulator = new ColumnOverrideLabelAccumulator(bodyDataLayer);
//        bodyDataLayer.setConfigLabelAccumulator(columnLabelAccumulator);
        // Selection layer
		final SelectionLayer selectionLayer = new SelectionLayer(bodyDataLayer, false);
		selectionLayer.addConfiguration(new GridEditorSelectionConfiguration(annotationGrid));
//		final ISelectionModel selectionModel = selectionLayer.getSelectionModel();
		ViewportLayer viewportLayer = new ViewportLayer(selectionLayer);
		
		// Column header layer stack
		IDataProvider colHeaderDataProvider = new GridColumnHeaderDataProvider(annotationGrid);
		DataLayer colHeaderDataLayer = new DataLayer(colHeaderDataProvider);
		ILayer columnHeaderLayer = new ColumnHeaderLayer(colHeaderDataLayer, viewportLayer, selectionLayer);

		// Row header layer stack
		IDataProvider rowHeaderDataProvider = new GridRowHeaderDataProvider(annotationGrid);
		DataLayer rowHeaderDataLayer = new DataLayer(rowHeaderDataProvider);
		ILayer rowHeaderLayer = new RowHeaderLayer(rowHeaderDataLayer, viewportLayer, selectionLayer);
//
		// Corner layer
		ILayer cornerLayer = new CornerLayer(new DataLayer(new DefaultCornerDataProvider(colHeaderDataProvider, rowHeaderDataProvider)), rowHeaderLayer, columnHeaderLayer);

		GridLayer compositeLayer = new CustomGridLayer(viewportLayer, columnHeaderLayer, rowHeaderLayer, cornerLayer);

		natTable = new NatTable(parent, SWT.NO_BACKGROUND
	            | SWT.NO_REDRAW_RESIZE | SWT.DOUBLE_BUFFERED | SWT.V_SCROLL
	            | SWT.H_SCROLL | SWT.BORDER, compositeLayer, false);
		GridDataFactory.fillDefaults().grab(true, true).applyTo(natTable);

		
		natTable.addConfiguration(new GridPopupMenuConfiguration(natTable, annotationGrid, selectionLayer));
//		natTable.addConfiguration(new TokenEditorKeyConfiguration(text));
		natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
		natTable.addConfiguration(new GridEditorConfiguration(this, columnLabelAccumulator));
		natTable.configure();

		/* 
		 * Necessary to select the first cell, activated for 
		 * keyboard navigation!
		 * Cf. https://www.eclipse.org/forums/index.php?t=msg&th=1083775&goto=1752061&#msg_1752061
		 */
		natTable.addPaintListener(new PaintListener() {
		    @Override
		    public void paintControl(PaintEvent e) {
		        natTable.setFocus();
		        natTable.doCommand(new SelectCellCommand(selectionLayer, 0, 0, false, false));
		        natTable.removePaintListener(this);
		    }
		});
		
		/* #################################
		 * Selection
		 * #################################
		 */
		selectionLayer.addLayerListener(new ILayerListener() {
			
			@Override
			public void handleLayerEvent(ILayerEvent event) {
				if (event instanceof ISelectionEvent) {
					Collection<ILayerCell> selectedCells = ((ISelectionEvent) event).getSelectionLayer().getSelectedCells();
					if (selectedCells.size() == 1) {
Answer: 						setSelection(new SingleCellSelection(selectedCells.iterator().next()));