/*******************************************************************************
* Copyright (c) 2014, 2016 itemis AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Alexander Nyßen (itemis AG) - initial API and implementation
*
*******************************************************************************/
package org.eclipse.gef.mvc.fx.domain;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.operations.IOperationHistory;
import org.eclipse.core.commands.operations.IOperationHistoryListener;
import org.eclipse.core.commands.operations.IUndoContext;
import org.eclipse.core.commands.operations.IUndoableOperation;
import org.eclipse.core.commands.operations.OperationHistoryEvent;
import org.eclipse.core.commands.operations.UndoContext;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.gef.common.activate.ActivatableSupport;
import org.eclipse.gef.common.activate.IActivatable;
import org.eclipse.gef.common.adapt.AdaptableSupport;
import org.eclipse.gef.common.adapt.AdapterKey;
import org.eclipse.gef.common.adapt.inject.InjectAdapters;
import org.eclipse.gef.mvc.fx.gestures.IGesture;
import org.eclipse.gef.mvc.fx.operations.AbstractCompositeOperation;
import org.eclipse.gef.mvc.fx.operations.ForwardUndoCompositeOperation;
import org.eclipse.gef.mvc.fx.operations.ITransactionalOperation;
import org.eclipse.gef.mvc.fx.operations.ReverseUndoCompositeOperation;
import org.eclipse.gef.mvc.fx.viewer.IViewer;
import com.google.common.reflect.TypeToken;
import com.google.inject.Inject;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyMapProperty;
import javafx.collections.ObservableMap;
/**
* The {@link HistoricizingDomain} is an {@link IDomain} that uses an
* {@link IOperationHistory} for executing {@link ITransactionalOperation
* ITransactionalOperations}.
*
* @author anyssen
*/
public class HistoricizingDomain implements IDomain {
private static final int DEFAULT_UNDO_LIMIT = 128;
private static final UncaughtExceptionHandler UNCAUGHT_EXCEPTION_HANDLER = new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
e.printStackTrace();
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
} else {
throw new RuntimeException(e);
}
}
};
private ActivatableSupport acs = new ActivatableSupport(this);
private AdaptableSupport<HistoricizingDomain> ads = new AdaptableSupport<>(
this);
private IOperationHistory operationHistory;
private IUndoContext undoContext;
private AbstractCompositeOperation transaction;
private Set<IGesture> transactionContext = new HashSet<>();
private IOperationHistoryListener transactionListener = new IOperationHistoryListener() {
@Override
public void historyNotification(OperationHistoryEvent event) {
if (event.getEventType() == OperationHistoryEvent.ABOUT_TO_UNDO) {
if (!transactionContext.isEmpty() && transaction != null) {
if (transaction.getOperations().isEmpty()) {
for (IGesture tool : transactionContext) {
closeExecutionTransaction(tool);
}
} else {
throw new IllegalStateException(
"Cannot perform UNDO while a currently open execution transaction contains operations.");
}
}
}
}
};
/**
* Creates a new {@link HistoricizingDomain} instance.
*/
public HistoricizingDomain() {
// ensure uncaught exception handler is used
Thread.currentThread()
.setUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER);
}
@Override
public final void activate() {
acs.activate(null, this::doActivate);
}
/**
* Activates the adapters registered at this {@link HistoricizingDomain}.
*/
protected void activateAdapters() {
// XXX: We keep a sorted map of adapters so activation
// is performed in a deterministic order
new TreeMap<>(ads.getAdapters()).values().forEach((adapter) -> {
if (adapter instanceof IActivatable) {
((IActivatable) adapter).activate();
}
});
}
@Override
public final ReadOnlyBooleanProperty activeProperty() {
return acs.activeProperty();
}
@Override
public ReadOnlyMapProperty<AdapterKey<?>, Object> adaptersProperty() {
return ads.adaptersProperty();
}
/**
* Applies the undo context to the given operation. May be overwritten by
* clients to filter out operations that should not be undoable in the given
* context.
*
* @param operation
* The {@link ITransactionalOperation} to apply the
* {@link #getUndoContext()} to.
*/
protected void applyUndoContext(ITransactionalOperation operation) {
// if (operation.isContentRelevant()) {
operation.addContext(getUndoContext());
// }
}
@Override
public void closeExecutionTransaction(IGesture tool) {
// if (!transactionContext.contains(tool)) {
// throw new IllegalStateException(
// "No transaction active for tool " + tool + ".");
// }
if (!transactionContext.contains(tool)) {
return; // TODO: is this legal?
}
// remove tool from the transaction context and close the transaction in
// case the tool was the last one
if (transactionContext.size() == 1
&& transactionContext.contains(tool)) {
// Close transaction by adding it to the operation history in case
// it has an effect; all its nested operations have already been
// executed, thus it does not have to be executed again
if (transaction == null) {
throw new IllegalStateException(
"No transaction is currently active, while the transaction context sill contained tool "
+ tool + ".");
}
List<ITransactionalOperation> operations = transaction
.getOperations();
if (!operations.isEmpty()) {
// use the concatenation of the operations' labels as the
// transaction label
StringBuffer label = new StringBuffer();
int operationCount = operations.size();
for (int i = 0; i < operationCount; i++) {
label.append(operations.get(i).getLabel());
if (operations.size() - 1 > i) {
label.append(", ");
}
}
transaction.setLabel(label.toString());
// only add undo context if we have a content related change
applyUndoContext(transaction);
getOperationHistory().add(transaction);
}
transaction = null;
}
transactionContext.remove(tool);
}
/**
* Creates a {@link ForwardUndoCompositeOperation} which is used to store
* the operations within an execution transaction. The operation is opened
* on the {@link #getOperationHistory() operation history}.
*
* @return A new {@link ForwardUndoCompositeOperation} which is configured
* to store the operations within an execution transaction.
*/
protected AbstractCompositeOperation createExecutionTransaction() {
ReverseUndoCompositeOperation transaction = new ReverseUndoCompositeOperation(
Long.toString(System.currentTimeMillis()));
return transaction;
}
@Override
public final void deactivate() {
acs.deactivate(this::doDeactivate, null);
}
/**
* Deactivates the adapters registered at this {@link HistoricizingDomain}.
*/
protected void deactivateAdapters() {
// XXX: We keep a sorted map of adapters so deactivation
// is performed in a deterministic order
new TreeMap<>(ads.getAdapters()).values().forEach((adapter) -> {
if (adapter instanceof IActivatable) {
((IActivatable) adapter).deactivate();
}
});
}
@Override
public void dispose() {
// dispose transaction related objects
operationHistory.removeOperationHistoryListener(transactionListener);
transactionListener = null;
transactionContext.clear();
transactionContext = null;
transaction = null;
// dispose operation history and undo context
operationHistory.dispose(undoContext, true, true, true);
operationHistory = null;
undoContext = null;
// dispose adaptable and activatable support
ads.dispose();
ads = null;
acs = null;
}
/**
* Activates this {@link HistoricizingDomain}, which activates its adapters.
*/
protected void doActivate() {
activateAdapters();
}
/**
* Deactivates this {@link HistoricizingDomain}, which deactivates its
* adapters.
*/
protected void doDeactivate() {
deactivateAdapters();
}
/**
* {@inheritDoc}
*
* In case an execution transaction is currently open (see
* {@link #openExecutionTransaction(IGesture)},
* {@link #closeExecutionTransaction(IGesture)}) the enclosing transaction will
* refer to the {@link IUndoContext} used by this {@link IDomain}) (so that
* no specific {@link IUndoContext} is set on the passed in
* {@link IUndoableOperation}). If no transaction is currently open, the
* {@link IUndoContext} of this {@link IDomain} will be set on the passed in
* {@link IUndoableOperation}.
*/
@Override
public void execute(ITransactionalOperation operation,
IProgressMonitor monitor) throws ExecutionException {
// reduce composite operations
if (operation instanceof AbstractCompositeOperation) {
operation = ((AbstractCompositeOperation) operation).unwrap(true);
}
// do not execute NoOps
if (operation == null || operation.isNoOp()) {
return;
}
// check if we can execute operation
if (!operation.canExecute()) {
throw new IllegalArgumentException("Operation cannot be executed.");
}
if (transaction != null) {
// execute operation locally and add it to the current transaction
operation.execute(monitor, null);
transaction.add(operation);
} else {
// execute operation directly on operation history
applyUndoContext(operation);
getOperationHistory().execute(operation, monitor, null);
}
}
@Override
public <T> T getAdapter(AdapterKey<T> key) {
return ads.getAdapter(key);
}
@Override
public <T> T getAdapter(Class<T> classKey) {
return ads.<T> getAdapter(classKey);
}
@Override
public <T> T getAdapter(TypeToken<T> key) {
return ads.getAdapter(key);
}
@Override
public <T> AdapterKey<T> getAdapterKey(T adapter) {
return ads.getAdapterKey(adapter);
}
@Override
public ObservableMap<AdapterKey<?>, Object> getAdapters() {
return ads.getAdapters();
}
@Override
public <T> Map<AdapterKey<? extends T>, T> getAdapters(
Class<? super T> classKey) {
return ads.getAdapters(classKey);
}
@Override
public <T> Map<AdapterKey<? extends T>, T> getAdapters(
TypeToken<? super T> key) {
return ads.getAdapters(key);
}
/**
* Returns the {@link IOperationHistory} used by this
* {@link HistoricizingDomain} to execute transactions.
*
* @return The {@link IOperationHistory}.
*/
public IOperationHistory getOperationHistory() {
return operationHistory;
}
@Override
public Map<AdapterKey<? extends IGesture>, IGesture> getTools() {
return ads.getAdapters(IGesture.class);
}
/**
* Returns the {@link UndoContext} that is used by this domain to execute
* transactions.
*
* @return The {@link UndoContext}.
*/
public IUndoContext getUndoContext() {
return undoContext;
}
@Override
public Map<AdapterKey<? extends IViewer>, IViewer> getViewers() {
return ads.getAdapters(IViewer.class);
}
@Override
public final boolean isActive() {
return acs.isActive();
}
/**
* Returns <code>true</code> if an execution transaction is currently open.
* Otherwise returns <code>false</code>.
*
* @return <code>true</code> if an execution transaction is currently open,
* otherwise <code>false</code>.
*/
protected boolean isExecutionTransactionOpen() {
return transaction != null;
}
@Override
public boolean isExecutionTransactionOpen(IGesture tool) {
return transactionContext.contains(tool);
}
@Override
public void openExecutionTransaction(IGesture tool) {
// if (transactionContext.contains(tool)) {
// throw new IllegalStateException(
// "A transaction is already active for tool " + tool + ".");
// }
// Create a new transaction in case the tool is the first one to open a
// transaction.
if (transactionContext.contains(tool)) {
return; // TODO: is this legal??
}
transactionContext.add(tool);
if (transactionContext.size() == 1
&& transactionContext.contains(tool)) {
if (transaction != null) {
throw new IllegalStateException(
"A transaction is already active, while this is the first tool within the transaction context.");
}
transaction = createExecutionTransaction();
}
}
@Override
public <T> void setAdapter(T adapter) {
ads.setAdapter(adapter);
}
@Override
public <T> void setAdapter(T adapter, String role) {
ads.setAdapter(adapter, role);
}
@Override
public <T> void setAdapter(TypeToken<T> adapterType, T adapter) {
ads.setAdapter(adapterType, adapter);
}
@InjectAdapters
@Override
public <T> void setAdapter(TypeToken<T> adapterType, T adapter,
String role) {
ads.setAdapter(adapterType, adapter, role);
}
/**
* Sets the {@link IOperationHistory} that is used by this
* {@link HistoricizingDomain} to the given value. Operation history
* listeners are un-/registered accordingly.
*
* @param operationHistory
* The new {@link IOperationHistory} for this domain.
*/
@Inject
public void setOperationHistory(IOperationHistory operationHistory) {
if (this.operationHistory != null
&& this.operationHistory != operationHistory) {
this.operationHistory
.removeOperationHistoryListener(transactionListener);
}
if (this.operationHistory != operationHistory) {
this.operationHistory = operationHistory;
if (this.operationHistory != null) {
this.operationHistory
.addOperationHistoryListener(transactionListener);
if (undoContext != null) {
this.operationHistory.setLimit(undoContext,
DEFAULT_UNDO_LIMIT);
}
}
}
}
/**
* Sets the {@link IUndoContext} that is used by this
* {@link HistoricizingDomain} to the given value.
*
* @param undoContext
* The new {@link IUndoContext} for this domain.
*/
@Inject
public void setUndoContext(IUndoContext undoContext) {
this.undoContext = undoContext;
if (operationHistory != null && undoContext != null) {
operationHistory.setLimit(undoContext, DEFAULT_UNDO_LIMIT);
}
}
@Override
public <T> void unsetAdapter(T adapter) {
ads.unsetAdapter(adapter);
}
}