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

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

It is now possible to complete the process of recovering access to a Expeditee account. Further work, in the form of frames in the authentication frameset, are to follow.
A refactoring/tidy up has also been completed.

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