/** * 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; } } } } }