source: trunk/src/org/expeditee/gui/DisplayController.java@ 1244

Last change on this file since 1244 was 1244, checked in by davidb, 5 years ago

After change to have resources-public and resources-private, some changes needed to support running Expeditee for a single user; other main change is to allow FrameDirs to specify relative directory paths, to help with when Expeditee is run on the cloud -- similar work still needs to occurr for ImageDir and AudioDir; some other minor changes also made.

File size: 46.2 KB
Line 
1/**
2 * DisplayIO.java
3 * Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19package org.expeditee.gui;
20
21import java.util.ArrayList;
22import java.util.Collection;
23import java.util.Collections;
24import java.util.HashSet;
25import java.util.LinkedList;
26import java.util.List;
27import java.util.Stack;
28import java.util.function.BooleanSupplier;
29
30import org.expeditee.auth.gui.MailBay;
31import org.expeditee.core.Clip;
32import org.expeditee.core.Colour;
33import org.expeditee.core.Cursor;
34import org.expeditee.core.DamageAreas;
35import org.expeditee.core.Dimension;
36import org.expeditee.core.EnforcedClipStack.EnforcedClipKey;
37import org.expeditee.core.Image;
38import org.expeditee.core.InOutReference;
39import org.expeditee.core.Line;
40import org.expeditee.core.Point;
41import org.expeditee.core.Stroke;
42import org.expeditee.core.bounds.AxisAlignedBoxBounds;
43import org.expeditee.core.bounds.Bounds;
44import org.expeditee.gio.EcosystemManager;
45import org.expeditee.gio.GraphicsManager;
46import org.expeditee.gio.gesture.StandardGestureActions;
47import org.expeditee.items.Item;
48import org.expeditee.items.ItemParentStateChangedEvent;
49import org.expeditee.items.ItemUtils;
50import org.expeditee.items.Picture;
51import org.expeditee.items.Text;
52import org.expeditee.items.MagneticConstraint.MagneticConstraints;
53import org.expeditee.settings.UserSettings;
54import org.expeditee.stats.SessionStats;
55import org.expeditee.taskmanagement.EntitySaveManager;
56
57/**
58 * Controls the layout of the frames inside the display.
59 *
60 * @author jdm18
61 * @author cts16
62 */
63public final class DisplayController {
64
65 /** Enumeration of the two sides of twin-frames mode. */
66 public static enum TwinFramesSide {
67 LEFT,
68 RIGHT
69 }
70
71 // To help title calculations on frame
72 public static final int MINIMUM_FRAME_WIDTH = 512;
73
74 /** Convenience definition of TwinFramesSide.LEFT. */
75 public static final TwinFramesSide LEFT = TwinFramesSide.LEFT;
76 /** Convenience definition of TwinFramesSide.RIGHT. */
77 public static final TwinFramesSide RIGHT = TwinFramesSide.RIGHT;
78
79 /** The side-length of a small cursor (in pixels). */
80 public static final int SMALL_CURSOR_SIZE = 16;
81 /** The side-length of a medium cursor (in pixels). */
82 public static final int MEDIUM_CURSOR_SIZE = 32;
83 /** The side-length of a large cursor (in pixels). */
84 public static final int LARGE_CURSOR_SIZE = 64;
85
86 /** The height of the message bay at program start. */
87 public static final int INITIAL_MESSAGE_BAY_HEIGHT = 105;
88
89 /** The colour to be used to highlight the linked parent item, when the user navigates backwards. */
90 public static final Colour BACK_HIGHLIGHT_COLOR = Colour.MAGENTA;
91
92 /** The stroke used to draw the separating lines between the display areas. */
93 protected static final Stroke SEPARATOR_STROKE = new Stroke(1);
94 /** The colour used to draw the separating lines between the display areas. */
95 protected static final Colour SEPARATOR_COLOUR = Colour.BLACK;
96
97 /** The title to display in the Title bar. */
98 public static final String TITLE = "Expeditee";
99
100 /** The image to use as the window icon. */
101 public static final String ICON_IMAGE = "org/expeditee/assets/icons/expediteeicon128.png";
102
103 /** The current frame being displayed on each side of the window. */
104 private static Frame[] _currentFrames = new Frame[2];
105
106 /** The transitions to use on each side when changing frame. */
107 private static FrameTransition[] _transitions = new FrameTransition[2];
108
109 /** Maintains the list of frames visited thus-far for back-tracking. */
110 @SuppressWarnings("unchecked")
111 private static Stack<String>[] _visitedFrames = new Stack[2];
112
113 /** TODO: Comment. cts16 */
114 @SuppressWarnings("unchecked")
115 private static Stack<String>[] _backedUpFrames = new Stack[2];
116
117 /** Whether we are currently in twin-frames mode. */
118 private static boolean _twinFramesMode = false;
119
120 /** Whether we are currently displaying in audience mode. */
121 private static boolean _audienceMode = false;
122
123 /** Whether we are currently displaying mail mode whilst not in audience mode */
124 private static boolean _mailMode = false;
125
126 /** Whether we are currently displaying in x-ray mode. */
127 private static boolean _xrayMode = false;
128
129 /** Notified whenever the frame changes. */
130 private static HashSet<DisplayObserver> _displayObservers = new HashSet<DisplayObserver>();
131
132 /** What type of cursor we are using. */
133 private static Cursor.CursorType _cursorType = Item.DEFAULT_CURSOR;
134
135 /** The size of the window which this class controls. */
136 private static Dimension _windowSize = null;
137
138 /** The area in the window where the left frame should be painted in twin-frames mode. */
139 private static AxisAlignedBoxBounds _leftFramePaintArea = null;
140 /** The area in the window where the right frame should be painted in twin-frames mode. */
141 private static AxisAlignedBoxBounds _rightFramePaintArea = null;
142 /** The area in the window where the frame should be painted in single-frame mode. */
143 private static AxisAlignedBoxBounds _framePaintArea = null;
144 /** The area in the window where the message bay should be painted. */
145 private static AxisAlignedBoxBounds _messageBayPaintArea = null;
146
147 /** The height of the message bay. */
148 private static int _messageBayHeight = INITIAL_MESSAGE_BAY_HEIGHT;
149 /** The percentage of the display width allocated to the left frame. */
150 private static float _twinFramesLeftWidthProportion = 0.5f;
151
152
153 /** The damage areas accumulated by item changes. */
154 private static DamageAreas _damagedAreas = new DamageAreas();
155
156 /** The buffered display image for preventing flickering during rendering. */
157 private static Image _refreshBuffer = null;
158
159 /** Static-only class. */
160 private DisplayController()
161 {
162 }
163
164 /** Initialises the display controller. */
165 public static void Init()
166 {
167 _visitedFrames[0] = new Stack<String>();
168 _visitedFrames[1] = new Stack<String>();
169 _backedUpFrames[0] = new Stack<String>();
170 _backedUpFrames[1] = new Stack<String>();
171
172 refreshCursor();
173 refreshWindowSize();
174 }
175
176 /** Notifies observers that the frame has changed. */
177 private static void fireFrameChanged()
178 {
179 for (DisplayObserver observer : _displayObservers) {
180 observer.frameChanged();
181 }
182 }
183
184 /**
185 * Adds a DisplayObserver to the display controller. DisplayObserver's are
186 * notified when frame changes.
187 *
188 * @see #removeDisplayObserver(DisplayObserver)
189 *
190 * @param observer
191 * The observer to add
192 *
193 * @throws NullPointerException
194 * If observer is null.
195 */
196 public static void addDisplayObserver(DisplayObserver observer)
197 {
198 if (observer == null) {
199 throw new NullPointerException("observer");
200 }
201
202 _displayObservers.add(observer);
203 }
204
205 /**
206 * Removes a DisplayObserver from the display controller.
207 *
208 * @see #addDisplayObserver(DisplayObserver)
209 *
210 * @param observer
211 * The observer to add
212 *
213 * @throws NullPointerException
214 * If observer is null.
215 */
216 public static void removeDisplayObserver(DisplayObserver observer)
217 {
218 if (observer == null) {
219 throw new NullPointerException("observer");
220 }
221
222 _displayObservers.remove(observer);
223 }
224
225 /**
226 * TODO: Comment. cts16
227 * TODO: Move. Doesn't belong here. cts16
228 */
229 public static void setTextCursor(Text text, int cursorMovement)
230 {
231 setTextCursor(text, cursorMovement, false, false, false, false);
232 }
233
234 /**
235 * TODO: Comment. cts16
236 * TODO: Refactor. Too monolithic. cts16
237 * TODO: Move. Doesn't belong here. cts16
238 */
239 public static void setTextCursor(Text text, int cursorMovement,
240 boolean newSize, boolean isShiftDown, boolean isCtrlDown,
241 boolean allowClearSelection) {
242
243 int size = Math.round(text.getSize());
244
245 if (allowClearSelection && !isShiftDown && text.hasSelection()) {
246 text.clearSelection();
247 }
248
249 Point newMouse = text.moveCursor(cursorMovement, DisplayController.getFloatMouseX(), EcosystemManager.getInputManager().getCursorPosition().getY(), isShiftDown, isCtrlDown);
250
251 if( isCtrlDown ||
252 (DisplayController.getFloatMouseX() <= newMouse.getX() && newMouse.getX() <= DisplayController.getFloatMouseX() + 1) ||
253 (DisplayController.getFloatMouseX() > newMouse.getX() && cursorMovement == Text.RIGHT))
254 {
255 if(cursorMovement == Text.RIGHT && !MagneticConstraints.getInstance().rightBorderHit(text)) {
256 MagneticConstraints.getInstance().endOfLineHit(text);
257 } else {
258 if(cursorMovement == Text.LEFT && !MagneticConstraints.getInstance().leftBorderHit(text)) {
259 MagneticConstraints.getInstance().startOfLineHit(text);
260 }
261 }
262 newMouse.setX((int) DisplayController.getFloatMouseX());
263 newMouse.setY((int) DisplayController.getFloatMouseY());
264 } else if(cursorMovement == Text.UP && MagneticConstraints.getInstance().topBorderHit(text)) {
265 newMouse.setX((int) DisplayController.getFloatMouseX());
266 newMouse.setY((int) DisplayController.getFloatMouseY());
267 } else if(cursorMovement == Text.DOWN && MagneticConstraints.getInstance().bottomBorderHit(text)) {
268 newMouse.setX((int) DisplayController.getFloatMouseX());
269 newMouse.setY((int) DisplayController.getFloatMouseY());
270 }
271
272 if (!newSize && _cursorType == Item.TEXT_CURSOR) {
273 if (cursorMovement != 0) {
274 DisplayController.setCursorPosition(newMouse, false);
275 }
276 return;
277 }
278
279 _cursorType = Item.TEXT_CURSOR;
280
281 // Do some stuff to adjust the cursor size based on the font size
282 final int MEDIUM_CURSOR_CUTOFF = 31;
283 final int LARGE_CURSOR_CUTOFF = 62;
284
285 int cursorSize = LARGE_CURSOR_SIZE;
286 int hotspotPos = 0;
287 int start = 0;
288
289 Dimension best_cursor_dim = EcosystemManager.getGraphicsManager().getBestCursorSize(new Dimension(cursorSize,cursorSize));
290 int best_cursor_height = best_cursor_dim.height;
291
292 if (best_cursor_height < cursorSize) {
293 // not able to provide the size of cursor Expeditee wants to
294 // => lock on to 'best_cursor_height' and use this to generate dependent values
295 cursorSize = best_cursor_height; // OS + Java version dependent: most likely MEDIUM_CURSOR_SIZE
296 if (size < best_cursor_height) {
297 start = cursorSize - size - 1;
298 hotspotPos = cursorSize - (size + 1) / 4;
299 }
300 else {
301 start = size - best_cursor_height;
302 hotspotPos = cursorSize -1;
303 }
304
305 }
306 else if (size < MEDIUM_CURSOR_CUTOFF) {
307 cursorSize = MEDIUM_CURSOR_SIZE;
308 start = cursorSize - size - 1;
309 hotspotPos = cursorSize - (size + 1) / 4;
310 } else if (size < LARGE_CURSOR_CUTOFF) {
311 hotspotPos = cursorSize - (size - 5) / 4;
312 start = cursorSize - size - 2;
313 } else {
314 int FIXED_CURSOR_MIN = 77;
315 if (size >= FIXED_CURSOR_MIN) {
316 hotspotPos = cursorSize - 2;
317 } else {
318 hotspotPos = size - (FIXED_CURSOR_MIN - cursorSize);
319 }
320 }
321
322 int[] pixels = new int[cursorSize * cursorSize];
323
324 for (int i = start; i < cursorSize; i++) {
325 pixels[i * cursorSize] = pixels[i * cursorSize + 1] = 0xFF000000;
326 }
327
328 Image image = Image.createImage(cursorSize, cursorSize, pixels);
329
330 EcosystemManager.getGraphicsManager().setCursor(new Cursor(image, new Point(0, hotspotPos), "textcursor"));
331
332 if (cursorMovement != Text.NONE) {
333 DisplayController.setCursorPosition(newMouse, false);
334 }
335 }
336
337 /**
338 * Sets the type of cursor the display should be using
339 *
340 * @param type
341 * The type of cursor to display, using constants defined in the
342 * Cursor class.
343 */
344 public static void setCursor(Cursor.CursorType type)
345 {
346 // Avoid flicker when not changing
347 if (type == _cursorType) {
348 return;
349 }
350
351 _cursorType = type;
352
353 refreshCursor();
354 }
355
356 /** Gets the type of cursor the display is currently using. */
357 public static Cursor.CursorType getCursor()
358 {
359 return _cursorType;
360 }
361
362 /** Updates the cursor with the graphics manager. */
363 private static void refreshCursor()
364 {
365 if (_cursorType == Item.HIDDEN_CURSOR || (FreeItems.hasCursor() && _cursorType == Item.DEFAULT_CURSOR)) {
366 Cursor invisibleCursor = Cursor.createInvisibleCursor();
367 EcosystemManager.getGraphicsManager().setCursor(invisibleCursor);
368 } else {
369 EcosystemManager.getGraphicsManager().setCursor(new Cursor(_cursorType));
370 }
371 }
372
373 /**
374 * Moves the mouse cursor to the given x,y coordinates on the screen
375 *
376 * @param x
377 * The x coordinate
378 * @param y
379 * The y coordinate
380 */
381 public static void setCursorPosition(float x, float y)
382 {
383 setCursorPosition(x, y, true);
384 }
385
386 /** TODO: Comment. cts16 */
387 public static void setCursorPosition(float x, float y, boolean forceArrow)
388 {
389 // Adjust the position to move the mouse to to account for being in
390 // TwinFramesMode
391 if (isTwinFramesOn()) {
392 if (getCurrentSide() == RIGHT) {
393 int middle = getTwinFramesSeparatorX();
394 x += middle;
395 }
396 }
397
398 if (FreeItems.hasItemsAttachedToCursor()) {
399 Point mousePos = EcosystemManager.getInputManager().getCursorPosition();
400 float deltax = x - mousePos.getX();
401 float deltay = y - mousePos.getY();
402 List<Item> toMove = FreeItems.getInstance();
403 for (Item move : toMove) {
404 move.setPosition(move.getX() + deltax, move.getY() + deltay);
405 }
406 }
407
408 // cheat
409 StandardGestureActions.setForceArrow(forceArrow);
410 EcosystemManager.getInputManager().setCursorPosition(new Point(x, y));
411 }
412
413 public static void resetCursorOffset()
414 {
415 StandardGestureActions.resetOffset();
416 }
417
418 /**
419 * Sets the current cursor position in the current frame
420 *
421 * @param pos
422 */
423 public static void setCursorPosition(Point pos)
424 {
425 setCursorPosition(pos.getX(), pos.getY());
426 }
427
428 public static void setCursorPosition(Point pos, boolean forceArrow)
429 {
430 setCursorPosition(pos.getX(), pos.getY(), forceArrow);
431 }
432
433 /**
434 * Returns the top item (last added) of the Back-Stack (which is popped off)
435 *
436 * @return The name of the last Frame added to the back-stack
437 */
438 public static String getLastFrame()
439 {
440 TwinFramesSide side = getCurrentSide();
441 Stack<String> visitedFrames = getVisitedFrames(side);
442
443 if (visitedFrames.size() > 0) {
444 return visitedFrames.pop();
445 } else {
446 return null;
447 }
448 }
449
450 /**
451 * Adds the given Frame to the back-stack
452 *
453 * @param frame
454 * The Frame to add
455 */
456 public static void addToBack(Frame toAdd)
457 {
458 TwinFramesSide side = getCurrentSide();
459 Stack<String> visitedFrames = getVisitedFrames(side);
460
461 visitedFrames.push(toAdd.getName());
462 }
463
464 public static String removeFromBack()
465 {
466 return getLastFrame();
467 }
468
469 /**
470 * Returns a 'peek' at the end element on the back-stack of the current
471 * side. If the back-stack is empty, null is returned.
472 *
473 * @return The name of the most recent Frame added to the back-stack, or
474 * null if the back-stack is empty.
475 */
476 public static String peekFromBackUpStack()
477 {
478 TwinFramesSide side = getCurrentSide();
479 Stack<String> visitedFrames = getVisitedFrames(side);
480
481 // check that the stack is not empty
482 if (visitedFrames.size() > 0) {
483 return visitedFrames.peek();
484 }
485
486 // if the stack is empty, return null
487 return null;
488 }
489
490 /** Sets the transition to use when leaving the given frame. */
491 public static void setTransition(Frame frame, FrameTransition transition)
492 {
493 if (frame == null) {
494 return;
495 }
496 TwinFramesSide side = getSideFrameIsOn(frame);
497 setTransition(side, transition);
498 }
499
500 /** Sets the transition to use when changing frame on the given side. */
501 private static void setTransition(TwinFramesSide side, FrameTransition transition)
502 {
503 if (side == null) {
504 return;
505 }
506
507 _transitions[side.ordinal()] = transition;
508 }
509
510 /** Gets the transition to use when changing frame on the given side. */
511 private static FrameTransition getTransition(TwinFramesSide side)
512 {
513 if (side == null) {
514 return null;
515 }
516
517 return _transitions[side.ordinal()];
518 }
519
520 /**
521 * TODO: Comment. cts16
522 * TODO: Refactor. Too monolithic. cts16
523 */
524 public static void setCurrentFrame(Frame frame, boolean incrementStats)
525 {
526 if (frame == null) {
527 return;
528 }
529
530 // If one of the sides doesn't have a frame yet, give it this one
531 if (isTwinFramesOn()) {
532 for (TwinFramesSide side : TwinFramesSide.values()) {
533 if (!sideHasFrame(side)) {
534 setFrameOnSide(frame, side);
535 fireFrameChanged();
536 return;
537 }
538 }
539 }
540
541 // if this is already the current frame
542 if (frame == getCurrentFrame()) {
543 requestRefresh(false);
544 MessageBay.displayMessage(frame.getName() + " is already the current frame.");
545 return;
546 }
547
548 // Update stats
549 if (incrementStats) {
550 SessionStats.AccessedFrame();
551 }
552
553 // Invalidate free items
554 if (!FreeItems.getInstance().isEmpty() && getCurrentFrame() != null) {
555
556 // Empty free items temporarily so that the old frames buffer is repainted without the free items.
557 ArrayList<Item> tmp = FreeItems.getInstance().clone();
558 FreeItems.getInstance().clear(); // NOTE: This will invalidate all the cleared free items
559 requestRefresh(true);
560 FreeItems.getInstance().addAll(tmp);
561
562 }
563
564 // Changing frames is a Save point for saveable entities:
565 EntitySaveManager.getInstance().saveAll();
566
567 if (_twinFramesMode) {
568 // if the same frame is being shown in both sides, load a fresh
569 // copy from disk
570 if (getOppositeFrame() == frame || getOppositeFrame().hasOverlay(frame)) {
571 FrameIO.SuspendCache();
572 frame = FrameIO.LoadFrame(frame.getName());
573 FrameIO.ResumeCache();
574 }
575
576 // If the frames are the same then the items for the
577 // frame that is just about to hide will still be in view
578 // so only notify items that they are hidden if the
579 // frames differ.
580 if (getCurrentFrame() != null && !bothSidesHaveSameFrame()) {
581 for (Item i : getCurrentFrame().getItems()) {
582 i.onParentStateChanged(new ItemParentStateChangedEvent(getCurrentFrame(), ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN));
583 }
584 }
585 setFrameOnSide(frame, getCurrentSide());
586
587 // BROOK : TODO... overlays and loadable widgets
588 for (Item i : getCurrentFrame().getItems()) {
589 i.onParentStateChanged(new ItemParentStateChangedEvent(getCurrentFrame(), ItemParentStateChangedEvent.EVENT_TYPE_SHOWN));
590 }
591 } else {
592
593 // Notifying items on the frame being hidden that they
594 // are about to be hidden.
595 // ie. Widgets use this method to remove themselves from the JPanel
596 List<Frame> currentOnlyOverlays = new LinkedList<Frame>();
597 List<Frame> nextOnlyOverlays = new LinkedList<Frame>();
598 List<Frame> sharedOverlays = new LinkedList<Frame>();
599
600 // Get all overlayed frames seen by the next frame
601 for (Overlay o : frame.getOverlays()) {
602 if (!nextOnlyOverlays.contains(o.Frame)) {
603 nextOnlyOverlays.add(o.Frame);
604 }
605 }
606
607 // Get all overlayed frames seen by the current frame
608 if (getCurrentFrame() != null) {
609 for (Overlay o : getCurrentFrame().getOverlays()) {
610 if (!currentOnlyOverlays.contains(o.Frame)) {
611 currentOnlyOverlays.add(o.Frame);
612 }
613 }
614 }
615
616 // Extract shared overlays between the current and next frame
617 for (Frame of : currentOnlyOverlays) {
618 if (nextOnlyOverlays.contains(of)) {
619 sharedOverlays.add(of);
620 }
621 }
622
623 // The first set, currentOnlyOverlays, must be notified that they
624 // are hidden
625 Collection<Item> items = new LinkedList<Item>();
626
627 // Notify items that will not be in view any more
628 if (getCurrentFrame() != null) {
629 List<Frame> seen = new LinkedList<Frame>();
630 seen.addAll(sharedOverlays); // Signify that seen all shared
631 // overlays
632 seen.remove(getCurrentFrame()); // must ensure
633 // excluded
634
635 // Get all items seen from the current frame - including all
636 // possible non-shared overlays
637 items = getCurrentFrame().getAllItems();
638 for (Frame f : seen) {
639 items.removeAll(f.getAllItems());
640 }
641
642 // Notify items that they are hidden
643 for (Item i : items) {
644 i.onParentStateChanged(new ItemParentStateChangedEvent(getCurrentFrame(), ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN));
645 }
646 }
647
648 // Set the new frame
649 setFrameOnSide(frame, getCurrentSide());
650 frame.refreshSize();
651
652 // Notify items on the frame being displayed that they are in view
653 // ie. widgets use this method to add themselves to the content pane
654 items.clear();
655
656 // Notify overlay items that they are shown
657 for (Item i : frame.getOverlayItems()) {
658 Overlay owner = frame.getOverlayOwner(i);
659 // if (owner == null) i.onParentFameShown(false, 0);
660 // else ...
661 assert (owner != null);
662 i
663 .onParentStateChanged(new ItemParentStateChangedEvent(
664 frame,
665 ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY,
666 owner.permission));
667 }
668
669 for (Item i : frame.getItems()) {
670 i.onParentStateChanged(new ItemParentStateChangedEvent(frame,
671 ItemParentStateChangedEvent.EVENT_TYPE_SHOWN));
672 }
673 }
674
675 frame.reset();
676
677 // fix text items
678 ItemUtils.Justify(frame);
679
680 StandardGestureActions.refreshHighlights();
681 requestRefresh(false);
682 fireFrameChanged();
683 }
684
685 /** Updates the window title bar to show which mode Expeditee is in. */
686 public static void updateTitle()
687 {
688 StringBuffer title = new StringBuffer(TITLE);
689
690 if (isAudienceMode()) {
691 title.append(" - Audience Mode");
692 } else if (isXRayMode()) {
693 title.append(" - X-Ray Mode");
694 } else {
695 title.append(" [").append(SessionStats.getShortStats()).append(']');
696 }
697
698 EcosystemManager.getGraphicsManager().setWindowTitle(title.toString());
699 }
700
701 /** Whether the given side has a frame. */
702 public static boolean sideHasFrame(TwinFramesSide side)
703 {
704 return getFrameOnSide(side) != null;
705 }
706
707 /** Gets the side of the twin-frames display that the mouse is over. */
708 public static TwinFramesSide getCurrentSide()
709 {
710
711 // If the mouse is over the right side of the window and there's a valid frame,
712 // we are on the right side.
713 if(isTwinFramesOn()) {
714 int mouseX = EcosystemManager.getInputManager().getCursorPosition().getX();
715 if(mouseX >= getTwinFramesSeparatorX() && sideHasFrame(RIGHT)) {
716 return RIGHT;
717 }
718 }
719
720 // If there's only a right frame, that's the current side
721 if (!sideHasFrame(LEFT) && sideHasFrame(RIGHT)) {
722 return RIGHT;
723 }
724
725 // In any other case the left side is the current side
726 return LEFT;
727 }
728
729 /** Gets the opposite side to the current side of the twin-frames. */
730 private static TwinFramesSide getOppositeSide()
731 {
732 if (getCurrentSide() == LEFT) {
733 return RIGHT;
734 }
735
736 return LEFT;
737 }
738
739 /** Returns the side that the given frame is on, or null if it's not on either side. */
740 public static TwinFramesSide getSideFrameIsOn(Frame frame)
741 {
742 // Loop through both sides
743 for (TwinFramesSide side : TwinFramesSide.values()) {
744 if (getFrameOnSide(side) == frame) {
745 return side;
746 }
747 }
748
749 return null;
750 }
751
752 /** Gets the frame that is on the given side of the display. */
753 public static Frame getFrameOnSide(TwinFramesSide side)
754 {
755 if (side == null) {
756 return null;
757 }
758
759 return _currentFrames[side.ordinal()];
760 }
761
762 /** Returns the frame on the current side of the display. */
763 public static Frame getCurrentFrame()
764 {
765 return getFrameOnSide(getCurrentSide());
766 }
767
768 /** Returns the frame on the opposite side of the display. */
769 public static Frame getOppositeFrame()
770 {
771 return getFrameOnSide(getOppositeSide());
772 }
773
774 /** Gets the two frames being displayed. */
775 public static Frame[] getFrames()
776 {
777 return _currentFrames;
778 }
779
780 /** Gets the x-coordinate of the division between the left and right sides. */
781 public static int getTwinFramesSeparatorX()
782 {
783 return getLeftFramePaintArea().getWidth() + (int) (SEPARATOR_STROKE.thickness / 2);
784 }
785
786 /** Gets the line that separates the two sides of the twin-frames display. */
787 public static Line getTwinFramesSeparatorLine()
788 {
789 if (!isTwinFramesOn()) {
790 return null;
791 }
792
793 int x = getTwinFramesSeparatorX();
794 int bottom = getFramePaintArea().getMaxY();
795
796 return new Line(x, 0, x, bottom);
797
798 }
799
800 /** Gets the line that separates the message bay from the frame area. */
801 public static Line getMessageBaySeparatorLine()
802 {
803 // No message bay in audience mode
804 if (isAudienceMode()) {
805 return null;
806 }
807
808 int separatorThickness = (int) SEPARATOR_STROKE.thickness;
809 int y = getFramePaintArea().getMaxY() + separatorThickness / 2 + separatorThickness % 2;
810 int right = getMessageBayPaintArea().getMaxX();
811
812 return new Line(0, y, right, y);
813 }
814
815 /**
816 * Returns the current mouse X coordinate. This coordinate is relative to
817 * the left edge of the frame the mouse is in. It takes into account the
818 * user being in twin frames mode.
819 *
820 * @return The X coordinate of the mouse.
821 */
822 public static float getFloatMouseX()
823 {
824 return getMouseX();
825 }
826
827
828 /**
829 * Returns the current mouse X coordinate. This coordinate is relative to
830 * the left edge of the frame the mouse is in. It takes into account the
831 * user being in twin frames mode.
832 *
833 * @return The X coordinate of the mouse.
834 */
835 public static int getMouseX()
836 {
837 return getMousePosition().getX();
838 }
839
840 /**
841 * Returns the current mouse position. This coordinate is relative to
842 * the upper-left edge of the frame the mouse is in. It takes into account the
843 * user being in twin frames mode.
844 *
845 * @return The position of the mouse.
846 */
847 public static Point getMousePosition()
848 {
849 Point mousePos = EcosystemManager.getInputManager().getCursorPosition();
850
851 if (isTwinFramesOn() && getRightFramePaintArea().contains(mousePos)) {
852 mousePos.setX(mousePos.getX() - getRightFramePaintArea().getMinX());
853 }
854
855 return mousePos;
856 }
857
858 /**
859 * Returns the current mouse Y coordinate. This coordinate is relative to
860 * the top edge of the frame the mouse is in.
861 *
862 * @return The Y coordinate of the mouse.
863 */
864 public static float getFloatMouseY()
865 {
866 return getMouseY();
867 }
868
869 /**
870 * Returns the current mouse Y coordinate. This coordinate is relative to
871 * the top edge of the frame the mouse is in.
872 *
873 * @return The Y coordinate of the mouse.
874 */
875 public static int getMouseY()
876 {
877 return getMousePosition().getY();
878 }
879
880 /**
881 * TODO: Comment. cts16
882 * @return
883 */
884 public static boolean Back()
885 {
886 TwinFramesSide side = getCurrentSide();
887 Stack<String> visitedFrames = getVisitedFrames(side);
888
889 // there must be a frame to go back to
890 if (visitedFrames.size() < 1) {
891 MessageBay.displayMessageOnce("You are already on the home frame");
892 return false;
893 }
894
895 if (!FrameUtils.LeavingFrame(getCurrentFrame())) {
896 MessageBay.displayMessage("Error navigating back");
897 return false;
898 }
899
900 String oldFrame = getCurrentFrame().getName().toLowerCase();
901
902 // do not get a cached version (in case it is in the other window)
903 if (isTwinFramesOn()) {
904 FrameIO.SuspendCache();
905 }
906
907 Frame frame = FrameIO.LoadFrame(removeFromBack());
908 // If the top frame on the backup stack is the current frame go back
909 // again... or if it has been deleted
910 // Recursively backup the stack
911 if (frame == null || frame.equals(getCurrentFrame())) {
912 Back();
913 return false;
914 }
915
916 if (isTwinFramesOn()) {
917 FrameIO.ResumeCache();
918 }
919 getBackedUpFrames(side).push(oldFrame);
920 FrameUtils.DisplayFrame(frame, false, true);
921 StandardGestureActions.setHighlightHold(true);
922
923 for (Item i : frame.getItems()) {
924 if (i.getLink() != null && i.getAbsoluteLink().toLowerCase().equals(oldFrame)) {
925 if (i.getHighlightMode() != Item.HighlightMode.Normal) {
926 i.setHighlightModeAndColour(Item.HighlightMode.Normal,
927 BACK_HIGHLIGHT_COLOR);
928 }
929 // check if its an @f item and if so update the buffer
930 if (i instanceof Picture) {
931 Picture p = (Picture) i;
932 p.refresh();
933 }
934 }
935 }
936 requestRefresh(true);
937 return true;
938 }
939
940 /**
941 * TODO: Comment. cts16
942 * @return
943 */
944 public static boolean Forward()
945 {
946 TwinFramesSide side = getCurrentSide();
947
948 // there must be a frame to go back to
949 if (getBackedUpFrames(side).size() == 0) {
950 return false;
951 }
952
953 if (!FrameUtils.LeavingFrame(getCurrentFrame())) {
954 MessageBay.displayMessage("Error navigating forward");
955 return false;
956 }
957
958 String oldFrame = getCurrentFrame().getName().toLowerCase();
959
960 // do not get a cached version (in case it is in the other window)
961 if (isTwinFramesOn()) {
962 FrameIO.SuspendCache();
963 }
964 Frame frame = FrameIO.LoadFrame(getBackedUpFrames(side).pop());
965 // If the top frame on the backup stack is the current frame go back
966 // again... or if it has been deleted
967 // Recursively backup the stack
968 if (frame == null || frame.equals(getCurrentFrame())) {
969 Forward();
970 return false;
971 }
972
973 if (isTwinFramesOn()) {
974 FrameIO.ResumeCache();
975 }
976 getVisitedFrames(side).push(oldFrame);
977 FrameUtils.DisplayFrame(frame, false, true);
978 requestRefresh(true);
979 return true;
980 }
981
982 /** Toggles the display of frames between TwinFrames mode and Single frame mode. */
983 public static void toggleTwinFrames()
984 {
985 // determine which side is the active side
986 TwinFramesSide opposite = getOppositeSide();
987 _twinFramesMode = !_twinFramesMode;
988
989 // if TwinFrames is being turned on
990 if (_twinFramesMode) {
991 // if this is the first time TwinFrames has been toggled on,
992 // load the user's first frame
993 if (getVisitedFrames(opposite).size() == 0) {
994 FrameIO.SuspendCache();
995 setCurrentFrame(FrameIO.LoadFrame(UserSettings.HomeFrame.get()), true);
996 FrameIO.ResumeCache();
997 } else {
998 // otherwise, restore the frame from the side's back-stack
999 setCurrentFrame(FrameIO.LoadFrame(getVisitedFrames(opposite).pop()), true);
1000 }
1001
1002 // else, TwinFrames is being turned off
1003 } else {
1004 // add the frame to the back-stack
1005 Frame hiding = getOppositeFrame();
1006 FrameUtils.LeavingFrame(hiding);
1007 getVisitedFrames(opposite).add(hiding.getName());
1008 setFrameOnSide(null, opposite);
1009 getCurrentFrame().refreshSize();
1010 }
1011
1012 // Update the sizes of the displayed frames
1013 if (getCurrentFrame() != null) {
1014 getCurrentFrame().refreshSize();
1015 }
1016 if (getOppositeFrame() != null) {
1017 getOppositeFrame().refreshSize();
1018 }
1019
1020 requestRefresh(false);
1021 }
1022
1023 /** Whether the display is currently in twin-frames mode. */
1024 public static boolean isTwinFramesOn()
1025 {
1026 return _twinFramesMode;
1027 }
1028
1029 /** TODO: Comment. cts16 */
1030 public static void Reload(TwinFramesSide side)
1031 {
1032 if (side == null) {
1033 return;
1034 }
1035
1036 FrameIO.SuspendCache();
1037 Frame frame = FrameIO.LoadFrame(getFrameOnSide(side).getName());
1038 setFrameOnSide(frame, side);
1039 FrameIO.ResumeCache();
1040 }
1041
1042 /**
1043 * Moves the cursor the end of this item.
1044 *
1045 * @param i
1046 */
1047 public static void MoveCursorToEndOfItem(Item i)
1048 {
1049 setTextCursor((Text) i, Text.END, true, false, false, false);
1050 }
1051
1052 public static void clearBackedUpFrames()
1053 {
1054 getBackedUpFrames(getCurrentSide()).clear();
1055 }
1056
1057 /**
1058 * @param secondsDelay
1059 * @param s
1060 * @throws InterruptedException
1061 */
1062 public static void typeStringDirect(double secondsDelay, String s) throws InterruptedException
1063 {
1064 for (int i = 0; i < s.length(); i++) {
1065 StandardGestureActions.processChar(s.charAt(i), false);
1066 Thread.sleep((int) (secondsDelay * 1000));
1067 }
1068 }
1069
1070 public static List<String> getUnmodifiableVisitedList()
1071 {
1072 return Collections.unmodifiableList(getVisitedFrames(getCurrentSide()));
1073 }
1074
1075 /** If both sides have the same frame (returns false if both sides have no frame). */
1076 public static boolean bothSidesHaveSameFrame()
1077 {
1078 return (getCurrentFrame() != null && getCurrentFrame() == getOppositeFrame());
1079 }
1080
1081 /** Sets the given side to the given frame. */
1082 private static void setFrameOnSide(Frame frame, TwinFramesSide side)
1083 {
1084 if (side == null) {
1085 return;
1086 }
1087
1088 _currentFrames[side.ordinal()] = frame;
1089 }
1090
1091 /** Gets the stack of visited frames for the given side. */
1092 private static Stack<String> getVisitedFrames(TwinFramesSide side)
1093 {
1094 if (side == null) {
1095 return null;
1096 }
1097
1098 return _visitedFrames[side.ordinal()];
1099 }
1100
1101 /** Gets the stack of backed-up frames for the given side. */
1102 private static Stack<String> getBackedUpFrames(TwinFramesSide side)
1103 {
1104 if (side == null) {
1105 return null;
1106 }
1107
1108 return _backedUpFrames[side.ordinal()];
1109 }
1110
1111 /**
1112 * Rotates through normal -> audience -> audience + fullscreen modes and back again.
1113 */
1114 public static void rotateAudienceModes()
1115 {
1116 Frame current = DisplayController.getCurrentFrame();
1117 GraphicsManager g = EcosystemManager.getGraphicsManager();
1118 if (_audienceMode && g.isFullscreen()) {
1119 ToggleAudienceMode();
1120 g.exitFullscreen();
1121 } else if (_audienceMode) {
1122 if (g.canGoFullscreen()) {
1123 g.goFullscreen();
1124 } else {
1125 ToggleAudienceMode();
1126 }
1127 } else {
1128 ToggleAudienceMode();
1129 ItemUtils.UpdateConnectedToAnnotations(current.getItems());
1130 for (Overlay o : current.getOverlays()) {
1131 ItemUtils.UpdateConnectedToAnnotations(o.Frame.getItems());
1132 }
1133 for (Vector v : current.getVectorsDeep()) {
1134 ItemUtils.UpdateConnectedToAnnotations(v.Frame.getItems());
1135 }
1136 }
1137 }
1138
1139 /**
1140 * If Audience Mode is on this method will toggle it to be off, or
1141 * vice-versa. This results in the Frame being re-parsed and repainted.
1142 */
1143 public static void ToggleAudienceMode()
1144 {
1145 Frame current = DisplayController.getCurrentFrame();
1146
1147 // Turn off x-ray mode if it's on
1148 if (_xrayMode) {
1149 ToggleXRayMode();
1150 }
1151
1152 // Toggle audience mode
1153 _audienceMode = !_audienceMode;
1154
1155 refreshPaintAreas();
1156 FrameUtils.Parse(current);
1157 updateTitle();
1158 requestRefresh(false);
1159 }
1160
1161 /**
1162 * If Mail Mode is on, this method will toggle it to be off, or vice-versa.
1163 * This results in the Frame being re-parsed and repainted.
1164 */
1165 public static void ToggleMailMode() {
1166 Frame current = DisplayController.getCurrentFrame();
1167
1168 // Turn off x-ray mode if it's on
1169 if (_xrayMode) {
1170 ToggleXRayMode();
1171 }
1172
1173 _mailMode = !_mailMode;
1174
1175 refreshPaintAreas();
1176 FrameUtils.Parse(current);
1177 updateTitle();
1178 requestRefresh(false);
1179 }
1180
1181 /**
1182 * Turns Mail Mode off.
1183 * This results in the Frame being re-parsed and repainted.
1184 */
1185 public static void DisableMailMode() {
1186 Frame current = DisplayController.getCurrentFrame();
1187
1188 // Turn off x-ray mode if it's on
1189 if (_xrayMode) {
1190 ToggleXRayMode();
1191 }
1192
1193 _mailMode = false;
1194
1195 refreshPaintAreas();
1196 FrameUtils.Parse(current);
1197 updateTitle();
1198 requestRefresh(false);
1199 }
1200
1201 /**
1202 * If X-Ray Mode is on this method will toggle it to be off, or vice-versa.
1203 * This results in the Frame being re-parsed and repainted.
1204 */
1205 public static void ToggleXRayMode()
1206 {
1207 // Turn off x-ray mode if it is on
1208 if (_audienceMode) {
1209 ToggleAudienceMode();
1210 }
1211
1212 _xrayMode = !_xrayMode;
1213
1214 if(_xrayMode) {
1215 //Entering XRay Mode is a Save point for saveable entities
1216 EntitySaveManager.getInstance().saveAll();
1217 }
1218
1219 getCurrentFrame().parse();
1220 getCurrentFrame().refreshSize();
1221 updateTitle();
1222 StandardGestureActions.refreshHighlights();
1223 StandardGestureActions.updateCursor();
1224 requestRefresh(false);
1225 }
1226
1227 /** Whether audience mode is currently on. */
1228 public static boolean isAudienceMode()
1229 {
1230 return _audienceMode;
1231 }
1232
1233 /** Whether mail mode is currently on. */
1234 public static boolean isMailMode() {
1235 return _mailMode;
1236 }
1237
1238 /** Whether x-ray mode is currently on. */
1239 public static boolean isXRayMode()
1240 {
1241 return _xrayMode;
1242 }
1243
1244 /** Tells the display controller to get the current window size. */
1245 public static void refreshWindowSize()
1246 {
1247 _windowSize = EcosystemManager.getGraphicsManager().getWindowSize();
1248 _refreshBuffer = Image.createImage(_windowSize, true);
1249 refreshPaintAreas();
1250 }
1251
1252 /** Recalculates the paint areas for the frames and message bay. */
1253 public static void refreshPaintAreas()
1254 {
1255 // Calculate the width of each frame in twin-frames mode
1256 int availableWindowWidth = _windowSize.width - (int) SEPARATOR_STROKE.thickness;
1257 int leftFrameWidth = (int) (availableWindowWidth * _twinFramesLeftWidthProportion);
1258 int rightFrameWidth = availableWindowWidth - leftFrameWidth;
1259
1260 // The height of each frame is the window height, minus the message bay if visible
1261 int frameHeight = _windowSize.height;
1262 if (!isAudienceMode()) {
1263 frameHeight -= _messageBayHeight + (int) SEPARATOR_STROKE.thickness;
1264 }
1265
1266 int rightFrameX = _windowSize.width - rightFrameWidth;
1267 int messageBayY = _windowSize.height - _messageBayHeight;
1268
1269 _leftFramePaintArea = new AxisAlignedBoxBounds(0, 0, leftFrameWidth, frameHeight);
1270 _rightFramePaintArea = new AxisAlignedBoxBounds(rightFrameX, 0, rightFrameWidth, frameHeight);
1271 _framePaintArea = new AxisAlignedBoxBounds(0, 0, _windowSize.width, frameHeight);
1272 _messageBayPaintArea = new AxisAlignedBoxBounds(0, messageBayY, _windowSize.width, _messageBayHeight);
1273 }
1274
1275 public static AxisAlignedBoxBounds getLeftFramePaintArea()
1276 {
1277 return _leftFramePaintArea;
1278 }
1279
1280 public static AxisAlignedBoxBounds getRightFramePaintArea()
1281 {
1282 return _rightFramePaintArea;
1283 }
1284
1285 public static AxisAlignedBoxBounds getFramePaintArea()
1286 {
1287 return _framePaintArea;
1288 }
1289
1290 public static AxisAlignedBoxBounds getMessageBayPaintArea()
1291 {
1292 return _messageBayPaintArea;
1293 }
1294
1295 /**
1296 * Checks that the item is visible (on current frame && overlays) - if
1297 * visible then damaged area will be re-rendered on the next refresh.
1298 *
1299 * @param damagedItem
1300 * @param toRepaint
1301 */
1302 public static void invalidateItem(Item damagedItem, Bounds toRepaint)
1303 {
1304 if (toRepaint == null) {
1305 return;
1306 }
1307
1308 // Only add area to repaint if item is visible...
1309 if (ItemUtils.isVisible(damagedItem)) {
1310 invalidateArea(AxisAlignedBoxBounds.getEnclosing(toRepaint));
1311 } else if (!_mailMode && MessageBay.isMessageItem(damagedItem)) {
1312 invalidateArea(AxisAlignedBoxBounds.getEnclosing(toRepaint).translate(0, getMessageBayPaintArea().getMinY()));
1313 } else if (_mailMode && MailBay.isPreviewMailItem(damagedItem)) {
1314 invalidateArea(AxisAlignedBoxBounds.getEnclosing(toRepaint).translate(0, getMessageBayPaintArea().getMinY()));
1315 }
1316 }
1317
1318 /**
1319 * The given area will be re-rendered in the next refresh. This is the
1320 * quicker version and is more useful for re-rendering animated areas.
1321 */
1322 public static void invalidateArea(AxisAlignedBoxBounds toRepaint)
1323 {
1324 _damagedAreas.addArea(toRepaint);
1325 }
1326
1327 public static void clearInvalidAreas()
1328 {
1329 _damagedAreas.clear();
1330 }
1331
1332 /*
1333 *
1334 * Thread-safe rendering.
1335 *
1336 */
1337
1338 /**
1339 * Redraws the entire window. Shouldn't be called directly, instead use
1340 * requestRefresh(boolean) to ensure no infinite refresh call loops are created.
1341 */
1342 private static boolean refresh(boolean useInvalidation)
1343 {
1344 Frame currentFrame = getCurrentFrame();
1345 if(currentFrame == null) {
1346 return false;
1347 }
1348
1349 // Always get the clip as it clears at the same time
1350 Clip clip = _damagedAreas.getClip();
1351
1352 // No damaged areas
1353 if (useInvalidation && clip.isFullyClipped()) {
1354 return true;
1355 }
1356
1357 GraphicsManager g = EcosystemManager.getGraphicsManager();
1358 g.pushDrawingSurface(_refreshBuffer);
1359
1360 if (isTwinFramesOn()) {
1361 Clip leftClip = null;
1362 Clip rightClip = null;
1363
1364 if (useInvalidation) {
1365 leftClip = clip.clone().intersectWith(getLeftFramePaintArea());
1366 rightClip = clip.clone().intersectWith(getRightFramePaintArea());
1367 if (!rightClip.isFullyClipped()) {
1368 rightClip = new Clip(rightClip.getBounds().translate(-getRightFramePaintArea().getMinX(), 0));
1369 }
1370 }
1371
1372 Image left = null;
1373 if (!useInvalidation || !leftClip.isFullyClipped()) {
1374 left = FrameGraphics.getFrameImage(getFrameOnSide(LEFT), leftClip, getLeftFramePaintArea().getSize());
1375 }
1376
1377 Image right = null;
1378 if (!useInvalidation || !rightClip.isFullyClipped()) {
1379 right = FrameGraphics.getFrameImage(getFrameOnSide(RIGHT), rightClip, getRightFramePaintArea().getSize());
1380 }
1381
1382 paintTwinFrames(left, right);
1383 } else {
1384 Clip frameClip = null;
1385
1386 if (useInvalidation) {
1387 frameClip = clip.clone().intersectWith(getFramePaintArea());
1388 }
1389
1390 Image image = null;
1391 if (currentFrame != null && (!useInvalidation || !frameClip.isFullyClipped())) {
1392 image = FrameGraphics.getFrameImage(currentFrame, frameClip, getFramePaintArea().getSize());
1393 }
1394
1395 paintSingleFrame(image);
1396 }
1397
1398 if (!isAudienceMode()) {
1399 if (!isMailMode()) {
1400 // Paint message bay
1401 paintMessageBay(useInvalidation, clip, g);
1402 } else {
1403 // Paint mail bay
1404 paintMailBay(useInvalidation, clip, g);
1405 }
1406 }
1407
1408 // Draw any separator lines
1409 g.drawLine(getMessageBaySeparatorLine(), SEPARATOR_COLOUR, SEPARATOR_STROKE);
1410 g.drawLine(getTwinFramesSeparatorLine(), SEPARATOR_COLOUR, SEPARATOR_STROKE);
1411
1412 // Paint any popups
1413 if (PopupManager.getInstance() != null) {
1414 PopupManager.getInstance().paint();
1415 }
1416
1417 g.popDrawingSurface();
1418 g.drawImage(_refreshBuffer, Point.ORIGIN);
1419 return true;
1420 }
1421
1422 private static void paintMessageBay(boolean useInvalidation, Clip clip, GraphicsManager g) {
1423 Clip messageBayClip = null;
1424
1425 AxisAlignedBoxBounds messageBayPaintArea = getMessageBayPaintArea();
1426 if (useInvalidation) {
1427 messageBayClip = clip.clone().intersectWith(messageBayPaintArea);
1428 if (!messageBayClip.isFullyClipped()) {
1429 messageBayClip = new Clip(messageBayClip.getBounds().translate(0, -messageBayPaintArea.getMinY()));
1430 }
1431 }
1432
1433 Image image = null;
1434 if (!useInvalidation || !messageBayClip.isFullyClipped()) {
1435 image = MessageBay.getImage(messageBayClip, messageBayPaintArea.getSize());
1436 }
1437
1438 if (image != null) {
1439 g.drawImage(image, messageBayPaintArea.getTopLeft(), messageBayPaintArea.getSize());
1440 }
1441 }
1442
1443 private static void paintMailBay(boolean useInvalidation, Clip clip, GraphicsManager g) {
1444 Clip mailBayClip = null;
1445
1446 AxisAlignedBoxBounds mailBayPaintArea = getMessageBayPaintArea();
1447 if (useInvalidation) {
1448 mailBayClip = clip.clone().intersectWith(mailBayPaintArea);
1449 if (!mailBayClip.isFullyClipped()) {
1450 mailBayClip = new Clip(mailBayClip.getBounds().translate(0, -mailBayPaintArea.getMinY()));
1451 }
1452 }
1453
1454 Image image = null;
1455 if (!useInvalidation || !mailBayClip.isFullyClipped()) {
1456 image = MailBay.getImage(mailBayClip, mailBayPaintArea.getSize());
1457 }
1458
1459 if (image != null) {
1460 g.drawImage(image, mailBayPaintArea.getTopLeft(), mailBayPaintArea.getSize());
1461 }
1462 }
1463
1464 /** Draws the two frame images to the display and puts the separator line between them. */
1465 private static void paintTwinFrames(Image leftFrameImage, Image rightFrameImage)
1466 {
1467 if (leftFrameImage == null && rightFrameImage == null) {
1468 return;
1469 }
1470
1471 InOutReference<FrameTransition> leftTransition = new InOutReference<FrameTransition>(getTransition(LEFT));
1472 InOutReference<FrameTransition> rightTransition = new InOutReference<FrameTransition>(getTransition(RIGHT));
1473
1474 paintFrameIntoArea(leftFrameImage, getLeftFramePaintArea(), leftTransition);
1475 paintFrameIntoArea(rightFrameImage, getRightFramePaintArea(), rightTransition);
1476
1477 setTransition(LEFT, leftTransition.get());
1478 setTransition(RIGHT, rightTransition.get());
1479 }
1480
1481 /** Draws the single frame image to the screen. */
1482 private static void paintSingleFrame(Image frameImage)
1483 {
1484 if (frameImage == null) {
1485 return;
1486 }
1487
1488 InOutReference<FrameTransition> transition = new InOutReference<FrameTransition>(getTransition(getCurrentSide()));
1489
1490 paintFrameIntoArea(frameImage, getFramePaintArea(), transition);
1491
1492 setTransition(getCurrentSide(), transition.get());
1493 }
1494
1495 /** Draws the given frame image into the given area, using a transition if one is provided. */
1496 private static void paintFrameIntoArea(Image frameImage, AxisAlignedBoxBounds area, InOutReference<FrameTransition> transition)
1497 {
1498 if (frameImage == null || area == null) {
1499 return;
1500 }
1501
1502 GraphicsManager g = EcosystemManager.getGraphicsManager();
1503 EnforcedClipKey key = g.pushClip(new Clip(area));
1504
1505 FrameTransition t = null;
1506 if (transition != null) {
1507 t = transition.get();
1508 }
1509
1510 // Attempt to draw the transition if there is one
1511 if (t != null) {
1512 if (!t.drawTransition(frameImage, area)) {
1513 // If drawing failed, throw the transition away
1514 t = null;
1515 } else {
1516 // Schedule the next frame to be drawn
1517 invalidateArea(area);
1518 requestRefresh(true);
1519 }
1520 }
1521
1522 // If t == null at this stage, no transition has been drawn, so just draw the image
1523 if (t == null) {
1524 g.drawImage(frameImage, area.getTopLeft(), area.getSize());
1525 }
1526
1527 // Discard the transition if drawing failed or it is finished
1528 if (t == null || t.isCompleted()) {
1529 transition.set(null);
1530 }
1531
1532 g.popClip(key);
1533 }
1534
1535 /** Runnable which ensures overlapping refresh requests are joined. */
1536 private static RenderRequestMarsheller _requestMarsheller = new RenderRequestMarsheller();
1537
1538 /**
1539 * If wanting to refresh from another thread - other than the main thread
1540 * that handles the expeditee datamodel (modifying / accessing / rendering).
1541 * Use this method for thread safety.
1542 */
1543 public static synchronized void requestRefresh(boolean useInvalidation) {
1544 requestRefresh(useInvalidation, null);
1545 }
1546
1547 public static synchronized void requestRefresh(final boolean useInvalidation, final BooleanSupplier callback) {
1548 try {
1549 _requestMarsheller.enqueue(useInvalidation, callback);
1550 } catch (Throwable e) {
1551 e.printStackTrace();
1552 }
1553 }
1554
1555 /**
1556 * Used for marshelling render requests from foreign threads to the event
1557 * dispatcher thread... (AWT)
1558 *
1559 * @author Brook Novak
1560 */
1561 private static class RenderRequestMarsheller implements Runnable {
1562
1563 private boolean _useInvalidation = true;
1564 private boolean _enqueued = false;
1565 private Boolean _requeueWithInvalidation = null;
1566 private Object _lock = new Object();
1567 private List<BooleanSupplier> callbacks = new LinkedList<BooleanSupplier>();
1568
1569 /** Enqueues a redraw on the GIO thread, or remembers to do so once the current redraw finishes. */
1570 public void enqueue(final boolean useInvalidation, final BooleanSupplier callback)
1571 {
1572 synchronized(callbacks) {
1573 if(callback != null) {
1574 callbacks.add(callback);
1575 }
1576 }
1577 synchronized (_lock) {
1578 if (!_enqueued) {
1579 _enqueued = true;
1580 _useInvalidation = useInvalidation;
1581 EcosystemManager.getMiscManager().runOnGIOThread(this);
1582 } else if (_requeueWithInvalidation == null || _requeueWithInvalidation == true) {
1583 _requeueWithInvalidation = useInvalidation;
1584 }
1585 }
1586 }
1587
1588 @Override
1589 public void run()
1590 {
1591 try {
1592 if(refresh(_useInvalidation)) {
1593 synchronized (callbacks) {
1594 callbacks.forEach(cb -> cb.getAsBoolean());
1595 callbacks.clear();
1596 }
1597 }
1598 } catch (Throwable e) {
1599 e.printStackTrace();
1600 }
1601
1602 // Do another redraw if we received a new request while doing this one.
1603 synchronized (_lock) {
1604 if (_requeueWithInvalidation != null) {
1605 _useInvalidation = _requeueWithInvalidation.booleanValue();
1606 _requeueWithInvalidation = null;
1607 EcosystemManager.getMiscManager().runOnGIOThread(this);
1608 } else {
1609 _enqueued = false;
1610 }
1611 }
1612 }
1613 }
1614}
Note: See TracBrowser for help on using the repository browser.