/**
* DisplayIO.java
* Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
*
* 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 .
*/
package org.expeditee.gui;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;
import java.util.function.BooleanSupplier;
import org.expeditee.auth.mail.gui.MailBay;
import org.expeditee.core.Clip;
import org.expeditee.core.Colour;
import org.expeditee.core.Cursor;
import org.expeditee.core.DamageAreas;
import org.expeditee.core.Dimension;
import org.expeditee.core.EnforcedClipStack.EnforcedClipKey;
import org.expeditee.core.Image;
import org.expeditee.core.InOutReference;
import org.expeditee.core.Line;
import org.expeditee.core.Point;
import org.expeditee.core.Stroke;
import org.expeditee.core.bounds.AxisAlignedBoxBounds;
import org.expeditee.core.bounds.Bounds;
import org.expeditee.encryption.items.surrogates.Label;
import org.expeditee.gio.EcosystemManager;
import org.expeditee.gio.GraphicsManager;
import org.expeditee.gio.gesture.StandardGestureActions;
import org.expeditee.items.Item;
import org.expeditee.items.ItemParentStateChangedEvent;
import org.expeditee.items.ItemUtils;
import org.expeditee.items.Picture;
import org.expeditee.items.Text;
import org.expeditee.items.MagneticConstraint.MagneticConstraints;
import org.expeditee.settings.UserSettings;
import org.expeditee.stats.SessionStats;
import org.expeditee.taskmanagement.EntitySaveManager;
/**
* Controls the layout of the frames inside the display.
*
* @author jdm18
* @author cts16
*/
public final class DisplayController {
/** Enumeration of the two sides of twin-frames mode. */
public static enum TwinFramesSide {
LEFT,
RIGHT
}
// To help title calculations on frame
public static final int MINIMUM_FRAME_WIDTH = 512;
public static final int MINIMUM_FRAME_HEIGHT = 512;
public static boolean DISPLAYED_ABOVE_MINIMUM_FRAME_WIDTH = false;
/** Convenience definition of TwinFramesSide.LEFT. */
public static final TwinFramesSide LEFT = TwinFramesSide.LEFT;
/** Convenience definition of TwinFramesSide.RIGHT. */
public static final TwinFramesSide RIGHT = TwinFramesSide.RIGHT;
/** The side-length of a small cursor (in pixels). */
public static final int SMALL_CURSOR_SIZE = 16;
/** The side-length of a medium cursor (in pixels). */
public static final int MEDIUM_CURSOR_SIZE = 32;
/** The side-length of a large cursor (in pixels). */
public static final int LARGE_CURSOR_SIZE = 64;
/** The height of the message bay at program start. */
public static final int INITIAL_MESSAGE_BAY_HEIGHT = 105;
/** The colour to be used to highlight the linked parent item, when the user navigates backwards. */
public static final Colour BACK_HIGHLIGHT_COLOR = Colour.MAGENTA;
/** The stroke used to draw the separating lines between the display areas. */
protected static final Stroke SEPARATOR_STROKE = new Stroke(1);
/** The colour used to draw the separating lines between the display areas. */
protected static final Colour SEPARATOR_COLOUR = Colour.BLACK;
/** The title to display in the Title bar. */
public static final String TITLE = "Expeditee";
/** The image to use as the window icon. */
public static final String ICON_IMAGE = "org/expeditee/assets/icons/expediteeicon128.png";
/** The current frame being displayed on each side of the window. */
private static Frame[] _currentFrames = new Frame[2];
/** The transitions to use on each side when changing frame. */
private static FrameTransition[] _transitions = new FrameTransition[2];
/** Maintains the list of frames visited thus-far for back-tracking. */
@SuppressWarnings("unchecked")
private static Stack[] _visitedFrames = new Stack[2];
/** TODO: Comment. cts16 */
@SuppressWarnings("unchecked")
private static Stack[] _backedUpFrames = new Stack[2];
/** Whether we are currently in twin-frames mode. */
private static boolean _twinFramesMode = false;
/** Whether we are currently displaying in audience mode. */
private static boolean _audienceMode = false;
/** Whether we are currently displaying mail mode whilst not in audience mode */
private static boolean _mailMode = false;
/** Whether we are currently displaying in x-ray mode. */
private static boolean _xrayMode = false;
/** Notified whenever the frame changes. */
private static HashSet _displayObservers = new HashSet();
/** What type of cursor we are using. */
private static Cursor.CursorType _cursorType = Item.DEFAULT_CURSOR;
/** The size of the window which this class controls. */
private static Dimension _windowSize = null;
/** The area in the window where the left frame should be painted in twin-frames mode. */
private static AxisAlignedBoxBounds _leftFramePaintArea = null;
/** The area in the window where the right frame should be painted in twin-frames mode. */
private static AxisAlignedBoxBounds _rightFramePaintArea = null;
/** The area in the window where the frame should be painted in single-frame mode. */
private static AxisAlignedBoxBounds _framePaintArea = null;
/** The area in the window where the message bay should be painted. */
private static AxisAlignedBoxBounds _messageBayPaintArea = null;
/** The height of the message bay. */
private static int _messageBayHeight = INITIAL_MESSAGE_BAY_HEIGHT;
/** The percentage of the display width allocated to the left frame. */
private static float _twinFramesLeftWidthProportion = 0.5f;
/** The damage areas accumulated by item changes. */
private static DamageAreas _damagedAreas = new DamageAreas();
/** The buffered display image for preventing flickering during rendering. */
private static Image _refreshBuffer = null;
/** Static-only class. */
private DisplayController()
{
}
/** Initialises the display controller. */
public static void Init()
{
_visitedFrames[0] = new Stack();
_visitedFrames[1] = new Stack();
_backedUpFrames[0] = new Stack();
_backedUpFrames[1] = new Stack();
refreshCursor();
refreshWindowSize();
}
/** Notifies observers that the frame has changed. */
private static void fireFrameChanged()
{
for (DisplayObserver observer : _displayObservers) {
observer.frameChanged();
}
}
/**
* Adds a DisplayObserver to the display controller. DisplayObserver's are
* notified when frame changes.
*
* @see #removeDisplayObserver(DisplayObserver)
*
* @param observer
* The observer to add
*
* @throws NullPointerException
* If observer is null.
*/
public static void addDisplayObserver(DisplayObserver observer)
{
if (observer == null) {
throw new NullPointerException("observer");
}
_displayObservers.add(observer);
}
/**
* Removes a DisplayObserver from the display controller.
*
* @see #addDisplayObserver(DisplayObserver)
*
* @param observer
* The observer to add
*
* @throws NullPointerException
* If observer is null.
*/
public static void removeDisplayObserver(DisplayObserver observer)
{
if (observer == null) {
throw new NullPointerException("observer");
}
_displayObservers.remove(observer);
}
/**
* TODO: Comment. cts16
* TODO: Move. Doesn't belong here. cts16
*/
public static void setTextCursor(Text text, int cursorMovement)
{
setTextCursor(text, cursorMovement, false, false, false, false);
}
/**
* TODO: Comment. cts16
* TODO: Refactor. Too monolithic. cts16
* TODO: Move. Doesn't belong here. cts16
*/
public static void setTextCursor(Text text, int cursorMovement,
boolean newSize, boolean isShiftDown, boolean isCtrlDown,
boolean allowClearSelection) {
int size = Math.round(text.getSize());
if (allowClearSelection && !isShiftDown && text.hasSelection()) {
text.clearSelection();
}
Point newMouse = text.moveCursor(cursorMovement, DisplayController.getFloatMouseX(), EcosystemManager.getInputManager().getCursorPosition().getY(), isShiftDown, isCtrlDown);
if( isCtrlDown ||
(DisplayController.getFloatMouseX() <= newMouse.getX() && newMouse.getX() <= DisplayController.getFloatMouseX() + 1) ||
(DisplayController.getFloatMouseX() > newMouse.getX() && cursorMovement == Text.RIGHT))
{
if(cursorMovement == Text.RIGHT && !MagneticConstraints.getInstance().rightBorderHit(text)) {
MagneticConstraints.getInstance().endOfLineHit(text);
} else {
if(cursorMovement == Text.LEFT && !MagneticConstraints.getInstance().leftBorderHit(text)) {
MagneticConstraints.getInstance().startOfLineHit(text);
}
}
newMouse.setX((int) DisplayController.getFloatMouseX());
newMouse.setY((int) DisplayController.getFloatMouseY());
} else if(cursorMovement == Text.UP && MagneticConstraints.getInstance().topBorderHit(text)) {
newMouse.setX((int) DisplayController.getFloatMouseX());
newMouse.setY((int) DisplayController.getFloatMouseY());
} else if(cursorMovement == Text.DOWN && MagneticConstraints.getInstance().bottomBorderHit(text)) {
newMouse.setX((int) DisplayController.getFloatMouseX());
newMouse.setY((int) DisplayController.getFloatMouseY());
}
if (!newSize && _cursorType == Item.TEXT_CURSOR) {
if (cursorMovement != 0) {
DisplayController.setCursorPosition(newMouse, false);
}
return;
}
_cursorType = Item.TEXT_CURSOR;
// Do some stuff to adjust the cursor size based on the font size
final int MEDIUM_CURSOR_CUTOFF = 31;
final int LARGE_CURSOR_CUTOFF = 62;
int cursorSize = LARGE_CURSOR_SIZE;
int hotspotPos = 0;
int start = 0;
Dimension best_cursor_dim = EcosystemManager.getGraphicsManager().getBestCursorSize(new Dimension(cursorSize,cursorSize));
int best_cursor_height = best_cursor_dim.height;
if (best_cursor_height < cursorSize) {
// not able to provide the size of cursor Expeditee wants to
// => lock on to 'best_cursor_height' and use this to generate dependent values
cursorSize = best_cursor_height; // OS + Java version dependent: most likely MEDIUM_CURSOR_SIZE
if (size < best_cursor_height) {
start = cursorSize - size - 1;
hotspotPos = cursorSize - (size + 1) / 4;
}
else {
start = size - best_cursor_height;
hotspotPos = cursorSize -1;
}
}
else if (size < MEDIUM_CURSOR_CUTOFF) {
cursorSize = MEDIUM_CURSOR_SIZE;
start = cursorSize - size - 1;
hotspotPos = cursorSize - (size + 1) / 4;
} else if (size < LARGE_CURSOR_CUTOFF) {
hotspotPos = cursorSize - (size - 5) / 4;
start = cursorSize - size - 2;
} else {
int FIXED_CURSOR_MIN = 77;
if (size >= FIXED_CURSOR_MIN) {
hotspotPos = cursorSize - 2;
} else {
hotspotPos = size - (FIXED_CURSOR_MIN - cursorSize);
}
}
int[] pixels = new int[cursorSize * cursorSize];
for (int i = start; i < cursorSize; i++) {
pixels[i * cursorSize] = pixels[i * cursorSize + 1] = 0xFF000000;
}
Image image = Image.createImage(cursorSize, cursorSize, pixels);
EcosystemManager.getGraphicsManager().setCursor(new Cursor(image, new Point(0, hotspotPos), "textcursor"));
if (cursorMovement != Text.NONE) {
DisplayController.setCursorPosition(newMouse, false);
}
}
/**
* Sets the type of cursor the display should be using
*
* @param type
* The type of cursor to display, using constants defined in the
* Cursor class.
*/
public static void setCursor(Cursor.CursorType type)
{
// Avoid flicker when not changing
if (type == _cursorType) {
return;
}
_cursorType = type;
refreshCursor();
}
/** Gets the type of cursor the display is currently using. */
public static Cursor.CursorType getCursor()
{
return _cursorType;
}
/** Updates the cursor with the graphics manager. */
private static void refreshCursor()
{
if (_cursorType == Item.HIDDEN_CURSOR || (FreeItems.hasCursor() && _cursorType == Item.DEFAULT_CURSOR)) {
Cursor invisibleCursor = Cursor.createInvisibleCursor();
EcosystemManager.getGraphicsManager().setCursor(invisibleCursor);
} else {
EcosystemManager.getGraphicsManager().setCursor(new Cursor(_cursorType));
}
}
/**
* Moves the mouse cursor to the given x,y coordinates on the screen
*
* @param x
* The x coordinate
* @param y
* The y coordinate
*/
public static void setCursorPosition(float x, float y)
{
setCursorPosition(x, y, true);
}
/** TODO: Comment. cts16 */
public static void setCursorPosition(float x, float y, boolean forceArrow)
{
// Adjust the position to move the mouse to to account for being in
// TwinFramesMode
if (isTwinFramesOn()) {
if (getCurrentSide() == RIGHT) {
int middle = getTwinFramesSeparatorX();
x += middle;
}
}
if (FreeItems.hasItemsAttachedToCursor()) {
Point mousePos = EcosystemManager.getInputManager().getCursorPosition();
float deltax = x - mousePos.getX();
float deltay = y - mousePos.getY();
List- toMove = FreeItems.getInstance();
for (Item move : toMove) {
move.setPosition(move.getX() + deltax, move.getY() + deltay);
}
}
// cheat
StandardGestureActions.setForceArrow(forceArrow);
EcosystemManager.getInputManager().setCursorPosition(new Point(x, y));
}
public static void resetCursorOffset()
{
StandardGestureActions.resetOffset();
}
/**
* Sets the current cursor position in the current frame
*
* @param pos
*/
public static void setCursorPosition(Point pos)
{
setCursorPosition(pos.getX(), pos.getY());
}
public static void setCursorPosition(Point pos, boolean forceArrow)
{
setCursorPosition(pos.getX(), pos.getY(), forceArrow);
}
/**
* Returns the top item (last added) of the Back-Stack (which is popped off)
*
* @return The name of the last Frame added to the back-stack
*/
public static String getLastFrame()
{
TwinFramesSide side = getCurrentSide();
Stack visitedFrames = getVisitedFrames(side);
if (visitedFrames.size() > 0) {
return visitedFrames.pop();
} else {
return null;
}
}
/**
* Adds the given Frame to the back-stack
*
* @param frame
* The Frame to add
*/
public static void addToBack(Frame toAdd)
{
TwinFramesSide side = getCurrentSide();
Stack visitedFrames = getVisitedFrames(side);
visitedFrames.push(toAdd.getName());
}
public static String removeFromBack()
{
return getLastFrame();
}
/**
* Returns a 'peek' at the end element on the back-stack of the current
* side. If the back-stack is empty, null is returned.
*
* @return The name of the most recent Frame added to the back-stack, or
* null if the back-stack is empty.
*/
public static String peekFromBackUpStack()
{
TwinFramesSide side = getCurrentSide();
Stack visitedFrames = getVisitedFrames(side);
// check that the stack is not empty
if (visitedFrames.size() > 0) {
return visitedFrames.peek();
}
// if the stack is empty, return null
return null;
}
/** Sets the transition to use when leaving the given frame. */
public static void setTransition(Frame frame, FrameTransition transition)
{
if (frame == null) {
return;
}
TwinFramesSide side = getSideFrameIsOn(frame);
setTransition(side, transition);
}
/** Sets the transition to use when changing frame on the given side. */
private static void setTransition(TwinFramesSide side, FrameTransition transition)
{
if (side == null) {
return;
}
_transitions[side.ordinal()] = transition;
}
/** Gets the transition to use when changing frame on the given side. */
private static FrameTransition getTransition(TwinFramesSide side)
{
if (side == null) {
return null;
}
return _transitions[side.ordinal()];
}
/**
* TODO: Comment. cts16
* TODO: Refactor. Too monolithic. cts16
*/
public static void setCurrentFrame(Frame frame, boolean incrementStats)
{
if (frame == null) {
return;
}
// If one of the sides doesn't have a frame yet, give it this one
if (isTwinFramesOn()) {
for (TwinFramesSide side : TwinFramesSide.values()) {
if (!sideHasFrame(side)) {
setFrameOnSide(frame, side);
fireFrameChanged();
return;
}
}
}
// if this is already the current frame
if (frame == getCurrentFrame()) {
requestRefresh(false);
MessageBay.displayMessage(frame.getName() + " is already the current frame.");
return;
}
// Update stats
if (incrementStats) {
SessionStats.AccessedFrame();
}
// Invalidate free items
if (!FreeItems.getInstance().isEmpty() && getCurrentFrame() != null) {
// Empty free items temporarily so that the old frames buffer is repainted without the free items.
ArrayList
- tmp = FreeItems.getInstance().clone();
FreeItems.getInstance().clear(); // NOTE: This will invalidate all the cleared free items
requestRefresh(true);
FreeItems.getInstance().addAll(tmp);
}
// Changing frames is a Save point for saveable entities:
EntitySaveManager.getInstance().saveAll();
if (_twinFramesMode) {
// if the same frame is being shown in both sides, load a fresh
// copy from disk
if (getOppositeFrame() == frame || getOppositeFrame().hasOverlay(frame)) {
FrameIO.SuspendCache();
frame = FrameIO.LoadFrame(frame.getName());
FrameIO.ResumeCache();
}
// If the frames are the same then the items for the
// frame that is just about to hide will still be in view
// so only notify items that they are hidden if the
// frames differ.
if (getCurrentFrame() != null && !bothSidesHaveSameFrame()) {
for (Item i : getCurrentFrame().getSortedItems()) {
i.onParentStateChanged(new ItemParentStateChangedEvent(getCurrentFrame(), ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN));
}
}
setFrameOnSide(frame, getCurrentSide());
// BROOK : TODO... overlays and loadable widgets
for (Item i : getCurrentFrame().getSortedItems()) {
i.onParentStateChanged(new ItemParentStateChangedEvent(getCurrentFrame(), ItemParentStateChangedEvent.EVENT_TYPE_SHOWN));
}
} else {
// Notifying items on the frame being hidden that they
// are about to be hidden.
// ie. Widgets use this method to remove themselves from the JPanel
List currentOnlyOverlays = new LinkedList();
List nextOnlyOverlays = new LinkedList();
List sharedOverlays = new LinkedList();
// Get all overlayed frames seen by the next frame
for (Overlay o : frame.getOverlays()) {
if (!nextOnlyOverlays.contains(o.Frame)) {
nextOnlyOverlays.add(o.Frame);
}
}
// Get all overlayed frames seen by the current frame
if (getCurrentFrame() != null) {
for (Overlay o : getCurrentFrame().getOverlays()) {
if (!currentOnlyOverlays.contains(o.Frame)) {
currentOnlyOverlays.add(o.Frame);
}
}
}
// Extract shared overlays between the current and next frame
for (Frame of : currentOnlyOverlays) {
if (nextOnlyOverlays.contains(of)) {
sharedOverlays.add(of);
}
}
// The first set, currentOnlyOverlays, must be notified that they
// are hidden
Collection
- items = new LinkedList
- ();
// Notify items that will not be in view any more
if (getCurrentFrame() != null) {
List seen = new LinkedList();
seen.addAll(sharedOverlays); // Signify that seen all shared
// overlays
seen.remove(getCurrentFrame()); // must ensure
// excluded
// Get all items seen from the current frame - including all
// possible non-shared overlays
items = getCurrentFrame().getAllItems();
for (Frame f : seen) {
items.removeAll(f.getAllItems());
}
// Notify items that they are hidden
for (Item i : items) {
i.onParentStateChanged(new ItemParentStateChangedEvent(getCurrentFrame(), ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN));
}
}
// Set the new frame
setFrameOnSide(frame, getCurrentSide());
frame.refreshSize();
// Notify items on the frame being displayed that they are in view
// ie. widgets use this method to add themselves to the content pane
items.clear();
// Notify overlay items that they are shown
for (Item i : frame.getOverlayItems()) {
Overlay owner = frame.getOverlayOwner(i);
// if (owner == null) i.onParentFameShown(false, 0);
// else ...
assert (owner != null);
i
.onParentStateChanged(new ItemParentStateChangedEvent(
frame,
ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY,
owner.permission));
}
for (Item i : frame.getSortedItems()) {
i.onParentStateChanged(new ItemParentStateChangedEvent(frame,
ItemParentStateChangedEvent.EVENT_TYPE_SHOWN));
}
}
frame.reset();
// fix text items
ItemUtils.Justify(frame);
StandardGestureActions.refreshHighlights();
requestRefresh(false);
fireFrameChanged();
}
/** Updates the window title bar to show which mode Expeditee is in. */
public static void updateTitle()
{
StringBuffer title = new StringBuffer(TITLE);
if (isAudienceMode()) {
title.append(" - Audience Mode");
} else if (isXRayMode()) {
title.append(" - X-Ray Mode");
} else {
title.append(" [").append(SessionStats.getShortStats()).append(']');
}
EcosystemManager.getGraphicsManager().setWindowTitle(title.toString());
}
/** Whether the given side has a frame. */
public static boolean sideHasFrame(TwinFramesSide side)
{
return getFrameOnSide(side) != null;
}
/** Gets the side of the twin-frames display that the mouse is over. */
public static TwinFramesSide getCurrentSide()
{
// If the mouse is over the right side of the window and there's a valid frame,
// we are on the right side.
if(isTwinFramesOn()) {
int mouseX = EcosystemManager.getInputManager().getCursorPosition().getX();
if(mouseX >= getTwinFramesSeparatorX() && sideHasFrame(RIGHT)) {
return RIGHT;
}
}
// If there's only a right frame, that's the current side
if (!sideHasFrame(LEFT) && sideHasFrame(RIGHT)) {
return RIGHT;
}
// In any other case the left side is the current side
return LEFT;
}
/** Gets the opposite side to the current side of the twin-frames. */
private static TwinFramesSide getOppositeSide()
{
if (getCurrentSide() == LEFT) {
return RIGHT;
}
return LEFT;
}
/** Returns the side that the given frame is on, or null if it's not on either side. */
public static TwinFramesSide getSideFrameIsOn(Frame frame)
{
// Loop through both sides
for (TwinFramesSide side : TwinFramesSide.values()) {
if (getFrameOnSide(side) == frame) {
return side;
}
}
return null;
}
/** Gets the frame that is on the given side of the display. */
public static Frame getFrameOnSide(TwinFramesSide side)
{
if (side == null) {
return null;
}
return _currentFrames[side.ordinal()];
}
/** Returns the frame on the current side of the display. */
public static Frame getCurrentFrame()
{
return getFrameOnSide(getCurrentSide());
}
/** Returns the frame on the opposite side of the display. */
public static Frame getOppositeFrame()
{
return getFrameOnSide(getOppositeSide());
}
/** Gets the two frames being displayed. */
public static Frame[] getFrames()
{
return _currentFrames;
}
/** Gets the x-coordinate of the division between the left and right sides. */
public static int getTwinFramesSeparatorX()
{
return getLeftFramePaintArea().getWidth() + (int) (SEPARATOR_STROKE.thickness / 2);
}
/** Gets the line that separates the two sides of the twin-frames display. */
public static Line getTwinFramesSeparatorLine()
{
if (!isTwinFramesOn()) {
return null;
}
int x = getTwinFramesSeparatorX();
int bottom = getFramePaintArea().getMaxY();
return new Line(x, 0, x, bottom);
}
/** Gets the line that separates the message bay from the frame area. */
public static Line getMessageBaySeparatorLine()
{
// No message bay in audience mode
if (isAudienceMode()) {
return null;
}
int separatorThickness = (int) SEPARATOR_STROKE.thickness;
int y = getFramePaintArea().getMaxY() + separatorThickness / 2 + separatorThickness % 2;
int right = getMessageBayPaintArea().getMaxX();
return new Line(0, y, right, y);
}
/**
* Returns the current mouse X coordinate. This coordinate is relative to
* the left edge of the frame the mouse is in. It takes into account the
* user being in twin frames mode.
*
* @return The X coordinate of the mouse.
*/
public static float getFloatMouseX()
{
return getMouseX();
}
/**
* Returns the current mouse X coordinate. This coordinate is relative to
* the left edge of the frame the mouse is in. It takes into account the
* user being in twin frames mode.
*
* @return The X coordinate of the mouse.
*/
public static int getMouseX()
{
return getMousePosition().getX();
}
/**
* Returns the current mouse position. This coordinate is relative to
* the upper-left edge of the frame the mouse is in. It takes into account the
* user being in twin frames mode.
*
* @return The position of the mouse.
*/
public static Point getMousePosition()
{
Point mousePos = EcosystemManager.getInputManager().getCursorPosition();
if (isTwinFramesOn() && getRightFramePaintArea().contains(mousePos)) {
mousePos.setX(mousePos.getX() - getRightFramePaintArea().getMinX());
}
return mousePos;
}
/**
* Returns the current mouse Y coordinate. This coordinate is relative to
* the top edge of the frame the mouse is in.
*
* @return The Y coordinate of the mouse.
*/
public static float getFloatMouseY()
{
return getMouseY();
}
/**
* Returns the current mouse Y coordinate. This coordinate is relative to
* the top edge of the frame the mouse is in.
*
* @return The Y coordinate of the mouse.
*/
public static int getMouseY()
{
return getMousePosition().getY();
}
/**
* TODO: Comment. cts16
* @return
*/
public static boolean Back()
{
TwinFramesSide side = getCurrentSide();
Stack visitedFrames = getVisitedFrames(side);
// there must be a frame to go back to
if (visitedFrames.size() < 1) {
MessageBay.displayMessageOnce("You are already on the home frame");
return false;
}
if (!FrameUtils.LeavingFrame(getCurrentFrame())) {
MessageBay.displayMessage("Error navigating back");
return false;
}
String oldFrame = getCurrentFrame().getName().toLowerCase();
// do not get a cached version (in case it is in the other window)
if (isTwinFramesOn()) {
FrameIO.SuspendCache();
}
Frame frame = FrameIO.LoadFrame(removeFromBack());
// If the top frame on the backup stack is the current frame go back
// again... or if it has been deleted
// Recursively backup the stack
if (frame == null || frame.equals(getCurrentFrame())) {
Back();
return false;
}
if (isTwinFramesOn()) {
FrameIO.ResumeCache();
}
getBackedUpFrames(side).push(oldFrame);
FrameUtils.DisplayFrame(frame, false, true);
StandardGestureActions.setHighlightHold(true);
for (Item i : frame.getSortedItems()) {
if (i.getLink() != null && i.getAbsoluteLink().toLowerCase().equals(oldFrame)) {
if (i.getHighlightMode() != Item.HighlightMode.Normal) {
i.setHighlightModeAndColour(Item.HighlightMode.Normal,
BACK_HIGHLIGHT_COLOR);
}
// check if its an @f item and if so update the buffer
if (i instanceof Picture) {
Picture p = (Picture) i;
p.refresh();
}
}
}
requestRefresh(true);
return true;
}
/**
* TODO: Comment. cts16
* @return
*/
public static boolean Forward()
{
TwinFramesSide side = getCurrentSide();
// there must be a frame to go back to
if (getBackedUpFrames(side).size() == 0) {
return false;
}
if (!FrameUtils.LeavingFrame(getCurrentFrame())) {
MessageBay.displayMessage("Error navigating forward");
return false;
}
String oldFrame = getCurrentFrame().getName().toLowerCase();
// do not get a cached version (in case it is in the other window)
if (isTwinFramesOn()) {
FrameIO.SuspendCache();
}
Frame frame = FrameIO.LoadFrame(getBackedUpFrames(side).pop());
// If the top frame on the backup stack is the current frame go back
// again... or if it has been deleted
// Recursively backup the stack
if (frame == null || frame.equals(getCurrentFrame())) {
Forward();
return false;
}
if (isTwinFramesOn()) {
FrameIO.ResumeCache();
}
getVisitedFrames(side).push(oldFrame);
FrameUtils.DisplayFrame(frame, false, true);
requestRefresh(true);
return true;
}
/** Toggles the display of frames between TwinFrames mode and Single frame mode. */
public static void toggleTwinFrames()
{
// determine which side is the active side
TwinFramesSide opposite = getOppositeSide();
_twinFramesMode = !_twinFramesMode;
// if TwinFrames is being turned on
if (_twinFramesMode) {
// if this is the first time TwinFrames has been toggled on,
// load the user's first frame
if (getVisitedFrames(opposite).size() == 0) {
FrameIO.SuspendCache();
setCurrentFrame(FrameIO.LoadFrame(UserSettings.HomeFrame.get()), true);
FrameIO.ResumeCache();
} else {
// otherwise, restore the frame from the side's back-stack
setCurrentFrame(FrameIO.LoadFrame(getVisitedFrames(opposite).pop()), true);
}
// else, TwinFrames is being turned off
} else {
// add the frame to the back-stack
Frame hiding = getOppositeFrame();
FrameUtils.LeavingFrame(hiding);
getVisitedFrames(opposite).add(hiding.getName());
setFrameOnSide(null, opposite);
getCurrentFrame().refreshSize();
}
// Update the sizes of the displayed frames
if (getCurrentFrame() != null) {
getCurrentFrame().refreshSize();
}
if (getOppositeFrame() != null) {
getOppositeFrame().refreshSize();
}
requestRefresh(false);
}
/** Whether the display is currently in twin-frames mode. */
public static boolean isTwinFramesOn()
{
return _twinFramesMode;
}
/** TODO: Comment. cts16 */
public static void Reload(TwinFramesSide side)
{
if (side == null) {
return;
}
FrameIO.SuspendCache();
Frame frame = FrameIO.LoadFrame(getFrameOnSide(side).getName());
setFrameOnSide(frame, side);
FrameIO.ResumeCache();
}
/**
* Moves the cursor the end of this item.
*
* @param i
*/
public static void MoveCursorToEndOfItem(Item i)
{
setTextCursor((Text) i, Text.END, true, false, false, false);
}
public static void clearBackedUpFrames()
{
getBackedUpFrames(getCurrentSide()).clear();
}
/**
* @param secondsDelay
* @param s
* @throws InterruptedException
*/
public static void typeStringDirect(double secondsDelay, String s) throws InterruptedException
{
for (int i = 0; i < s.length(); i++) {
StandardGestureActions.processChar(s.charAt(i), false);
Thread.sleep((int) (secondsDelay * 1000));
}
}
public static List getUnmodifiableVisitedList()
{
return Collections.unmodifiableList(getVisitedFrames(getCurrentSide()));
}
/** If both sides have the same frame (returns false if both sides have no frame). */
public static boolean bothSidesHaveSameFrame()
{
return (getCurrentFrame() != null && getCurrentFrame() == getOppositeFrame());
}
/** Sets the given side to the given frame. */
private static void setFrameOnSide(Frame frame, TwinFramesSide side)
{
if (side == null) {
return;
}
_currentFrames[side.ordinal()] = frame;
}
/** Gets the stack of visited frames for the given side. */
private static Stack getVisitedFrames(TwinFramesSide side)
{
if (side == null) {
return null;
}
return _visitedFrames[side.ordinal()];
}
/** Gets the stack of backed-up frames for the given side. */
private static Stack getBackedUpFrames(TwinFramesSide side)
{
if (side == null) {
return null;
}
return _backedUpFrames[side.ordinal()];
}
/**
* Rotates through normal -> audience -> audience + fullscreen modes and back again.
*/
public static void rotateAudienceModes()
{
Frame current = DisplayController.getCurrentFrame();
GraphicsManager g = EcosystemManager.getGraphicsManager();
if (_audienceMode && g.isFullscreen()) {
ToggleAudienceMode();
g.exitFullscreen();
} else if (_audienceMode) {
if (g.canGoFullscreen()) {
g.goFullscreen();
} else {
ToggleAudienceMode();
}
} else {
ToggleAudienceMode();
ItemUtils.UpdateConnectedToAnnotations(current.getSortedItems());
for (Overlay o : current.getOverlays()) {
ItemUtils.UpdateConnectedToAnnotations(o.Frame.getSortedItems());
}
for (Vector v : current.getVectorsDeep()) {
ItemUtils.UpdateConnectedToAnnotations(v.Frame.getSortedItems());
}
}
}
/**
* If Audience Mode is on this method will toggle it to be off, or
* vice-versa. This results in the Frame being re-parsed and repainted.
*/
public static void ToggleAudienceMode()
{
Frame current = DisplayController.getCurrentFrame();
// Turn off x-ray mode if it's on
if (_xrayMode) {
ToggleXRayMode();
}
// Toggle audience mode
_audienceMode = !_audienceMode;
refreshPaintAreas();
FrameUtils.Parse(current);
updateTitle();
requestRefresh(false);
}
/**
* If Mail Mode is on, this method will toggle it to be off, or vice-versa.
* This results in the Frame being re-parsed and repainted.
*/
public static void ToggleMailMode() {
Frame current = DisplayController.getCurrentFrame();
// Turn off x-ray mode if it's on
if (_xrayMode) {
ToggleXRayMode();
}
_mailMode = !_mailMode;
refreshPaintAreas();
FrameUtils.Parse(current);
updateTitle();
requestRefresh(false);
}
/**
* Turns Mail Mode off.
* This results in the Frame being re-parsed and repainted.
*/
public static void DisableMailMode() {
Frame current = DisplayController.getCurrentFrame();
// Turn off x-ray mode if it's on
if (_xrayMode) {
ToggleXRayMode();
}
_mailMode = false;
refreshPaintAreas();
FrameUtils.Parse(current);
updateTitle();
requestRefresh(false);
}
/**
* If X-Ray Mode is on this method will toggle it to be off, or vice-versa.
* This results in the Frame being re-parsed and repainted.
*/
public static void ToggleXRayMode() {
// Turn off x-ray mode if it is on
if (_audienceMode) {
ToggleAudienceMode();
}
_xrayMode = !_xrayMode;
if(_xrayMode) {
//Entering XRay Mode is a Save point for saveable entities
EntitySaveManager.getInstance().saveAll();
}
getCurrentFrame().parse();
getCurrentFrame().refreshSize();
updateTitle();
StandardGestureActions.refreshHighlights();
StandardGestureActions.updateCursor();
requestRefresh(false);
}
public static void ToggleSurrogateMode() {
// Turn off x-ray mode if it is on
if (_audienceMode) {
ToggleAudienceMode();
}
Label.progressSurrogateMode();
getCurrentFrame().parse();
getCurrentFrame().refreshSize();
updateTitle();
StandardGestureActions.refreshHighlights();
StandardGestureActions.updateCursor();
requestRefresh(false);
}
public static void ResetSurrogateMode() {
// Turn off x-ray mode if it is on
if (_audienceMode) {
ToggleAudienceMode();
}
Label.resetSurrogateMode();
getCurrentFrame().parse();
getCurrentFrame().refreshSize();
updateTitle();
StandardGestureActions.refreshHighlights();
StandardGestureActions.updateCursor();
requestRefresh(false);
}
/** Whether audience mode is currently on. */
public static boolean isAudienceMode()
{
return _audienceMode;
}
/** Whether mail mode is currently on. */
public static boolean isMailMode() {
return _mailMode;
}
/** Whether x-ray mode is currently on. */
public static boolean isXRayMode()
{
return _xrayMode;
}
public static Dimension getSizeEnforceMinimumXXXX() { // **** DB
Dimension actual_dim = getFramePaintAreaSize();
int enforced_width = Math.max(actual_dim.width, MINIMUM_FRAME_WIDTH);
int enforced_height = Math.max(actual_dim.height,MINIMUM_FRAME_HEIGHT);
Dimension enforced_dim = new Dimension(enforced_width,enforced_height);
return enforced_dim;
}
/** Tells the display controller to get the current window size. */
public static void refreshWindowSize()
{
_windowSize = EcosystemManager.getGraphicsManager().getWindowSize();
_refreshBuffer = Image.createImage(_windowSize, true);
if (_windowSize.getWidth() > MINIMUM_FRAME_WIDTH) {
DISPLAYED_ABOVE_MINIMUM_FRAME_WIDTH = true;
}
refreshPaintAreas();
}
/** Recalculates the paint areas for the frames and message bay. */
public static void refreshPaintAreas()
{
// Calculate the width of each frame in twin-frames mode
int availableWindowWidth = _windowSize.width - (int) SEPARATOR_STROKE.thickness;
int leftFrameWidth = (int) (availableWindowWidth * _twinFramesLeftWidthProportion);
int rightFrameWidth = availableWindowWidth - leftFrameWidth;
// The height of each frame is the window height, minus the message bay if visible
int frameHeight = _windowSize.height;
if (!isAudienceMode()) {
frameHeight -= _messageBayHeight + (int) SEPARATOR_STROKE.thickness;
}
int rightFrameX = _windowSize.width - rightFrameWidth;
int messageBayY = _windowSize.height - _messageBayHeight;
_leftFramePaintArea = new AxisAlignedBoxBounds(0, 0, leftFrameWidth, frameHeight);
_rightFramePaintArea = new AxisAlignedBoxBounds(rightFrameX, 0, rightFrameWidth, frameHeight);
_framePaintArea = new AxisAlignedBoxBounds(0, 0, _windowSize.width, frameHeight);
_messageBayPaintArea = new AxisAlignedBoxBounds(0, messageBayY, _windowSize.width, _messageBayHeight);
}
public static AxisAlignedBoxBounds getLeftFramePaintArea()
{
return _leftFramePaintArea;
}
public static AxisAlignedBoxBounds getRightFramePaintArea()
{
return _rightFramePaintArea;
}
public static AxisAlignedBoxBounds getFramePaintArea()
{
return _framePaintArea;
}
public static int getFramePaintAreaWidth()
{
if (!DISPLAYED_ABOVE_MINIMUM_FRAME_WIDTH) {
return MINIMUM_FRAME_WIDTH;
}
return _framePaintArea.getWidth();
}
public static int getFramePaintAreaHeight()
{
if (!DISPLAYED_ABOVE_MINIMUM_FRAME_WIDTH) {
return MINIMUM_FRAME_HEIGHT;
}
return _framePaintArea.getHeight();
}
public static Dimension getFramePaintAreaSize()
{
if (!DISPLAYED_ABOVE_MINIMUM_FRAME_WIDTH) {
Dimension min_dim = new Dimension(MINIMUM_FRAME_WIDTH,MINIMUM_FRAME_HEIGHT);
return min_dim;
}
return _framePaintArea.getSize();
}
public static AxisAlignedBoxBounds getMessageBayPaintArea()
{
return _messageBayPaintArea;
}
/**
* Checks that the item is visible (on current frame && overlays) - if
* visible then damaged area will be re-rendered on the next refresh.
*
* @param damagedItem
* @param toRepaint
*/
public static void invalidateItem(Item damagedItem, Bounds toRepaint)
{
if (toRepaint == null) {
return;
}
// Only add area to repaint if item is visible...
if (ItemUtils.isVisible(damagedItem)) {
invalidateArea(AxisAlignedBoxBounds.getEnclosing(toRepaint));
} else if (!_mailMode && MessageBay.isMessageItem(damagedItem)) {
invalidateArea(AxisAlignedBoxBounds.getEnclosing(toRepaint).translate(0, getMessageBayPaintArea().getMinY()));
} else if (_mailMode && MailBay.isPreviewMailItem(damagedItem)) {
invalidateArea(AxisAlignedBoxBounds.getEnclosing(toRepaint).translate(0, getMessageBayPaintArea().getMinY()));
}
}
/**
* The given area will be re-rendered in the next refresh. This is the
* quicker version and is more useful for re-rendering animated areas.
*/
public static void invalidateArea(AxisAlignedBoxBounds toRepaint)
{
_damagedAreas.addArea(toRepaint);
}
public static void clearInvalidAreas()
{
_damagedAreas.clear();
}
public static void refreshBayArea() {
// Always get the clip as it clears at the same time
Clip clip = _damagedAreas.getClip();
GraphicsManager g = EcosystemManager.getGraphicsManager();
pushBayAreaUpdate(true, clip, g);
}
/**
* Redraws the entire window. Shouldn't be called directly, instead use
* requestRefresh(boolean) to ensure no infinite refresh call loops are created.
*/
private static boolean refresh(boolean useInvalidation)
{
Frame currentFrame = getCurrentFrame();
if(currentFrame == null) {
return false;
}
// Always get the clip as it clears at the same time
Clip clip = _damagedAreas.getClip();
// No damaged areas
if (useInvalidation && clip.isFullyClipped()) {
return true;
}
GraphicsManager g = EcosystemManager.getGraphicsManager();
g.pushDrawingSurface(_refreshBuffer);
if (isTwinFramesOn()) {
Clip leftClip = null;
Clip rightClip = null;
if (useInvalidation) {
leftClip = clip.clone().intersectWith(getLeftFramePaintArea());
rightClip = clip.clone().intersectWith(getRightFramePaintArea());
if (!rightClip.isFullyClipped()) {
rightClip = new Clip(rightClip.getBounds().translate(-getRightFramePaintArea().getMinX(), 0));
}
}
Image left = null;
if (!useInvalidation || !leftClip.isFullyClipped()) {
left = FrameGraphics.getFrameImage(getFrameOnSide(LEFT), leftClip, getLeftFramePaintArea().getSize());
}
Image right = null;
if (!useInvalidation || !rightClip.isFullyClipped()) {
right = FrameGraphics.getFrameImage(getFrameOnSide(RIGHT), rightClip, getRightFramePaintArea().getSize());
}
paintTwinFrames(left, right);
} else {
Clip frameClip = null;
if (useInvalidation) {
frameClip = clip.clone().intersectWith(getFramePaintArea());
}
Image image = null;
if (currentFrame != null && (!useInvalidation || !frameClip.isFullyClipped())) {
image = FrameGraphics.getFrameImage(currentFrame, frameClip, getFramePaintAreaSize());
}
paintSingleFrame(image);
}
pushBayAreaUpdate(useInvalidation, clip, g);
// Draw any separator lines
g.drawLine(getMessageBaySeparatorLine(), SEPARATOR_COLOUR, SEPARATOR_STROKE);
g.drawLine(getTwinFramesSeparatorLine(), SEPARATOR_COLOUR, SEPARATOR_STROKE);
// Paint any popups
if (PopupManager.getInstance() != null) {
PopupManager.getInstance().paint();
}
g.popDrawingSurface();
g.drawImage(_refreshBuffer, Point.ORIGIN);
return true;
}
private static void pushBayAreaUpdate(boolean useInvalidation, Clip clip, GraphicsManager g) {
if (!isAudienceMode()) {
if (!isMailMode()) {
// Paint message bay
paintMessageBay(useInvalidation, clip, g);
} else {
// Paint mail bay
paintMailBay(useInvalidation, clip, g);
}
}
}
private static void paintMessageBay(boolean useInvalidation, Clip clip, GraphicsManager g) {
Clip messageBayClip = null;
AxisAlignedBoxBounds messageBayPaintArea = getMessageBayPaintArea();
if (useInvalidation) {
messageBayClip = clip.clone().intersectWith(messageBayPaintArea);
if (!messageBayClip.isFullyClipped()) {
messageBayClip = new Clip(messageBayClip.getBounds().translate(0, -messageBayPaintArea.getMinY()));
}
}
Image image = null;
if (!useInvalidation || !messageBayClip.isFullyClipped()) {
image = MessageBay.getImage(messageBayClip, messageBayPaintArea.getSize());
}
if (image != null) {
g.drawImage(image, messageBayPaintArea.getTopLeft(), messageBayPaintArea.getSize());
}
}
private static void paintMailBay(boolean useInvalidation, Clip clip, GraphicsManager g) {
Clip mailBayClip = null;
AxisAlignedBoxBounds mailBayPaintArea = getMessageBayPaintArea();
if (useInvalidation) {
mailBayClip = clip.clone().intersectWith(mailBayPaintArea);
if (!mailBayClip.isFullyClipped()) {
mailBayClip = new Clip(mailBayClip.getBounds().translate(0, -mailBayPaintArea.getMinY()));
}
}
Image image = null;
if (!useInvalidation || !mailBayClip.isFullyClipped()) {
image = MailBay.getImage(mailBayClip, mailBayPaintArea.getSize());
}
if (image != null) {
g.drawImage(image, mailBayPaintArea.getTopLeft(), mailBayPaintArea.getSize());
}
}
/** Draws the two frame images to the display and puts the separator line between them. */
private static void paintTwinFrames(Image leftFrameImage, Image rightFrameImage)
{
if (leftFrameImage == null && rightFrameImage == null) {
return;
}
InOutReference leftTransition = new InOutReference(getTransition(LEFT));
InOutReference rightTransition = new InOutReference(getTransition(RIGHT));
paintFrameIntoArea(leftFrameImage, getLeftFramePaintArea(), leftTransition);
paintFrameIntoArea(rightFrameImage, getRightFramePaintArea(), rightTransition);
setTransition(LEFT, leftTransition.get());
setTransition(RIGHT, rightTransition.get());
}
/** Draws the single frame image to the screen. */
private static void paintSingleFrame(Image frameImage)
{
if (frameImage == null) {
return;
}
InOutReference transition = new InOutReference(getTransition(getCurrentSide()));
paintFrameIntoArea(frameImage, getFramePaintArea(), transition);
setTransition(getCurrentSide(), transition.get());
}
/** Draws the given frame image into the given area, using a transition if one is provided. */
private static void paintFrameIntoArea(Image frameImage, AxisAlignedBoxBounds area, InOutReference transition)
{
if (frameImage == null || area == null) {
return;
}
GraphicsManager g = EcosystemManager.getGraphicsManager();
EnforcedClipKey key = g.pushClip(new Clip(area));
FrameTransition t = null;
if (transition != null) {
t = transition.get();
}
// Attempt to draw the transition if there is one
if (t != null) {
if (!t.drawTransition(frameImage, area)) {
// If drawing failed, throw the transition away
t = null;
} else {
// Schedule the next frame to be drawn
invalidateArea(area);
requestRefresh(true);
}
}
// If t == null at this stage, no transition has been drawn, so just draw the image
if (t == null) {
g.drawImage(frameImage, area.getTopLeft(), area.getSize());
}
// Discard the transition if drawing failed or it is finished
if (t == null || t.isCompleted()) {
transition.set(null);
}
g.popClip(key);
}
/** Runnable which ensures overlapping refresh requests are joined. */
private static RenderRequestMarsheller _requestMarsheller = new RenderRequestMarsheller();
/**
* If wanting to refresh from another thread - other than the main thread
* that handles the expeditee datamodel (modifying / accessing / rendering).
* Use this method for thread safety.
*/
public static synchronized void requestRefresh(boolean useInvalidation) {
requestRefresh(useInvalidation, null);
}
public static synchronized void requestRefresh(final boolean useInvalidation, final BooleanSupplier callback) {
try {
_requestMarsheller.enqueue(useInvalidation, callback);
} catch (Throwable e) {
e.printStackTrace();
}
}
/**
* Used for marshelling render requests from foreign threads to the event
* dispatcher thread... (AWT)
*
* @author Brook Novak
*/
private static class RenderRequestMarsheller implements Runnable {
private boolean _useInvalidation = true;
private boolean _enqueued = false;
private Boolean _requeueWithInvalidation = null;
private Object _lock = new Object();
private List callbacks = new LinkedList();
/** Enqueues a redraw on the GIO thread, or remembers to do so once the current redraw finishes. */
public void enqueue(final boolean useInvalidation, final BooleanSupplier callback)
{
synchronized(callbacks) {
if(callback != null) {
callbacks.add(callback);
}
}
synchronized (_lock) {
if (!_enqueued) {
_enqueued = true;
_useInvalidation = useInvalidation;
EcosystemManager.getMiscManager().runOnGIOThread(this);
} else if (_requeueWithInvalidation == null || _requeueWithInvalidation == true) {
_requeueWithInvalidation = useInvalidation;
}
}
}
@Override
public void run()
{
try {
if(refresh(_useInvalidation)) {
synchronized (callbacks) {
callbacks.forEach(cb -> cb.getAsBoolean());
callbacks.clear();
}
}
} catch (Throwable e) {
e.printStackTrace();
}
// Do another redraw if we received a new request while doing this one.
synchronized (_lock) {
if (_requeueWithInvalidation != null) {
_useInvalidation = _requeueWithInvalidation.booleanValue();
_requeueWithInvalidation = null;
EcosystemManager.getMiscManager().runOnGIOThread(this);
} else {
_enqueued = false;
}
}
}
}
}