/*
* Copyright (C) 2011 Alasdair C. Hamilton
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package ketUI.panel;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.net.URL;
import java.util.*;
import javax.swing.JPanel;
import geom.Offset;
import geom.Position;
import ket.*;
import ket.display.*;
import ket.display.box.Box;
import ket.display.box.BoxList;
import ket.display.box.BoxText;
import ket.display.box.BoxTools;
import ket.display.box.BoxWord;
import ket.math.*;
import ket.math.purpose.Word;
import ketUI.*;
import ketUI.Document;
import ketUI.Ket;
import ketUI.MouseEventHandler;
import ketUI.chord.Chord;
import ketUI.chord.KeyboardEventHandler;
import ketUI.modes.*;
import ketUI.responder.*;
import ketUI.Ket; // NO!
/**
* The user interface panel consists of a series of box instances that each represent
* an equation or other user interface component such as a label, message or
* similar.
*/
public class KetPanel extends JPanel {
static final boolean DEBUG = false;
/**
* An estimate of half the number of equations currently visible on the
* screen used to determine the number of equations to move up/down
* when page-up/page-down is pressed.
*/
public static final int EQUATION_STEP_SIZE = 5;
Graphics2D oldGraphics = null; // HACK
public static final int LEFT_BORDER_SIZE = 15; // pixels
public static final int TOP_BORDER_SIZE = 5; // pixels
public static final int BOTTOM_BORDER_SIZE = 5; // pixels
public static final int RIGHT_BORDER_SIZE = 5; // pixels
public static final Position TOP_LEFT_BORDER = new Position(LEFT_BORDER_SIZE, TOP_BORDER_SIZE);
public static final Offset BORDER_OFFSET = new Offset(
LEFT_BORDER_SIZE+RIGHT_BORDER_SIZE,
TOP_BORDER_SIZE+BOTTOM_BORDER_SIZE);
final Document document;
final MouseEventHandler mouseEventHandler;
final PanelDecoration panelDecoration;
final Vector<UpdateListener> listeners;
final ScatterDisplay scatterDisplay;
final AnimatedDisplay animatedDisplay;
final GraphDisplay graphDisplay;
final GridDisplay gridDisplay;
final HistoryDisplay historyDisplay;
final ListDisplay listDisplay;
final NetworkDisplay networkDisplay;
final PerspectiveDisplay perspectiveDisplay;
Display display;
/**
* When animating, keep a record of the previous state before editing from which to transition.
*/
Argument before;
Equation beforeEquation;
boolean singleSelection;
Boolean showBackground;
boolean retainVisible;
Position dragPosition;
BufferedImage icon;
public KetPanel(Document document) {
this.document = document;
ResizeListener resizeListener = new ResizeListener(this);
this.addComponentListener(resizeListener);
mouseEventHandler = new MouseEventHandler(document);
this.addMouseListener(mouseEventHandler);
if (MouseEventHandler.MOUSE_MOTION) {
this.addMouseMotionListener(mouseEventHandler);
}
this.panelDecoration = new PanelDecoration(document); //! Moved above **
setFocusable(true);
MathCollection mathCollection = document.getMathCollection();
this.scatterDisplay = new ScatterDisplay(mathCollection, panelDecoration);
this.listDisplay = new ListDisplay(mathCollection);
this.gridDisplay = new GridDisplay(mathCollection);
this.animatedDisplay = new AnimatedDisplay(mathCollection);
this.networkDisplay = new NetworkDisplay(mathCollection);
this.graphDisplay = new GraphDisplay(document, mathCollection); // document is only used for font size: de-coupling.
this.historyDisplay = new HistoryDisplay(mathCollection);
this.perspectiveDisplay = new PerspectiveDisplay(mathCollection, mouseEventHandler);
display = listDisplay;
// **
beforeEquation = null;
before = null;
singleSelection = true;
// Unless set, default to the colourscheme's default choise.
showBackground = null;
retainVisible = false;
listeners = new Vector<UpdateListener>();
}
/**
* Unless explicitly set, the background is shown when specified in the colourscheme's settings file.
*/
public void toggleShowBackground() {
if (showBackground==null) {
showBackground = !getShowBackground();
} else {
showBackground = !showBackground;
}
}
public boolean getShowBackground() {
if (showBackground==null) {
return getColourScheme().getShowBackground();
} else {
return showBackground;
}
}
public void setHold(boolean hold) {
mouseEventHandler.setHold(hold);
}
public BoxList getMessageBoxList() {
return panelDecoration.getMessageBoxList(); // TODO: Simplify?
}
public void setVisibleEquations(Vector<Equation> equations) {
listDisplay.setVisibleEquations(equations); //?
}
public Vector<Equation> getVisibleEquations() {
return listDisplay.getVisibleEquations(); //?
}
public Modes getModes() {
return document.getModes();
}
public void moveViewDown() {
if (display==listDisplay) {
listDisplay.moveViewDown(); //?
} else if (display==gridDisplay) {
gridDisplay.moveViewDown();
}
setRetainVisible(); // TODO: Should this be here or in normalResponder. If this is called within another call it may affect retainVisible.
}
public void moveViewUp() {
if (display==listDisplay) {
listDisplay.moveViewUp();
} else if (display==gridDisplay) {
gridDisplay.moveViewUp();
}
setRetainVisible(); // TODO: Should this be here or in normalResponder. If this is called within another call it may affect retainVisible.
}
public void setRetainVisible() {
retainVisible = true;
}
public void ensureSelectionVisibility() {
if (retainVisible) {
retainVisible = false;
listDisplay.selectByVisible(document.getCursor(), document.getEquationList());
gridDisplay.selectByVisible(document.getCursor(), document.getEquationList());
scatterDisplay.selectByVisible(document.getCursor(), document.getEquationList()); //[new]
updateAndRepaint(); // <--- !!! Drawn twice !!!
} else {
Equation equation = document.getCursor().getEquation();
int index = equation.getEquationIndex();
viewEquation(index);
}
}
public void moveViewPageUp() {
for (int i=0; i<EQUATION_STEP_SIZE; i++) {
moveViewUp();
}
}
public void moveViewPageDown() {
for (int i=0; i<EQUATION_STEP_SIZE; i++) {
moveViewDown();
}
}
public void moveViewHalfPageDown() {
for (int i=0; i<EQUATION_STEP_SIZE/2; i++) {
moveViewDown();
}
}
public void moveViewHalfPageUp() {
for (int i=0; i<EQUATION_STEP_SIZE/2; i++) {
moveViewUp();
}
}
public PanelDecoration getPanelDecoration() {
return panelDecoration;
}
public Equation pointToEquation(Position p) {
return display.pointToEquation(p);
}
public Box findDeepestBox(Position p) {
return display.findDeepestBox(p);
}
public Argument findDeepestArgument(Position p) {
return display.findDeepestArgument(p);
}
public void fromTop() {
fromTop(document.getCursor().getEquationIndex());
}
public void fromMiddle() {
fromMiddle(document.getCursor().getEquationIndex());
}
public void fromBottom() {
fromBottom(document.getCursor().getEquationIndex());
}
public void fromTop(int initialBoxIndex) {
// <---------------------
// Why do they all keep a separate copy of super.topIndex, ... and why update animated but not network or graph display?
listDisplay.fromTop(initialBoxIndex);
animatedDisplay.fromTop();
gridDisplay.viewEquation(initialBoxIndex);
perspectiveDisplay.fromTop();
}
public void fromMiddle(int middleBoxIndex) {
listDisplay.fromMiddle(middleBoxIndex);
animatedDisplay.fromMiddle();
gridDisplay.viewEquation(middleBoxIndex); //ish
perspectiveDisplay.fromMiddle();
}
public void fromBottom(int bottomBoxIndex) {
listDisplay.fromBottom(bottomBoxIndex);
animatedDisplay.fromBottom();
gridDisplay.viewEquation(bottomBoxIndex); //ish
perspectiveDisplay.fromBottom();
}
public void updateForAnimatedDisplay() {
Equation afterEquation = getCurrentEquation();
// TODO: Animate undo and redo?
/*-T
Ket.out.println("before = " + before);
Ket.out.println("beforeEquation = " + beforeEquation);
Ket.out.println("afterEquation = " + afterEquation);
Ket.out.println(" * old root = " + cloneOldRoot(beforeEquation));
Ket.out.println(" * old root' = " + cloneOldRoot(afterEquation));
*/
if (before!=null && (singleSelection && isSingleSelectionSet()) ) {
// To avoid jumps during edits [why?] a more
// fine-grained record is kept of the previous state
// than that kept in the undo stack.
if (beforeEquation==afterEquation) {
display.noteChange(
oldGraphics,
getColourScheme(),
before,
afterEquation,
getFontSize(),
getPanelRectangle());
} else {
Argument oldRoot = cloneOldRoot(afterEquation);
if (oldRoot!=null) {
display.noteChange(
oldGraphics,
getColourScheme(),
oldRoot,
afterEquation,
getFontSize(),
getPanelRectangle());
}
}
}
beforeEquation = afterEquation;
before = Argument.cloneArgument(afterEquation.getRoot());
singleSelection = isSingleSelectionSet();
}
private boolean isSingleSelectionSet() {
return document.getMathCollection().isSingleSelectionSet();
}
/**
* Look in the undo stack for the last 'save' state and return a clone of the root of the equation of the given index.
*/
private Argument cloneOldRoot(Equation e) {
if (e==null) return null;
Undo undo = document.getMathCollection().getUndo();
int equationIndex = e.getEquationIndex();
return undo.cloneOldRoot(equationIndex);
}
private Equation getCurrentEquation() {
return document.getCursor().getEquation();
}
/**
* This is called in response to mouse events (including scrolls), key
* events, clearing the screen (why?) opening and saving files (why?).
*/
public void updateAndRepaint() {
mark = null;
//-? deepest = null; //?
//-? clearDragIcon(); //?
panelDecoration.update();
ColourScheme colourScheme = getColourScheme();
boolean ok = display.generateBoxes(oldGraphics, colourScheme, getFontSize(), getPanelRectangle());
repaint();
for (UpdateListener l : listeners) {
l.updateEvent(document);
}
}
public void addUpdateListener(UpdateListener l) {
listeners.add(l);
}
public void removeUpdateListener(UpdateListener l) {
listeners.remove(l);
}
public void paint(Graphics g) {
Graphics2D g2D = (Graphics2D) g;
oldGraphics = g2D; //?
ImageTools.setHints(g2D);
paintBackground(g2D);
if (Box.displayBorders) {
g2D.setColor(new Color(255, 128, 255));
// Note: top, left, width, height
g2D.drawRect(4, 4, this.getWidth()-8, this.getHeight()-8);
}
display.paint(g2D, getColourScheme(), getFontSize(), getPanelRectangle());
Offset drawRectangle = getDrawRectangle();
panelDecoration.paint(g2D, drawRectangle, getColourScheme());
if (DEBUG) {
mouseEventHandler.getMouseLoop().paint(g2D);
repaint();
//- return;
}
if (icon!=null) { // TODO: Clean up:
if (dragPosition!=null) {
g2D.drawImage(icon, (int) dragPosition.x-icon.getWidth()/2, (int) dragPosition.y-icon.getHeight()/2, null);
g2D.drawOval((int) dragPosition.x-3, (int) dragPosition.y-3, 5, 5); // D
}
Position topLeft = deepest!=null ? deepest.getTopLeft() : null;
if (topLeft!=null) {
//- deepest.drawBorder(g2D, topLeft, getColourScheme());
deepest.drawInnerBorder(g2D, topLeft, getColourScheme(), true);
}
repaint();
} else if (mark!=null) {
Position topLeft = mark!=null ? mark.getTopLeft() : null;
if (topLeft!=null) {
mark.drawInnerBorder(g2D, topLeft, getColourScheme(), highlightMark());
}
repaint();
} else if (display.requiresRepaint()) {
repaint();
}
}
private boolean highlightMark() {
// TODO: Clean
if (mark==null) return false;
Argument a = mark.getArgument();
if (a==null) return false;
Argument c = getCurrent();
if (a==c) return true;
//- if (a.hasAncestor(c)) return true;
//- return false;
boolean highlight = false;
for (Branch b : a.getAncestors()) {
if (b==c) return true;
}
return highlight;
}
private Offset getDrawRectangle() {
double drawWidth = this.getWidth() - BORDER_OFFSET.width;
double drawHeight = this.getHeight() - BORDER_OFFSET.height;
return new Offset(drawWidth, drawHeight);
}
private void paintBackground(Graphics2D g2D) {
int width = this.getWidth();
int height = this.getHeight();
if (getShowBackground()) {
// Draw a background radial gradient pattern.
float cx = width / 2.0f;
float cy = height / 2.0f;
float r = 8.0f * (float) Math.sqrt(cx*cx + cy*cy);
float[] keyFrames = new float[]{0.0f, 1.0f};
//- Color centreColour = getColourScheme().getBackgroundColour();
//- Color plainTextColour = getColourScheme().getPlainTextColour();
//- Color[] colours = new Color[]{centreColour, plainTextColour};
Color[] colours = new Color[]{getColourScheme().getGradientFrom(), getColourScheme().getGradientTo()};
RadialGradientPaint rgp = new RadialGradientPaint(cx, cy, r, keyFrames, colours);
g2D.setPaint(rgp);
} else {
//- Color centreColour = getColourScheme().getBackgroundColour();
//- g2D.setPaint(centreColour);
g2D.setPaint(getColourScheme().getBackgroundColour());
}
g2D.fillRect(0, 0, width, height);
}
public Position getCentre() {
Point p = getLocationOnScreen();
return new Position(p.x + getWidth()/2, p.y + getHeight()/2);
}
public Position getTopLeft() {
Point p = getLocationOnScreen();
return new Position(p.x, p.y);
}
public boolean toggleListDisplay() {
display = isListDisplayMode() ? animatedDisplay : listDisplay; //? <default> and listDisplay
return isListDisplayMode();
}
public boolean toggleScatterDisplay() {
display = isScatterDisplayMode() ? listDisplay : scatterDisplay;
scatterDisplay.clear();
return isScatterDisplayMode();
}
public boolean toggleNetworkDisplay() {
display = isNetworkDisplayMode() ? listDisplay : networkDisplay;
return isNetworkDisplayMode();
}
public boolean toggleGraphDisplay() {
display = isGraphDisplayMode() ? listDisplay : graphDisplay;
return isGraphDisplayMode();
}
public boolean toggleHistoryDisplay() {
display = isHistoryDisplayMode() ? listDisplay : historyDisplay;
return isHistoryDisplayMode();
}
public boolean toggleAnimatedDisplay() {
display = isAnimatedDisplayMode() ? listDisplay : animatedDisplay;
return isAnimatedDisplayMode();
}
public boolean toggleGridDisplay() {
if (isGridDisplayMode()) {
display = listDisplay;
} else {
display = gridDisplay;
gridDisplay.viewEquation(0);
}
return isGridDisplayMode();
}
public boolean togglePerspectiveDisplay() {
display = isPerspectiveDisplayMode() ? listDisplay : perspectiveDisplay;
return isPerspectiveDisplayMode();
}
/**
* Cycle between an animated and a list display mode, returning true if
* more than one equation is displayed.
*/
public void cygleDisplayMode() {
if (display==listDisplay) {
display = networkDisplay;
} else if (display==networkDisplay) {
display = graphDisplay;
} else if (display==graphDisplay) {
display = animatedDisplay;
} else if (display==animatedDisplay) {
display = gridDisplay;
gridDisplay.viewEquation(0);
} else if (display==gridDisplay) {
display = listDisplay;
}
document.getFrameManager().getMenuEventHandler().updateMultipleEquations();
}
public Vector<Integer> findVisibleEquationIndices() {
return display.findVisibleEquationIndices();
}
public boolean isNetworkDisplayMode() {
return display instanceof NetworkDisplay;
}
public boolean isGraphDisplayMode() {
return display instanceof GraphDisplay;
}
public boolean isHistoryDisplayMode() {
return display instanceof HistoryDisplay;
}
public boolean isAnimatedDisplayMode() {
return display instanceof AnimatedDisplay;
}
public boolean isListDisplayMode() {
return display instanceof ListDisplay;
}
public boolean isPerspectiveDisplayMode() {
return display instanceof PerspectiveDisplay;
}
public boolean isScatterDisplayMode() {
return display instanceof ScatterDisplay;
}
public boolean isGridDisplayMode() {
return display==gridDisplay;
}
public Display getDisplay() {
return display;
}
// Used by mouse event handler
public Vector<Box> getVisibleBoxes() {
// TODO: Implement based on mathCollection.getSelection().
// getEquationList().getEquations().subList(visible-box-range);
Ket.out.println(" !!! ketPanel.getVisibleBoxes():: Not implemented !!! ");
return null;
}
public boolean isArgumentVisible(Argument argument) {
return display.isArgumentVisible(argument);
}
public void viewEquation(int index) {
listDisplay.viewEquation(index);
}
/**
* This highlights only a particular argument when it is exactly
* clicked on. If no argument is clicked on, regardless if you click
* on either an equation's background or equation number, the selection
* does not change.
*/
public void selectArgument(Position p) {
Argument argument = findDeepestArgument(p);
if (argument!=null) {
document.getSelection().setOnly(argument);
}
}
public AnimatedDisplay getAnimatedDisplay() {
return animatedDisplay;
}
public Document getDocument() {
return document;
}
private int getFontSize() {
return document.getBoxFontSize();
}
public Offset getPanelRectangle() {
return new Offset(this.getWidth(), this.getHeight());
}
private ColourScheme getColourScheme() {
return document.getColourScheme();
}
public void toggleScatter() {
scatterDisplay.toggleScatter();
}
/**
* Drag an equation (current) to a given position (within scatter display).
*/
public void dragTo(Equation e, Position p) {
assert display==scatterDisplay;
scatterDisplay.dragTo(e, p);
}
public void processPairs() {
if (isAnimatedDisplayMode()) {
animatedDisplay.processPairs();
}
}
public void setDragLocation(Position dragPosition) {
this.dragPosition = dragPosition;
// Find the deepest box and highlight it.
deepest = findDeepestBox(dragPosition);
}
public void clearDragIcon() {
dragPosition = null;
icon = null;
}
public void updateDragIcon(Position dragPosition) {
//- this.dragPosition = dragPosition;
Box box = getCurrent().toBox(0L, getColourScheme());
box.setupInnerRectangle(getFontSize());
Offset innerRectangle = box.getInnerRectangle();
int w = (int) Math.max(10, innerRectangle.width);
int h = (int) Math.max(10, innerRectangle.height);
icon = ImageTools.boxToBufferedImage(box, w, h, getColourScheme(), false);
deepest = null;
}
Box deepest = null;
Box mark = null;
public void markBorder(Box mark) {
if (this.mark==mark) return;
this.mark = mark;
repaint();
}
private Argument getCurrent() {
return document.getCursor().getCurrent();
}
}