source: trunk/src/org/expeditee/items/widgets/HeavyDutyInteractiveWidget.java@ 284

Last change on this file since 284 was 277, checked in by bjn8, 16 years ago

Simple changes

File size: 20.5 KB
Line 
1package org.expeditee.items.widgets;
2
3import java.awt.Color;
4import java.awt.Font;
5import java.awt.FontMetrics;
6import java.awt.Graphics;
7import java.awt.Graphics2D;
8import java.awt.Rectangle;
9import java.awt.Shape;
10import java.awt.geom.Rectangle2D;
11import java.util.LinkedList;
12
13import javax.swing.JComponent;
14import javax.swing.SwingUtilities;
15
16import org.expeditee.gui.Browser;
17import org.expeditee.gui.DisplayIO;
18import org.expeditee.gui.FrameGraphics;
19import org.expeditee.gui.FreeItems;
20import org.expeditee.items.Item;
21import org.expeditee.items.ItemParentStateChangedEvent;
22import org.expeditee.items.Text;
23import org.expeditee.taskmanagement.EntityLoadManager;
24import org.expeditee.taskmanagement.EntitySaveManager;
25import org.expeditee.taskmanagement.LoadableEntity;
26import org.expeditee.taskmanagement.SaveableEntity;
27
28/**
29 * Interactive widgets that may require loading prior to usage, and/or saving.
30 *
31 * HeavyDutyInteractiveWidget provides a conventional asynchronous loading and saving scheme.
32 *
33 * When a loadable interactive widget is created. It is at a pending state - waiting to load
34 * until expeditee allocates a thread to begin loading. The swing component is set as not-visible.
35 *
36 * At the the loading stage, a loadable widget proccesses its load task and renders a
37 * load message/bar.
38 *
39 * Once loaded, the widget is like any other widget - and its visible state will be set as visible.
40 *
41 * When a HeavyDutyInteractiveWidget becomes visible / is anchored, it will register itself for saving at the next
42 * save point according to SaveEntityManager. Heavey duty widgets will only save at the save points
43 * if the widget still belongs to a frame (i.e. is not removed) and is anchored.
44 *
45 * Heavey duty widgets can also have an expiry on how long they should stay cached for. If they do have an
46 * expiry then it they unload once expired, and will re-load once in view again. Must take care for
47 * unloadable widgets because you must check your unloadable elements to whether they are unloaded or not
48 * every time your widget accessess them.
49 *
50 * When a heavy duty widget is deleted - any data consuming memory can be dumped temporarily if needed.
51 *
52 * @author Brook Novak
53 *
54 */
55public abstract class HeavyDutyInteractiveWidget extends InteractiveWidget implements LoadableEntity, SaveableEntity {
56
57 /** load finished */
58 public static final float LOAD_STATE_COMPLETED = 2.0f;
59
60 /** waiting to load. Could have been loaded previously but expired. */
61 public static final float LOAD_STATE_PENDING = 3.0f;
62
63 /** load failed */
64 public static final float LOAD_STATE_FAILED = 4.0f;
65
66 /** load was cancelled and still requires loading */
67 public static final float LOAD_STATE_INCOMPLETED = 5.0f;
68
69 // GUI Stuff
70 private static final String DEFAULT_LOAD_MESSAGE = "Loading";
71 private static final String PENDING_MESSAGE = "Pending";
72
73 private static final Color LOAD_SCREEN_COLOR = Color.LIGHT_GRAY;
74 private static final Color LOAD_SCREEN_COLOR_FREESPACE = FREESPACE_BACKCOLOR;
75
76 private static final Font LOAD_NORMAL_FONT = new Font("Arial", Font.BOLD, 12);
77 private static final Font LOAD_INDERTMIN_FONT = new Font("Arial", Font.ITALIC, 12);
78
79 private static final int BAR_HOROZONTIAL_MARGIN = 20;
80 private static final int BAR_HEIGHT = 40;
81
82 /** Unifies state transitions to a single thread. */
83 private LoadStateManager stateProccessor = null;
84 private LinkedList<HDWTask> queuedTasks = new LinkedList<HDWTask>();
85
86 // Model data
87 private float loadState; // handled by constructors and stateProccessor
88 private String screenMessage = DEFAULT_LOAD_MESSAGE;
89 private boolean hasCancelledBeenRequested;
90 private boolean isExpired = false;
91 private int cacheDepth = -1;
92
93 /**
94 * Constructor.
95 *
96 * @param source
97 *
98 * @param component
99 *
100 * @param minWidth
101 *
102 * @param maxWidth
103 *
104 * @param minHeight
105 *
106 * @param maxHeight
107 *
108 * @param cacheDepth
109 * Less or equal to zero for no cache management - i.e. use expeditees frame caching.
110 * Positive to limit cache; where the widget will be explicity unloaded
111 * ({@link HeavyDutyInteractiveWidget#unloadWidgetData()}) once the user has traversed
112 * cacheDepth frames without seeing this instance.
113 * The general rule is: the more memory your widget may take, the smaller the cache depth.
114 *
115 * @param skipLoad
116 * If true, the load state will be set to completed and the widget will not go through
117 * the loading proccess. Otherwise if true then the widget will go the loading phase.
118 */
119 protected HeavyDutyInteractiveWidget(Text source, JComponent component,
120 int minWidth, int maxWidth, int minHeight, int maxHeight, int cacheDepth, boolean skipLoad) {
121 super(source, component, minWidth, maxWidth, minHeight, maxHeight);
122
123 this.cacheDepth = cacheDepth;
124
125 if (skipLoad) {
126 loadState = LOAD_STATE_COMPLETED;
127 } else {
128 loadState = LOAD_STATE_PENDING;
129 component.setVisible(false); // not allowed to interact with it yet
130 component.setEnabled(false);
131 }
132 }
133
134 /**
135 * Chained constructor.
136 *
137 * @param source
138 * @param component
139 * @param minWidth
140 * @param maxWidth
141 * @param minHeight
142 * @param maxHeight
143 */
144 protected HeavyDutyInteractiveWidget(Text source, JComponent component,
145 int minWidth, int maxWidth, int minHeight, int maxHeight, int cacheDepth) {
146 this(source, component, minWidth, maxWidth, minHeight, maxHeight, cacheDepth, false);
147 }
148
149 /**
150 * Updates the percentage of the load. Should only call when in load phase.
151 *
152 * @param percent
153 * Must be between 0.0 and 1.0 inclusive for percentage. Or neagtive if indeterminant.
154 *
155 * @throws IllegalArgumentException
156 * If percent is larger than 1.0
157 *
158 * @throws IllegalStateException
159 * Load is not in progress
160 *
161 */
162 protected final void updateLoadPercentage(float percent) {
163 if (percent > 1.0f)
164 throw new IllegalArgumentException("loadState is larger than 1.0");
165
166 else if (!isInLoadProgress() ||
167 stateProccessor == null ||
168 !stateProccessor.isAlive())
169 throw new IllegalStateException("Load is not in progress");
170
171 // Assuming that this is called from stateProccessor.
172 stateProccessor.setLoadState(percent, false);
173 }
174
175
176 /**
177 * @return The current load state.
178 */
179 protected float getLoadState() {
180 return loadState;
181 }
182
183 /**
184 * @return True if in a loading phase.
185 */
186 protected boolean isInLoadProgress() {
187 return loadState <= 1.0f;
188 }
189
190 /**
191 * Sets the message which is displayed to the users while loading
192 * or when a load has failed.
193 *
194 * @param message
195 * A short human readable decription of the content being loaded
196 * or describing the failure.
197 * If null then the default load message will be assigned
198 */
199 protected final void setLoadScreenMessage(String message) {
200 this.screenMessage = message;
201 if (this.screenMessage == null) {
202 this.screenMessage = DEFAULT_LOAD_MESSAGE;
203 }
204
205 // Re-render loading state
206 FrameGraphics.invalidateArea(_swingComponent.getBounds());
207 FrameGraphics.requestRefresh(true);
208 }
209
210 @Override
211 public void paintInFreeSpace(Graphics g) {
212 if (loadState == LOAD_STATE_COMPLETED) super.paintInFreeSpace(g);
213 else paintLoadScreen(g, LOAD_SCREEN_COLOR_FREESPACE);
214 }
215
216 @Override
217 public void paint(Graphics g) {
218 if (loadState == LOAD_STATE_COMPLETED) {
219 super.paint(g);
220 } else {
221 paintLoadScreen(g, LOAD_SCREEN_COLOR);
222 this.paintLink((Graphics2D)g);
223 }
224
225 }
226
227 /**
228 * Rendersthe load bar / load messages
229 *
230 * @param g
231 */
232 private void paintLoadScreen(Graphics g, Color backgroundColor) {
233
234 if (Browser._theBrowser == null) return;
235
236 // Render shaded window over widget
237 g.setColor(backgroundColor);
238 g.fillRect(getX(), getY(), getWidth(), getHeight());
239
240 // Center the bar
241 int barX = getX() + BAR_HOROZONTIAL_MARGIN;
242 int barY = getY() + (getHeight() >> 1) - (BAR_HEIGHT >> 1);
243
244 int barWidth = getWidth() - (BAR_HOROZONTIAL_MARGIN * 2);
245 if (barWidth <= 0) barWidth = 10;
246
247 // Center the text
248 Font f = (loadState < 0.0f) ? LOAD_INDERTMIN_FONT : LOAD_NORMAL_FONT;
249 String message = (loadState == LOAD_STATE_PENDING || loadState == LOAD_STATE_INCOMPLETED) ?
250 PENDING_MESSAGE : screenMessage;
251
252 g.setFont(f);
253
254 // If need to re-calc the message drawing area... do so
255
256 FontMetrics fm = g.getFontMetrics(f);
257 Rectangle2D rect = fm.getStringBounds(message, g);
258 int textHeight = (int)(rect.getHeight());
259 int textWidth = (int)(rect.getWidth());
260
261 int textX = barX + ((barWidth - textWidth) >> 1);
262 if (textX <= 0) textX = BAR_HOROZONTIAL_MARGIN + 10;
263 int textY = barY + ((BAR_HEIGHT - textHeight) >> 1);
264 if (textY <= 0) textY = barY + 2;
265 textY += textHeight;
266
267 // Ensure that load bar and text doesn't spill over widgets invalidation area
268 Shape clipBackUp = g.getClip();
269 Rectangle tmpClip = (clipBackUp != null) ? clipBackUp.getBounds() :
270 new Rectangle(0, 0,
271 Browser._theBrowser.getContentPane().getWidth(),
272 Browser._theBrowser.getContentPane().getHeight());
273
274 g.setClip(tmpClip.intersection(getBounds()));
275
276
277 if (loadState < 0.0f) { // indeterminant
278
279 g.setColor(Color.ORANGE);
280 g.fillRect(barX, barY, barWidth, BAR_HEIGHT);
281
282 } else if (isInLoadProgress()) {
283
284 int progBarWidth = (int)(barWidth * loadState);
285
286 g.setColor(new Color(200, 200, 255));
287 g.fillRect(barX, barY, progBarWidth, BAR_HEIGHT);
288
289 }
290
291 g.setColor(Color.DARK_GRAY);
292 g.drawRect(barX, barY, barWidth, BAR_HEIGHT);
293
294 if (loadState == LOAD_STATE_FAILED)
295 g.setColor(Color.RED);
296
297 else g.setColor(Color.BLACK);
298
299
300
301 g.drawString(message, textX, textY);
302
303 g.setClip(clipBackUp);
304
305 }
306
307
308
309 /**
310 * Invoked by the load queue manager only.
311 */
312 public final void performLoad() {
313
314 try {
315 doTaskAndWait(HDWTask.Load);
316 } catch (InterruptedException e) {
317 loadState = LOAD_STATE_INCOMPLETED; // safety
318 e.printStackTrace();
319 }
320 }
321
322 /**
323 * Invoked by load manager
324 */
325 public final void cancelLoadRequested() {
326 hasCancelledBeenRequested = true;
327 cancelLoadWidgetData();
328 }
329
330 /**
331 * @return
332 * True if cancel has been requested. This is reset before a new load phase.
333 */
334 protected boolean hasCancelBeenRequested() {
335 return hasCancelledBeenRequested;
336 }
337
338 /**
339 *
340 * @return
341 * True if this widget is in an expired state
342 */
343 protected boolean isExpired() {
344 return isExpired;
345 }
346
347 protected boolean isLoaded() {
348 return this.loadState == LOAD_STATE_COMPLETED;
349 }
350
351
352
353/* @Override
354 public final InteractiveWidget deletedCopy() throws InteractiveWidgetNotAvailableException, InteractiveWidgetInitialisationFailedException {
355
356 queueTask(HDWTask.UnloadTMP);
357
358
359 }*/
360
361 @Override
362 public final void onDelete() {
363 super.onDelete();
364 // Evenetually - unload the data - with the intention of possible recovery, but not saving...
365 queueTask(HDWTask.UnloadTMP);
366 }
367
368 /**
369 *
370 * Invoked when it is time to perform all asynchronous (heavey duty) loading for the widget.
371 *
372 * The convention is to load until cancelLoadWidgetData is invoked. Once this is invoked it is
373 * the widgets load logic's choice to whether to acknowledge it. Note that proceeding
374 * loadable entities must wait until this returns - so it is best to heed the as soon as possible.
375 *
376 * Use updateLoadPercentage to update the load bar and setLoadScreenMessage to set the message
377 * for load feedback to users. It will start with the default loading message and progress at 0%.
378 *
379 * @return
380 * The final load state. Must be either:
381 * <ul>
382 * <li>LOAD_STATE_COMPLETED
383 * <li>LOAD_STATE_FAILED
384 * <li>LOAD_STATE_INCOMPLETED
385 * </ul>
386 * If not, then LOAD_STATE_COMPLETED is assumed - but an exception trace will be printed.
387 */
388 protected abstract float loadWidgetData();
389
390 /**
391 * @see loadWidgetData, hasCancelBeenRequested
392 */
393 protected void cancelLoadWidgetData() {}
394
395 @Override
396 protected void onParentStateChanged(int eventType) {
397
398 switch (eventType) {
399
400 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
401 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
402 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
403 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
404
405 // When anchored to the window, then requeue for loading iff load state
406 // is currently pending or incomplete
407 if (hasCancelledBeenRequested ||
408 loadState == LOAD_STATE_INCOMPLETED ||
409 loadState == LOAD_STATE_PENDING) { // if needing to load - then load
410 EntityLoadManager.getInstance().queue(this, getLoadDelayTime());
411 }
412
413 // Ensure that registered for saving at next save point
414 EntitySaveManager.getInstance().register(this);
415
416 // Ensure is cached
417 WidgetCacheManager.cacheWidget(this);
418
419 break;
420
421
422 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
423 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
424 case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
425
426 // Whenever the widget is not longer in view then cancel current loading proccess
427 // if currently loading. This must be performed later because this event occurs
428 // before the widget has had a chance to bee moved into free space.
429 SwingUtilities.invokeLater(new DoCancelLoad()); // proccess on this thread later.
430
431 // Always unregister from save point.
432 EntitySaveManager.getInstance().unregister(this);
433
434 // If the widget has been removed then unregister from caching
435 // So that if deleted then won't hang around in cache
436 if (eventType == ItemParentStateChangedEvent.EVENT_TYPE_REMOVED) {
437 WidgetCacheManager.uncacheWidget(this);
438 // If removed via overlay, then cached frames may still contain overlay with the widget...
439 }
440 break;
441 }
442 }
443
444 private class DoCancelLoad implements Runnable {
445 public void run() {
446
447 for (Item i : getItems()) { // Only cancel if the widget is not floating. Allow loading while in free space
448 if (FreeItems.getInstance().contains(i))
449 return;
450 }
451
452 // TODO: One problem with loading in free space is that there is no way of cancelling
453 // if the heavy duty widget is deleted in free space while loading.
454 // This is just an inefficiency.
455 EntityLoadManager.getInstance().cancel(HeavyDutyInteractiveWidget.this);
456
457 }
458 }
459
460 /**
461 * @return The time to delay before loading begin when first in view.
462 */
463 public abstract int getLoadDelayTime();
464
465 /**
466 * Invoked by save manager
467 */
468 public final void performSave() {
469 try {
470 doTaskAndWait(HDWTask.Save);
471 } catch (InterruptedException e) {
472 e.printStackTrace();
473 }
474 }
475
476
477 /**
478 * Called by dedicated thread to save data at a save point.
479 * See EntitySaveManager for more inforamtion about save points.
480 *
481 * Heavey duty widgets will only save at the save points if the widget
482 * still belongs to a frame and is anchored.
483 */
484 protected abstract void saveWidgetData();
485
486 /**
487 * Only invoked if has cache expiry and has expired. Only if this is no longer visible.
488 */
489 void expire() {
490 doTaskLater(HDWTask.Unload);
491 }
492
493 /**
494 * Invoked if this widget has a cache expiry and has been expired.
495 * The protocol is that you should unload anything in memory...
496 * This should be quick, e.g. setting references to null / disposing.
497 * Pupose is to free memory.
498 *
499 * Important note:
500 * It is possible for a widget to expire before it had a chance to be saved,
501 * or even when saving. Thus must be careful.
502 * A simple approach would be to check the saving flag
503 *
504 *
505 */
506 protected abstract void unloadWidgetData();
507
508 /**
509 * This is invoked asynronously when the item is deleted. This widget
510 * will be added to an undo list in memory.
511 *
512 * The intention is that the implementation should release its memory as if it were
513 * expired - But it will not be saved. Thus the data should be dumped to a temporary file
514 * so it can be recovered if it is recalled (un-deleted). Note that this is not the same as saving -
515 * the user would not expect the widget to save state if deleted.
516 *
517 * This is never invoke <i>while</i> loading / saving / expiring. The operations
518 * are mutually exclusive. It can be invoked when the widget is not yet loaded.
519 *
520 */
521 protected abstract void tempUnloadWidgetData();
522
523 /**
524 * The cache depth is measured by how many frames the user can traverse through until
525 * the widget should expire.
526 *
527 * This is immutable.
528 *
529 * @return
530 * The cache depth for this interactive widget. less or equal to zero for no cache expiry.
531 */
532 public final int getCacheDepth() {
533 return cacheDepth;
534 }
535
536 private synchronized void doTaskAndWait(HDWTask task) throws InterruptedException {
537 queueTask(task);
538 stateProccessor.join();
539 }
540
541 private void doTaskLater(HDWTask task) {
542 queueTask(task);
543 }
544
545 private void queueTask(HDWTask task) {
546
547 synchronized(queuedTasks) {
548
549 if (!queuedTasks.contains(task))
550 queuedTasks.add(task);
551
552
553 if (stateProccessor == null || !stateProccessor.isAlive()) {
554 stateProccessor = new LoadStateManager();
555 stateProccessor.start();
556 }
557
558 }
559
560 }
561
562
563 /**
564 * Unified state management. Load states are handled by one thread always.
565 * Only has one instance - per widget instance, of this at any given time,
566 *
567 * @author Brook
568 *
569 */
570 private class LoadStateManager extends Thread {
571
572 /**
573 * @param loadState
574 * Can be any of the following values.
575 * <ul>
576 * <li>PENDING_STATE if pending.
577 * <li>COMPLETED_STATE if loaded.
578 * <li>between 0.0f and 1.0f inclusive if loading: represents progress percent complete.
579 * <li>FAILED_STATE if failed.
580 * <li>INCOMPLETED_STATE if load was unable to complete.
581 * <li>negative if loading but indeterminant.
582 * </ul>
583 *
584 */
585 private void setLoadState(float state, boolean expired) {
586 assert (state == LOAD_STATE_FAILED || state != LOAD_STATE_INCOMPLETED ||
587 state != LOAD_STATE_PENDING || state != LOAD_STATE_COMPLETED ||
588 state < 1.0f) ;
589
590 assert (!expired ||
591 (expired && state == LOAD_STATE_PENDING));
592
593
594 isExpired = expired;
595 loadState = state;
596
597 if (loadState == LOAD_STATE_COMPLETED) { // set enabled state - show the swing components
598 SwingUtilities.invokeLater(new Runnable() {
599 public void run() {
600 _swingComponent.setVisible(true);
601 _swingComponent.setEnabled(true);
602 }
603 });
604
605 } else if(expired) { // disable/hide swing GUI when expires, like a reset
606
607 SwingUtilities.invokeLater(new Runnable() {
608 public void run() {
609 _swingComponent.setVisible(false);
610 _swingComponent.setEnabled(false);
611 }
612 });
613 }
614
615 // Re-render loading state - if not expired
616 if (!expired) {
617 FrameGraphics.invalidateArea(new Rectangle(getX(), getY(), getWidth(), getHeight()));
618 FrameGraphics.requestRefresh(true);
619 }
620 }
621
622 /**
623 * All state transitions performed by one thread.
624 */
625 public void run() {
626
627 while (true) { // keep proccessing tasks
628
629 HDWTask task = null;
630
631 synchronized(queuedTasks) {
632 if (queuedTasks.isEmpty()) return;
633 task = queuedTasks.remove();
634 }
635
636 if (task == HDWTask.Load) {
637 doLoad();
638 } else if (task == HDWTask.Save) {
639 doSave(); // does not change state
640 } else if (task == HDWTask.Unload){
641 doUnload();
642 } else {
643 assert(task == HDWTask.UnloadTMP);
644 doTempUnload();
645 }
646
647
648 }
649 }
650
651 private void doLoad() {
652
653 // Check that not already loaded.
654 if (loadState == LOAD_STATE_COMPLETED ||
655 loadState == LOAD_STATE_FAILED) return;
656
657 // Only load if in view
658 if (!isFloating() && getParentFrame() != DisplayIO.getCurrentFrame())
659 return;
660
661 // Reset flag.
662 hasCancelledBeenRequested = false;
663
664 // Set the load state as loading... 0%
665 setLoadState(0.0f, false);
666
667 float finalState = LOAD_STATE_FAILED;
668
669 try {
670 finalState = loadWidgetData();
671 } catch (Exception e) {
672 e.printStackTrace();
673 }
674
675 // Safety check for return state
676 try {
677 if (finalState != LOAD_STATE_COMPLETED
678 && finalState != LOAD_STATE_FAILED
679 && finalState != LOAD_STATE_INCOMPLETED) {
680 throw new Exception("ERROR: Bad return state: " + finalState);
681 }
682 } catch (Exception e) {
683 e.printStackTrace();
684 finalState = LOAD_STATE_COMPLETED;
685 }
686
687 // Set the final state
688 setLoadState(finalState, false);
689
690 }
691
692 private void doSave() {
693
694 // Only save if still belongs to a frame
695 if (!isFloating() && getParentFrame() != null) {
696 saveWidgetData();
697 }
698
699 }
700
701 private void doUnload() {
702
703 // Reset the load state
704 setLoadState(LOAD_STATE_PENDING, true);
705
706 // Get rid of memory
707 unloadWidgetData();
708
709 }
710
711 private void doTempUnload() {
712 // Reset the load state
713 setLoadState(LOAD_STATE_PENDING, true);
714
715 // Get rid of memory
716 tempUnloadWidgetData();
717 }
718
719
720
721 }
722
723 private enum HDWTask {
724 Save,
725 Load,
726 Unload,
727 UnloadTMP
728 }
729
730
731}
Note: See TracBrowser for help on using the repository browser.