/* Copyright (C) 2005-2011 Fabio Riccardi */
package com.lightcrafts.ui.editor;
import com.lightcrafts.model.CropBounds;
import com.lightcrafts.model.Operation;
import com.lightcrafts.platform.Platform;
import com.lightcrafts.ui.crop.CropListener;
import com.lightcrafts.ui.crop.CropMode;
import com.lightcrafts.ui.mode.AbstractMode;
import com.lightcrafts.ui.mode.Mode;
import com.lightcrafts.ui.mode.ModeOverlay;
import com.lightcrafts.ui.operation.OpControl;
import com.lightcrafts.ui.operation.OpControlModeListener;
import com.lightcrafts.ui.operation.OpStackListener;
import com.lightcrafts.ui.operation.SelectableControl;
import com.lightcrafts.ui.region.RegionOverlay;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
/**
* Handle switching among all the Modes. Lots of special mode-transition
* logic lives here, plus the affine transform updates for Modes.
* <p>
* ModeManager sets and resets the crop when CropMode starts and ends.
* The pan mode is entered on a key press and exited on release. The
* temporary Modes defined by OpControls are added and removed here too.
*/
public class ModeManager
implements OpStackListener, OpControlModeListener, XFormListener
{
// Look for the special key events to enter and exit the pan mode,
// taking care to filter out auto-repeat events:
private static int PanKeyCode = KeyEvent.VK_SPACE;
private KeyEventPostProcessor panModeKeyProcessor =
new KeyEventPostProcessor() {
private boolean isPanMode;
public boolean postProcessKeyEvent(KeyEvent e) {
final boolean wasPanMode = isPanMode;
if (e.getKeyCode() == PanKeyCode) {
if (e.getID() == KeyEvent.KEY_PRESSED) {
isPanMode = true;
}
if (e.getID() == KeyEvent.KEY_RELEASED) {
if (Platform.getType() != Platform.MacOSX) {
// Detect and ignore auto-repeat release events
final boolean isReallyPressed =
Platform.getPlatform().isKeyPressed(
PanKeyCode
);
isPanMode = isReallyPressed;
}
else {
isPanMode = false;
}
}
}
if (isPanMode && ! wasPanMode) {
overlay.pushMode(transientPanMode);
}
if (wasPanMode && ! isPanMode) {
overlay.popMode();
}
return false; // these key events have other interpretations
}
};
private Document doc;
private ModeOverlay overlay;
private AffineTransform xform;
private Mode regionMode;
private CropMode cropMode;
private AbstractMode transientPanMode; // works with other modes (space key)
private AbstractMode permanentPanMode; // present in "no mode" (arrow cursor)
private CropMode rotateMode;
// The "extras" component for the region mode
private JComponent regionExtras;
// If someone clicks "Commit" in crop mode, we must update the mode buttons
private ModeButtons modeButtons;
// OpControls can ask for short-term Mode push/pops:
private OpControl control; // add and remove the listener
private Mode controlMode; // update the AffineTransform
// Keep track of where the underlay is, so the crop overlay can be
// initialized the first time it's entered.
private Rectangle underlayBounds;
ModeManager(
Mode regionMode,
CropMode cropMode,
AbstractMode transientPanMode,// After spacebar
AbstractMode permanentPanMode,// Always-on
CropMode rotateMode,
ModeOverlay overlay,
Component underlay,
JComponent regionExtras, // The curve-type buttons
Document doc // For zoom-to-fit around the crop mode
) {
this.overlay = overlay;
this.regionExtras = regionExtras;
this.doc = doc;
xform = new AffineTransform();
this.regionMode = regionMode;
this.cropMode = cropMode;
this.transientPanMode = transientPanMode;
this.permanentPanMode = permanentPanMode;
this.rotateMode = rotateMode;
// The AffineTransform in the Modes sometimes depends on the size
// of the ModeOverlay:
overlay.addComponentListener(
new ComponentAdapter() {
public void componentResized(ComponentEvent event) {
setTransform(xform);
}
}
);
// Modes also require notification about the location and size of
// their underlay:
underlay.addComponentListener(
new ComponentAdapter() {
public void componentResized(ComponentEvent event) {
Rectangle bounds = event.getComponent().getBounds();
setUnderlayBounds(bounds);
}
public void componentMoved(ComponentEvent event) {
Rectangle bounds = event.getComponent().getBounds();
setUnderlayBounds(bounds);
}
}
);
// The crop and rotate modes likes to end themselves:
cropMode.addCropListener(
new CropListener() {
public void cropCommitted(CropBounds bounds) {
// This may be called because someone already switched
// away from the crop mode, but it may also be called
// from the "Commit" crop mode popup menu item, in which
// case we must handle the mode switch ourselves.
EventQueue.invokeLater(
new Runnable() {
public void run() {
if (modeButtons != null) {
if (modeButtons.isCropSelected()) {
setEditorMode( EditorMode.ARROW );
}
}
}
}
);
}
public void unCrop() {
}
}
);
rotateMode.addCropListener(
new CropListener() {
public void cropCommitted(CropBounds bounds) {
// This may be called because someone already switched
// away from the rotate mode, but it may also be called
// from the "Commit" crop mode popup menu item, in which
// case we must handle the mode switch ourselves.
EventQueue.invokeLater(
new Runnable() {
public void run() {
if (modeButtons != null) {
if (modeButtons.isRotateSelected()) {
setEditorMode( EditorMode.ARROW );
}
}
}
}
);
}
public void unCrop() {
}
}
);
registerPanModeListener();
setMode(this.permanentPanMode);
}
public EditorMode getMode() {
return m_editorMode;
}
void setModeButtons(ModeButtons buttons) {
modeButtons = buttons;
}
// Start OpStackListener:
public void opAdded(OpControl control) {
}
public void opChanged(OpControl control) {
if (isControlModeActive()) {
exitMode(null);
}
if (this.control != null) {
this.control.removeModeListener(this);
} else
setEditorMode( EditorMode.ARROW );
this.control = control;
if (control != null && ! control.isLocked()) {
control.addModeListener(this);
}
// Some OpControls don't get regions.
final boolean isLocked = (control != null) && control.isLocked();
final boolean isRaw = (control != null) && control.isRawCorrection();
if (isLocked || isRaw) {
modeButtons.setRegionsEnabled(false);
} else {
modeButtons.setRegionsEnabled(true);
}
if ( control != null ) {
final Operation op = control.getOperation();
final EditorMode prefMode = op.getPreferredMode();
if ( isLocked || isRaw ||
prefMode != EditorMode.ARROW ||
m_editorMode == EditorMode.ARROW )
setEditorMode( prefMode );
}
}
public void opChanged(SelectableControl control) {
if (this.control != null) {
this.control.removeModeListener(this);
this.control = null;
}
if ( control instanceof ProofSelectableControl )
setMode( null );
if ( control == null )
setMode( null );
}
public void opLockChanged(OpControl control) {
if (this.control == control) {
opChanged(control);
}
}
public void opRemoved(OpControl control) {
// Since region mode interacts with the tool stack selection,
// be sure to pop out of that mode if it's the current mode at
// the time a tool is deleted.
if (overlay.peekMode() == regionMode) {
if (this.control != null) {
Operation op = this.control.getOperation();
if (op.getPreferredMode() != EditorMode.REGION) {
EventQueue.invokeLater(
new Runnable() {
public void run() {
setEditorMode( EditorMode.ARROW );
}
}
);
}
}
}
}
// End OpStackListener.
public void setEditorMode( EditorMode mode ) {
if ( mode == m_editorMode )
return;
if ( modeButtons != null )
switch ( mode ) {
case ARROW:
modeButtons.clickNoMode();
break;
case CROP:
modeButtons.clickCropButton();
break;
case REGION:
modeButtons.clickRegionButton();
break;
case ROTATE:
modeButtons.clickRotateButton();
}
m_editorMode = mode;
}
// Start OpControlModeListener:
public void enterMode(Mode mode) {
mode.enter();
assert (! isControlModeActive());
controlMode = mode;
controlMode.setTransform(getOverlayTransform());
overlay.pushMode(controlMode);
}
public void exitMode(Mode mode) {
assert isControlModeActive();
if ( mode != null )
mode.exit();
overlay.popMode();
controlMode = null;
}
// End OpControlModeListener.
// Start XFormListener:
public void xFormChanged(AffineTransform xform) {
// First, validate from the overlay's validate root, because the
// overlay bounds partly determine the overlay affine transform.
JScrollPane scroll = (JScrollPane) SwingUtilities.getAncestorOfClass(
JScrollPane.class, overlay
);
if (scroll != null){
overlay.invalidate(); // mark the overay as needing to update
scroll.validate(); // update the layout under the scroll pane
}
setTransform(xform);
}
// End XFormListener.
private boolean isControlModeActive() {
return (controlMode != null) && overlay.peekMode().equals(controlMode);
}
private boolean isTransientPanModeActive() {
return overlay.peekMode().equals(transientPanMode);
}
private void setTransform(AffineTransform xform) {
//
// The given AffineTranform maps original image coordinates to the
// coordinates of the image component on the screen. The Modes
// require an AffineTransform mapping original image coordinates to
// coordinates of the Mode overlay component.
//
// Referring to ModeOverlay, the origin of a Mode's overlay component
// does not necessarily coincide with the origin of the image
// component. If the image is zoomed out to a size smaller than an
// enclosing scroll pane viewport, then the Mode overlay component
// will be larger than the image, filling the viewport.
//
// So, depending on this viewport condition, we may need to add a
// translation to the transform:
//
this.xform = xform;
AffineTransform overlayTransform = getOverlayTransform();
regionMode.setTransform(overlayTransform);
cropMode.setTransform(overlayTransform);
transientPanMode.setTransform(overlayTransform);
rotateMode.setTransform(overlayTransform);
if (controlMode != null) {
controlMode.setTransform(overlayTransform);
}
}
// Also used from DocPanel to forward mouse motion to the Previews.
AffineTransform getOverlayTransform() {
AffineTransform overlayXform = overlay.getTransform();
AffineTransform totalXform = (AffineTransform) xform.clone();
totalXform.preConcatenate(overlayXform);
return totalXform;
}
private void setUnderlayBounds(Rectangle bounds) {
if (bounds.equals(underlayBounds)) {
return;
}
underlayBounds = bounds;
regionMode.setUnderlayBounds(bounds);
cropMode.setUnderlayBounds(bounds);
transientPanMode.setUnderlayBounds(bounds);
rotateMode.setUnderlayBounds(bounds);
if (controlMode != null) {
controlMode.setUnderlayBounds(bounds);
}
// The default crop bounds depend on the underlay bounds.
CropBounds crop = cropMode.getCrop();
CropBounds initCrop = getInitialCropBounds();
if (crop == null) {
cropMode.setCropWithConstraints(initCrop);
}
cropMode.setResetCropBounds(initCrop);
}
JComponent setNoMode() {
boolean wasCrop =
((overlay.peekMode() == cropMode) ||
(overlay.peekMode() == rotateMode));
setMode(null);
m_editorMode = EditorMode.ARROW;
if (wasCrop) {
boolean isFitMode = doc.popFitMode();
if (! isFitMode) {
doc.zoomToFit();
}
}
return null;
}
EditorMode m_editorMode;
JComponent setCropMode() {
doc.markDirty();
setMode(cropMode);
m_editorMode = EditorMode.CROP;
doc.pushFitMode();
doc.zoomToFit();
return cropMode.getControl();
}
JComponent setRotateMode() {
doc.markDirty();
setMode(rotateMode);
m_editorMode = EditorMode.ROTATE;
doc.pushFitMode();
doc.zoomToFit();
// The "reset" button in rotate mode depends on the initial crop:
CropBounds crop = cropMode.getCrop();
rotateMode.setCrop(crop);
rotateMode.setResetCropBounds(crop);
return rotateMode.getControl().getResetButton();
}
JComponent setRegionMode() {
boolean wasCrop =
((overlay.peekMode() == cropMode) ||
(overlay.peekMode() == rotateMode));
setMode(regionMode);
m_editorMode = EditorMode.REGION;
if (wasCrop) {
boolean isFitMode = doc.popFitMode();
if (! isFitMode) {
doc.zoomToFit();
}
}
return regionExtras;
}
private void setMode(Mode newMode) {
// Peel off any temporary Modes we may have pushed.
while (isControlModeActive() || isTransientPanModeActive()) {
overlay.popMode();
controlMode = null;
}
// See if that did the trick, to avoid duplicate pushes.
Mode oldMode = overlay.peekMode();
if (oldMode == newMode) {
return;
}
// If the current Mode is one of ours, pop it before pushing the next:
if (oldMode == regionMode ||
oldMode == cropMode ||
oldMode == rotateMode) {
// If we are in region mode make sure we exit edit mode
if (oldMode == regionMode && regionMode instanceof RegionOverlay)
((RegionOverlay) regionMode).finishEditingCurve();
overlay.popMode();
// The CropModes need setup and teardown:
if ((oldMode == cropMode) || (oldMode == rotateMode)) {
CropMode crop = (CropMode) oldMode;
crop.doCrop();
}
oldMode.exit();
}
if (newMode != null) {
overlay.pushMode(newMode);
newMode.enter();
}
// The CropModes need setup and teardown:
if ((newMode == cropMode) || (newMode == rotateMode)) {
CropMode crop = (CropMode) newMode;
crop.resetCrop();
}
// The CropModes need to be kept in sync with each other:
if (oldMode == cropMode) {
rotateMode.setCrop(cropMode.getCrop());
}
if (oldMode == rotateMode) {
cropMode.setCrop(rotateMode.getCrop());
}
}
// Get a default CropBounds for the CropMode overlay, in image
// coordinates.
private CropBounds getInitialCropBounds() {
double x = underlayBounds.getX();
double y = underlayBounds.getY();
double width = underlayBounds.getWidth();
double height = underlayBounds.getHeight();
Rectangle2D inset = new Rectangle2D.Double(
x, y, width, height
);
CropBounds crop = new CropBounds(inset);
try {
AffineTransform inverse = getOverlayTransform().createInverse();
crop = CropBounds.transform(inverse, crop);
return crop;
}
catch (NoninvertibleTransformException e) {
// Leave the crop overlay uninitialized.
}
return null;
}
// Don't rely on keyboard focus to switch the pan mode.
// Instead, postprocess unclaimed key events:
private void registerPanModeListener() {
KeyboardFocusManager focus =
KeyboardFocusManager.getCurrentKeyboardFocusManager();
focus.addKeyEventPostProcessor(panModeKeyProcessor);
}
void dispose() {
KeyboardFocusManager focus =
KeyboardFocusManager.getCurrentKeyboardFocusManager();
focus.removeKeyEventPostProcessor(panModeKeyProcessor);
}
}