/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.ui.operation;
import com.lightcrafts.model.Operation;
import com.lightcrafts.ui.mode.Mode;
import static com.lightcrafts.ui.operation.Locale.LOCALE;
import com.lightcrafts.utils.xml.XMLException;
import com.lightcrafts.utils.xml.XmlDocument;
import com.lightcrafts.utils.xml.XmlNode;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoableEditSupport;
import java.awt.*;
import java.util.LinkedList;
import java.util.List;
/** This is the base class for all the Operation controls in an OpStack.
* <p>
* Important features required in derived classes include:
* <li>
* <ul>Support for <code>save(XmlNode)</code> and
* <code>restore(XmlNode)</code>, used for serialization and also undo.</ul>
* <ul>Support for operation changes as notified through operationChanged(),
* necessary for undo.</ul>
* </li>
*/
public abstract class OpControl extends SelectableControl {
protected OpControlUndoSupport undoSupport;
private Operation operation;
private boolean isActive;
private boolean isLocked;
private LockedOverlay lockOver;
private OpFooter footer;
private OpControlEdit currentEdit;
private List<OpControlModeListener> modeListeners;
protected OpControl(Operation operation, OpStack stack) {
this.operation = operation;
undoSupport = new OpControlUndoSupport();
modeListeners = new LinkedList<OpControlModeListener>();
remove(title);
title = new OpTitle(this, stack);
title.setBackground(Background);
title.setFont(ControlFont);
add(title);
maybeSetEngineDeactivatable();
isActive = true;
isLocked = false;
lockOver = new LockedOverlay();
final List layerModes = stack.getLayerModes();
footer = new OpFooter(this, layerModes);
if (hasFooter())
add(footer);
String name = OpActions.getName(operation);
if (! isSingleton()) {
name = stack.getNextUniqueName(name);
}
title.setTitleText(name);
}
// Derived classes must call readyForUndo() when they're ready for their
// first undo snapshot, probably at the end of their constructors:
protected void readyForUndo() {
undoSupport.initialize();
}
void setActivated(boolean active) {
operation.setActivated(active);
((OpTitle) title).setActive(active);
isActive = active;
if (isActive) {
undoSupport.postEdit(
LOCALE.get("EnableEditName") + ' ' + title.getTitleText(), true
);
}
else {
undoSupport.postEdit(
LOCALE.get("DisableEditName") + ' ' + title.getTitleText(), true
);
}
}
boolean isActivated() {
return isActive;
}
String getTitleText() {
return title.getTitleText();
}
public void setLocked(boolean locked) {
final boolean wasLocked = isLocked;
if (locked) {
add(lockOver, PALETTE_LAYER);
title.addLock();
}
else {
remove(lockOver);
title.removeLock();
if (! isContentVisible) {
title.doExpand();
}
}
isLocked = locked;
if (isLocked != wasLocked) {
if (isLocked) {
undoSupport.postEdit(
LOCALE.get("LockEditName") + ' ' + title.getTitleText(),
false
);
}
else {
undoSupport.postEdit(
LOCALE.get("UnlockEditName") + ' ' + title.getTitleText(),
false
);
}
title.findOpStack().notifyLockChanged(this);
revalidate();
repaint();
}
}
public boolean isLocked() {
return isLocked;
}
public boolean isSingleton() {
return operation.isSingleton();
}
public boolean hasFooter() {
return operation.hasFooter();
}
public boolean isRawCorrection() {
return title.getTitleText().startsWith("RAW");
}
public Operation getOperation() {
return operation;
}
public void addModeListener(OpControlModeListener listener) {
modeListeners.add(listener);
}
public void removeModeListener(OpControlModeListener listener) {
modeListeners.remove(listener);
}
public void notifyListenersEnterMode(Mode mode) {
for (OpControlModeListener listener : modeListeners) {
listener.enterMode(mode);
}
}
public void notifyListenersExitMode(Mode mode) {
for (OpControlModeListener listener : modeListeners) {
listener.exitMode(mode);
}
}
void setRegionInverted(boolean inverted) {
operation.setRegionInverted(inverted);
}
/** Derived classes must be able to handle Operation changes at any time.
* When the Operation changes, current Operation settings
* must be copied into the new Operation.
* <p>
* The framework promises that the class of the given Operation will equal
* the class of the Operation passed in the constructor.
*/
protected void operationChanged(Operation operation) {
maybeSetEngineDeactivatable();
footer.operationChanged(operation);
}
// To support undo, the OpStack must be able to replace this OpControl's
// Operation. Engines don't allow Operations to be recycled, but we must
// recycle OpControls because references to them are held by the undo
// edits.
void setOperation(Operation operation) {
final Class oldClass = this.operation.getClass();
final Class newClass = operation.getClass();
if (! oldClass.equals(newClass)) {
throw new RuntimeException(
"Incompatible Operation change: " +
"was " + oldClass + ", is " + newClass
);
}
this.operation = operation;
operation.setActivated(isActive);
operationChanged(operation);
}
// Restore to presets, and capture the change as an undoable edit.
void restorePresets(XmlNode presets) throws XMLException {
restore(presets);
undoSupport.postEdit(LOCALE.get("ApplyPresetEditName"));
}
public void doLayout() {
super.doLayout();
// Cram the layer controls under the content:
if (isContentVisible) {
final Point contentLoc = content.getLocation();
final Dimension contentSize = content.getSize();
final Dimension layerSize =
(! isSingleton() && hasFooter()) ? footer.getPreferredSize() : new Dimension();
contentSize.height -= layerSize.height;
content.setSize(contentSize);
final Dimension size = getSize();
final Insets insets = getInsets();
final int minX = insets.left;
final int maxX = size.width - insets.right;
if (! isSingleton() && hasFooter()) {
footer.setLocation(
minX, contentLoc.y + contentSize.height
);
footer.setSize(maxX - minX, layerSize.height);
}
if (isLocked) {
final Rectangle lockBounds = new Rectangle(
insets.left,
contentLoc.y,
maxX - minX,
contentSize.height + layerSize.height
);
lockOver.setBounds(lockBounds);
}
}
}
public Dimension getPreferredSize() {
final Dimension size = super.getPreferredSize();
if (isContentVisible && ! isSingleton() && hasFooter()) {
size.height += footer.getPreferredSize().height;
}
return size;
}
public boolean isSwappable() {
return ! operation.isSingleton();
}
private final static String ActiveTag = "Active";
private final static String LockedTag = "Locked";
/**
* Store state under the given XmlDocument.XmlNode.
*/
public void save(XmlNode node) {
footer.save(node);
title.save(node);
node.setAttribute(ActiveTag, Boolean.toString(isActive));
node.setAttribute(LockedTag, Boolean.toString(isLocked));
}
/**
* Reinitialize from data under the given XmlDocument.XmlNode.
*/
public void restore(XmlNode node) throws XMLException {
undoSupport.restoreStart();
footer.restore(node);
title.restore(node);
maybeSetEngineDeactivatable();
if (node.hasAttribute(ActiveTag)) {
final boolean active = Boolean.valueOf(node.getAttribute(ActiveTag));
setActivated(active);
// Default to isActive==true is OK.
}
if (node.hasAttribute(LockedTag)) {
final boolean locked = Boolean.valueOf(node.getAttribute(LockedTag));
if (locked) {
// Don't call setLocked(false), because that expands the tool.
setLocked(locked);
}
// Default to isLocked==false is OK.
}
undoSupport.restoreEnd();
}
public void addUndoableEditListener(UndoableEditListener listener) {
undoSupport.addUndoableEditListener(listener);
}
public void removeUndoableEditListener(UndoableEditListener listener) {
undoSupport.removeUndoableEditListener(listener);
}
// Whenever an Operation is created, check whether the tool title is one
// of the kind that should not be deactivated by the "show original"
// button.
private void maybeSetEngineDeactivatable() {
if (isRawCorrection() || isSingleton()) {
// Sign of a special tool, decouple it from the "original" button:
operation.setEngineDeActivatable(false);
}
}
/**
* Extend UndoableEditSupport to be aware when we are doing restore,
* so it won't broadcast the change batches that happen then.
*/
public class OpControlUndoSupport extends UndoableEditSupport {
private boolean restoring;
// This method is called from readyForUndo() to indicate that this
// OpControl is ready for its first state snapshot by OpControlEdit:
public void initialize() {
currentEdit = new OpControlEdit();
}
public void postEdit(String name, boolean significant) {
if ((! restoring) && (currentEdit != null)) {
currentEdit.end(name);
currentEdit.setSignificance(significant);
postEdit(currentEdit);
currentEdit = new OpControlEdit();
}
}
public void postEdit(String name) {
if ((! restoring) && (currentEdit != null)) {
currentEdit.end(name);
postEdit(currentEdit);
currentEdit = new OpControlEdit();
}
}
public void restoreStart() {
restoring = true;
}
public void restoreEnd() {
restoring = false;
}
public boolean isRestoring() {
return restoring;
}
}
/** A state preserve/restore edit, like StateEdit but with XmlDocuments
* instead of Hashtables.
*/
private final class OpControlEdit extends AbstractUndoableEdit {
private XmlDocument beforeDoc = new XmlDocument("before");
private XmlDocument afterDoc = new XmlDocument("after");
private String name;
private boolean isSignificant = true;
private OpControlEdit() {
save(beforeDoc.getRoot());
}
private void end(String name) {
this.name = name;
save(afterDoc.getRoot());
}
private void setSignificance(boolean significant) {
isSignificant = significant;
}
public String getPresentationName() {
return name;
}
public boolean isSignificant() {
return isSignificant;
}
public void undo() {
super.undo();
try {
restore(beforeDoc.getRoot());
}
catch (XMLException e) {
final CannotUndoException cue = new CannotUndoException();
cue.initCause(e);
throw cue;
}
currentEdit = new OpControlEdit();
}
public void redo() {
super.redo();
try {
restore(afterDoc.getRoot());
}
catch (XMLException e) {
final CannotRedoException cre = new CannotRedoException();
cre.initCause(e);
throw cre;
}
currentEdit = new OpControlEdit();
}
}
}