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

Last change on this file since 1216 was 1216, checked in by bln4, 5 years ago

DisplayController.java -> Ability to Enter and check if the MailBay is currently shown. When a message is received in the MessageBay, it gets screen

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