source: trunk/src/org/expeditee/items/widgets/InteractiveWidget.java@ 217

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

Fixed bug with parent event filtering

File size: 38.7 KB
Line 
1package org.expeditee.items.widgets;
2
3import java.awt.Color;
4import java.awt.Component;
5import java.awt.Container;
6import java.awt.Graphics;
7import java.awt.Graphics2D;
8import java.awt.Point;
9import java.awt.Rectangle;
10import java.awt.event.MouseEvent;
11import java.lang.reflect.Constructor;
12import java.util.ArrayList;
13import java.util.Collection;
14import java.util.Collections;
15import java.util.LinkedList;
16import java.util.List;
17
18import javax.swing.JComponent;
19import javax.swing.SwingUtilities;
20
21import org.expeditee.gui.Browser;
22import org.expeditee.gui.DisplayIO;
23import org.expeditee.gui.Frame;
24import org.expeditee.gui.FrameGraphics;
25import org.expeditee.gui.FreeItems;
26import org.expeditee.gui.MouseEventRouter;
27import org.expeditee.items.Item;
28import org.expeditee.items.ItemParentStateChangedEvent;
29import org.expeditee.items.ItemUtils;
30import org.expeditee.items.Permission;
31import org.expeditee.items.Text;
32
33/**
34 * The bridge between swing space and expeditee space
35 *
36 * @author Brook
37 *
38 */
39public abstract class InteractiveWidget {
40
41 protected JComponent _swingComponent;
42
43 /** A widget is comprised of dots and lines that basically form a rectangle */
44 private WidgetCorner _d1, _d2, _d3, _d4;
45
46 private WidgetEdge _l1, _l2, _l3, _l4;
47
48 /*
49 * GUIDE: l1 d1-------d2 | | l4 | X | 12 | | d4-------d3 13
50 */
51 private List<Item> _expediteeItems; // used for quickly returning item list
52
53 // Widget size restrictions
54 private int _minWidth = 50;
55
56 private int _minHeight = 50;
57
58 private int _maxWidth = 300;
59
60 private int _maxHeight = 300;
61
62 // The Expeditee item that is used for saving widget state in expiditee
63 // world
64 private Text _textRepresentation;
65
66 protected final static Color FREESPACE_BACKCOLOR = new Color(100, 100, 100);
67
68 // A flag for signifying whether the swing components are ready to paint.
69 // If the swing components has not been layed out, if they are painted they
70 // will not draw in the correct positions.
71 // Also for setting AWT and Swing -Related drawing options after
72 // construction
73 private boolean _isReadyToPaint = false;
74
75 /** For ensuring only one event is listened to - instead of four. */
76 private MouseEvent _lastParentStateChangedMouseEvent = null;
77
78 /** For ensuring only one event is listened to - instead of four. */
79 private ItemParentStateChangedEvent _lastParentStateChangedEvent = null;
80
81 /** The minum border thickness for widgets. */
82 public final static float DEFAULT_MINIMUM_BORDER_THICKNESS = 1.0f;
83
84 /**
85 * Creates a InteractiveWidget from a text item.
86 *
87 * @param source
88 * Must not be null, first line of text used - which the format
89 * must be as follows: "@iw: <<widget_class_name>> [<<width>>] [<<height>>] [: [<<arg1>>] [<<arg2>>]
90 * [...]]".
91 *
92 * e.g: "@iw: org.expeditee.items.SampleWidget1 100 20 : 2" creates a
93 * SampleWidget1 with width = 100 and height = 20 with 1 argument = "2"
94 *
95 * @return An InteractiveWidget instance. Never null.
96 *
97 * @throws NullPointerException
98 * if source is null
99 *
100 * @throws IllegalArgumentException
101 * if source's text is in the incorrect format or if source's
102 * parent is null
103 *
104 * @throws InteractiveWidgetNotAvailableException
105 * If the given widget class name in the source text doesn't
106 * exist or not an InteractiveWidget or the widget does not
107 * supply a valid constructor for creation.
108 *
109 * @throws InteractiveWidgetInitialisationFailedException
110 * The sub-constructor failed - depends on the type of widget
111 * being instantainiated.
112 *
113 * class names beginning with $, the $ will be replaced with
114 * "org.expeditee.items."
115 */
116 public static InteractiveWidget createWidget(Text source)
117 throws InteractiveWidgetNotAvailableException,
118 InteractiveWidgetInitialisationFailedException {
119
120 if (source == null)
121 throw new NullPointerException("source");
122 if (source.getParent() == null)
123 throw new IllegalArgumentException(
124 "source's parent is null, InteractiveWidget's must be created from Text items with non-null parents");
125
126 String TAG = ItemUtils.GetTag(ItemUtils.TAG_IWIDGET);
127
128 String text = source.getText();
129 if (text == null)
130 throw new IllegalArgumentException("source does not have any text");
131
132 text = text.trim();
133
134 int index = 0;
135 if (text.length() > TAG.length() + 1) {
136 index = text.indexOf(':', TAG.length() + 1); // used for
137 // signifying start
138 // of
139 }
140
141 // arguments
142 if (index == 0)
143 throw new IllegalArgumentException("Source text must begin with \""
144 + TAG + "\"");
145
146 String[] tokens = (index == -1) ? text.split("\\s+") : text.substring(
147 0, index).split(" ");
148
149 // Check starts with the widget tag and seporator
150 if (tokens.length == 0 || !tokens[0].equals(TAG + ":"))
151 throw new IllegalArgumentException("Source text must begin with \""
152 + TAG + ":\"");
153
154 float width = -1, height = -1;
155
156 if (tokens.length < 2)
157 throw new IllegalArgumentException(
158 "Missing widget class name in source text");
159
160 try {
161
162 if (tokens.length >= 3) { // parse optional width
163 width = Integer.parseInt(tokens[2]);
164 width = (width <= 0) ? width = -1 : width;
165 }
166
167 if (tokens.length >= 4) { // parse optional height
168 height = Integer.parseInt(tokens[3]);
169 height = (height <= 0) ? height = -1 : height;
170 }
171
172 } catch (NumberFormatException nfe) {
173 throw new IllegalArgumentException(
174 "Bad width or height given in source text", nfe);
175 }
176
177 if (tokens.length > 4)
178 throw new IllegalArgumentException(
179 "to many arguments given before \":\" in source text");
180
181 String classname = tokens[1];
182 if (classname.charAt(0) == '$')
183 classname = "org.expeditee.items.widgets." + classname.substring(1);
184 // Attempt to locate the class using reflection
185 Class<?> iwclass = findIWidgetClass(classname);
186
187 if (iwclass == null) // ensure it exists
188 throw new InteractiveWidgetNotAvailableException(classname
189 + " does not exist or is not an InteractiveWidget");
190
191 // Extract out the parameters - if any
192 String[] args = null;
193 if (index > 0) { // index of the first ":"
194 args = (text.length() == (index + 1)) ? null : parseArgs(text
195 .substring(index + 1));
196 }
197
198 InteractiveWidget inst = null;
199 try {
200 // Instantainiate the widget - passing the params
201 Class parameterTypes[] = new Class[] { Text.class, String[].class };
202 Constructor ct = iwclass.getConstructor(parameterTypes);
203
204 Object arglist[] = new Object[] { source, args };
205
206 inst = (InteractiveWidget) ct.newInstance(arglist);
207 } catch (Exception e) {
208 throw new InteractiveWidgetNotAvailableException(
209 "Failed to create instance via reflection: " + e.toString(),
210 e);
211 }
212
213 // Use default dimensions if not provided (or provided as negitive
214 // values)
215 if (width <= 0)
216 width = inst.getWidth();
217 if (height <= 0)
218 height = inst.getHeight();
219
220 inst.setSize(width, height);
221
222 return inst;
223 }
224
225 /**
226 * Locates the class from the classname of an InteractiveWidget class
227 *
228 * @param classname
229 * The name of the class to search
230 * @return Null if doesn't exist or not an InteractiveWidget
231 */
232 private static Class findIWidgetClass(String classname) {
233 try {
234 Class c = Class.forName(classname); // attempt to find the class
235
236 // If one is found, ensure that it is a descendant of an
237 // InteractiveWidget
238 for (Class superclass = c.getSuperclass(); superclass != null
239 && superclass != Item.class; superclass = superclass
240 .getSuperclass()) {
241 if (superclass == InteractiveWidget.class)
242 return c;
243 }
244
245 } catch (ClassNotFoundException e) {
246 }
247
248 // Doesn't exist or not an InteractiveWidget
249 return null;
250 }
251
252 /**
253 * Using Microsofts commandline convention: Args seperated with white
254 * spaces. Options with white spaces enclosed with quotes. Args with quotes
255 * must be double quoted args1 args2=sfasas args3="option with spaces"
256 * arg""4""
257 *
258 * @param args
259 * Null and empty excepted
260 * @return An array of args. null if none provided
261 */
262 static String[] parseArgs(String args) {
263
264 if (args == null)
265 return null;
266
267 args = args.trim();
268 if (args.length() == 0)
269 return null;
270
271 List<String> vargs = new LinkedList<String>();
272 StringBuilder sb = new StringBuilder();
273 boolean quoteOn = false;
274 for (int i = 0; i < args.length(); i++) {
275
276 char c = args.charAt(i);
277 if (c == ' ' && !quoteOn) {
278
279 // Extract arg
280 vargs.add(sb.toString());
281 sb = new StringBuilder();
282
283 // Consume white spaces
284 while (args.charAt(++i) == ' ' && i < args.length()) {
285 }
286 i--;
287
288 } else if (c == '\"') {
289 // If double qouted
290 if (args.length() >= (i + 2) && args.charAt(i + 1) == '\"') {
291
292 sb.append(c); // add escaped
293 i++;
294
295 } else {
296 quoteOn = !quoteOn;
297 }
298
299 } else {
300 sb.append(c);
301 }
302 }
303
304 if (sb.length() > 0)
305 vargs.add(sb.toString());
306
307 if (vargs.size() == 0)
308 return null;
309 else
310 return vargs.toArray(new String[vargs.size()]);
311 }
312
313 /**
314 * Reverse of parseArgs
315 *
316 * @return Null if args is null or empty / all whitespace
317 */
318 public static String formatArgs(String[] args) {
319 if (args == null)
320 return null;
321
322 StringBuilder sb = new StringBuilder();
323
324 for (String s : args) {
325 if (s == null)
326 continue;
327
328 // Escape quotes
329 StringBuilder formatted = new StringBuilder(s.replaceAll("\"",
330 "\"\""));
331
332 // Encapsulate spaces
333 int index = formatted.indexOf(" ");
334 if (index >= 0) {
335 formatted.insert(index, "\"");
336 formatted.append('\"');
337 }
338
339 if (sb.length() > 0)
340 sb.append(' ');
341 sb.append(formatted);
342 }
343
344 return sb.length() > 0 ? sb.toString() : null;
345 }
346
347 /**
348 * Arguments represent the widgets <i>current state</i> state. They are
349 * used for saving, loading, creating and cloning Special formatting is done
350 * for you.
351 *
352 * @see #getData()
353 *
354 * @return Can be null for no params.
355 */
356 protected abstract String[] getArgs();
357
358 /**
359 * Data represents the widgets <i>current state</i> state. For any state
360 * information you do not wish to be defined as arguments (e.g. metadata),
361 * you can set as the widgets source items data.
362 *
363 * The default implementation returns null. Override to make use of.
364 *
365 * @see #getArgs()
366 *
367 * @return Null for for data. Otherwise the data that represent this widgets
368 * <i>current state</i>
369 */
370 protected List<String> getData() {
371 return null;
372 }
373
374 /**
375 * Constructor
376 *
377 * @param source
378 * Must not be null. Neither must it's parent
379 *
380 * @param component
381 * Must not be null.
382 *
383 * @param minWidth
384 * The min width restriction for the widget. If negative, then
385 * there is no restriction.
386 *
387 * @param maxWidth
388 * The max width restriction for the widget. If negative, then
389 * there is no restriction.
390 *
391 * @param minHeight
392 * The min height restriction for the widget. If negative, then
393 * there is no restriction.
394 *
395 * @param maxHeight
396 * The max height restriction for the widget. If negative, then
397 * there is no restriction.
398 *
399 * @throws NullPointerException
400 * If source, component if null.
401 *
402 * @throws IllegalArgumentException
403 * If source's parent is null. If maxWidth smaller than minWidth
404 * and maxWidth larger or equal to zero or if maxHeight smaller
405 * than minHeight && maxHeight is larger or equal to zero
406 *
407 */
408 protected InteractiveWidget(Text source, JComponent component,
409 int minWidth, int maxWidth, int minHeight, int maxHeight) {
410
411 if (component == null)
412 throw new NullPointerException("component");
413 if (source == null)
414 throw new NullPointerException("source");
415 if (source.getParent() == null)
416 throw new IllegalArgumentException(
417 "source's parent is null, InteractiveWidget's must be created from Text items with non-null parents");
418
419 _swingComponent = component;
420 _textRepresentation = source;
421
422 setSizeRestrictions(minWidth, maxWidth, minHeight, maxHeight); // throws
423 // IllegalArgumentException's
424
425 int x = source.getX();
426 int y = source.getY();
427 int width = (int) ((_minWidth < 0) ? 10 : _minWidth);
428 int height = (int) ((_minHeight < 0) ? 10 : _minHeight);
429
430 Frame idAllocator = _textRepresentation.getParent();
431
432 // create WidgetCorners
433 _d1 = new WidgetCorner(x, y, idAllocator.getNextItemID(), this);
434 _d2 = new WidgetCorner(x + width, y, idAllocator.getNextItemID(), this);
435 _d3 = new WidgetCorner(x + width, y + height, idAllocator
436 .getNextItemID(), this);
437 _d4 = new WidgetCorner(x, y + height, idAllocator.getNextItemID(), this);
438
439 // create WidgetEdges
440 _l1 = new WidgetEdge(_d1, _d2, idAllocator.getNextItemID(), this);
441 _l2 = new WidgetEdge(_d2, _d3, idAllocator.getNextItemID(), this);
442 _l3 = new WidgetEdge(_d3, _d4, idAllocator.getNextItemID(), this);
443 _l4 = new WidgetEdge(_d4, _d1, idAllocator.getNextItemID(), this);
444
445 Collection<Item> enclist = new ArrayList<Item>(4);
446 enclist.add(_d1);
447 enclist.add(_d2);
448 enclist.add(_d3);
449 enclist.add(_d4);
450 _d1.setEnclosedList(enclist);
451 _d2.setEnclosedList(enclist);
452 _d3.setEnclosedList(enclist);
453 _d4.setEnclosedList(enclist);
454
455 _expediteeItems = new ArrayList<Item>(8); // Note: order important
456 _expediteeItems.add(_d1);
457 _expediteeItems.add(_d2);
458 _expediteeItems.add(_d3);
459 _expediteeItems.add(_d4);
460 _expediteeItems.add(_l1);
461 _expediteeItems.add(_l2);
462 _expediteeItems.add(_l3);
463 _expediteeItems.add(_l4);
464
465 }
466
467 /**
468 * Sets the restrictions - checks values.
469 *
470 * @param minWidth
471 * The min width restriction for the widget. If negative, then
472 * there is no restriction.
473 *
474 * @param maxWidth
475 * The max width restriction for the widget. If negative, then
476 * there is no restriction.
477 *
478 * @param minHeight
479 * The min height restriction for the widget. If negative, then
480 * there is no restriction.
481 *
482 * @param maxHeight
483 * The max height restriction for the widget. If negative, then
484 * there is no restriction.
485 *
486 * @throws IllegalArgumentException
487 * If maxWidth smaller than minWidth and maxWidth larger or
488 * equal to zero or if maxHeight smaller than minHeight &&
489 * maxHeight is larger or equal to zero
490 *
491 */
492 private void setSizeRestrictions(int minWidth, int maxWidth, int minHeight,
493 int maxHeight) {
494
495 _minWidth = minWidth;
496
497 if (maxWidth < _minWidth && maxWidth >= 0)
498 throw new IllegalArgumentException(
499 "maxWidth smaller than the min Width");
500 _maxWidth = maxWidth;
501
502 _minHeight = minHeight;
503 if (maxHeight < _minHeight && maxHeight >= 0)
504 throw new IllegalArgumentException(
505 "maxHeight smaller than the min Height");
506 _maxHeight = maxHeight;
507 }
508
509 /**
510 * This can be overrided for creating custom copies. The default
511 * implementation creates a new widget based on the current state of the
512 * widget (via getArgs).
513 *
514 * @see InteractiveWidget#getArgs().
515 *
516 * @return A copy of this widget.
517 *
518 */
519 public InteractiveWidget copy()
520 throws InteractiveWidgetNotAvailableException,
521 InteractiveWidgetInitialisationFailedException {
522
523 Text t = _textRepresentation.copy();
524 String clonedAnnotation = getAnnotationString();
525 t.setText(clonedAnnotation);
526 t.setData(getData());
527 return InteractiveWidget.createWidget(t);
528
529 }
530
531 /**
532 * Notifies widget of delete
533 */
534 public void onDelete() {
535
536 // Allocate new ID's
537 Frame parent = getParentFrame();
538 if (parent == null)
539 parent = DisplayIO.getCurrentFrame();
540
541 if (parent != null) {
542 for (Item i : _expediteeItems) {
543 i.setID(parent.getNextItemID());
544 }
545 }
546
547 invalidateLink();
548
549 }
550
551 /**
552 * Note updates the source text with current state info
553 *
554 * @return The Text item that this widget was created from.
555 */
556 public Item getSource() {
557
558 // Build the annotation string such that it represents this widgets
559 // current state
560 String newAnnotation = getAnnotationString();
561
562 // Set the new text
563 _textRepresentation.setText(newAnnotation);
564
565 // Set the data
566 _textRepresentation.setData(getData());
567
568 return _textRepresentation;
569 }
570
571 /**
572 * @return The current representation for this widget. The representation
573 * stores link infomation, data etc... It is used for saving and
574 * loading of the widget. Never null.
575 *
576 */
577 protected Item getCurrentRepresentation() {
578 return _textRepresentation;
579 }
580
581 /**
582 * @return The expeditee anotation string.
583 */
584 private String getAnnotationString() {
585
586 // Create tag and append classname
587 StringBuilder sb = new StringBuilder(ItemUtils
588 .GetTag(ItemUtils.TAG_IWIDGET));
589 sb.append(':');
590 sb.append(' ');
591 sb.append(getClass().getName());
592
593 // Append size information if needed (not an attibute of text items)
594 if (!isFixedSize()) {
595 sb.append(' ');
596 sb.append(getWidth());
597 sb.append(' ');
598 sb.append(getHeight());
599 }
600
601 // Append arguments if any
602 String stateArgs = InteractiveWidget.formatArgs(getArgs());
603 if (stateArgs != null) {
604 sb.append(':');
605 sb.append(stateArgs);
606 }
607
608 return sb.toString();
609 }
610
611 /**
612 * Sets both the new size as well as the new min/max widdet/height
613 * restricitons.
614 *
615 * @param minWidth
616 * The min width restriction for the widget. If negative, then
617 * there is no restriction.
618 *
619 * @param maxWidth
620 * The max width restriction for the widget. If negative, then
621 * there is no restriction.
622 *
623 * @param minHeight
624 * The min height restriction for the widget. If negative, then
625 * there is no restriction.
626 *
627 * @param maxHeight
628 * The max height restriction for the widget. If negative, then
629 * there is no restriction.
630 *
631 * @param newWidth
632 * Clamped to new restrictions.
633 *
634 * @param newHeight
635 * Clamped to new restrictions.
636 *
637 * @throws IllegalArgumentException
638 * If maxWidth smaller than minWidth and maxWidth larger or
639 * equal to zero or if maxHeight smaller than minHeight &&
640 * maxHeight is larger or equal to zero
641 *
642 * @see #setSize(float, float)
643 *
644 */
645 public void setSize(int minWidth, int maxWidth, int minHeight,
646 int maxHeight, float newWidth, float newHeight) {
647 setSizeRestrictions(minWidth, maxWidth, minHeight, maxHeight); // throws IllegalArgumentException's
648 setSize(newWidth, newHeight);
649 }
650
651 /**
652 * Clamped to current min/max width/height.
653 *
654 * @param width
655 * Clamped to current restrictions.
656 * @param height
657 * Clamped to current restrictions.
658 *
659 * @see #setSize(int, int, int, int, float, float)
660 */
661 public void setSize(float width, float height) {
662
663 // Clamp
664 if (width < _minWidth && _minWidth >= 0)
665 width = _minWidth;
666 else if (width > _maxWidth && _maxWidth >= 0)
667 width = _maxWidth;
668
669 if (height < _minHeight && _minHeight >= 0)
670 height = _minHeight;
671 else if (height > _maxHeight && _maxHeight >= 0)
672 height = _maxHeight;
673
674 boolean vfloating[] = new boolean[] { _d1.isFloating(),
675 _d2.isFloating(), _d3.isFloating(), _d4.isFloating() };
676
677 _d1.setFloating(true);
678 _d2.setFloating(true);
679 _d3.setFloating(true);
680 _d4.setFloating(true);
681
682 float x = _d1.getX() + width;
683 float y = _d1.getY() + height;
684
685 _d2.setX(x);
686 _d3.setX(x);
687 _d3.setY(y);
688 _d4.setY(y);
689
690 _d1.setFloating(vfloating[0]);
691 _d2.setFloating(vfloating[1]);
692 _d3.setFloating(vfloating[2]);
693 _d4.setFloating(vfloating[3]);
694
695 onSizeChanged();
696 }
697
698 public void setPosition(int x, int y) {
699 if (x == getX() && y == getY()) return;
700
701 boolean vfloating[] = new boolean[] { _d1.isFloating(),
702 _d2.isFloating(), _d3.isFloating(), _d4.isFloating() };
703
704 int width = getWidth();
705 int height = getHeight();
706
707 invalidateLink();
708
709 _d1.setFloating(true);
710 _d2.setFloating(true);
711 _d3.setFloating(true);
712 _d4.setFloating(true);
713
714 _d1.setPosition(x, y);
715 _d2.setPosition(x + width, y);
716 _d3.setPosition(x + width, y + height);
717 _d4.setPosition(x, y + height);
718
719 _d1.setFloating(vfloating[0]);
720 _d2.setFloating(vfloating[1]);
721 _d3.setFloating(vfloating[2]);
722 _d4.setFloating(vfloating[3]);
723
724 invalidateLink();
725
726 onMoved();
727
728 }
729
730 private boolean _settingPositionFlag = false; // used for recursion
731
732 /**
733 * to be called from corners only
734 *
735 * @param src
736 * @param x
737 * @param y
738 * @return False if need to call super.setPosition
739 */
740 boolean setPositions(WidgetCorner src, float x, float y) {
741
742 if (_settingPositionFlag)
743 return false;
744 _settingPositionFlag = true;
745
746 invalidateLink();
747
748 // Check to see if the widget is fully being picked up
749 boolean isAllPickedUp = (_d1.isFloating() && _d2.isFloating()
750 && _d3.isFloating() && _d4.isFloating());
751
752 // If so, then this will be called one by one ..
753 if (isAllPickedUp) {
754 src.setPosition(x, y);
755 } else {
756
757 float newX = x;
758
759 // Reference:
760 // D1 D2
761 // D3 D4
762
763 //
764 // GUIDE:
765 // l1
766 // d1-------d2
767 // | |
768 // l4 | X | 12
769 // | |
770 // d4-------d3
771 // 13
772 //
773
774 // X Positions
775 if (src == _d1 || src == _d4) {
776
777 // Check min X constraint
778 if (_minWidth >= 0) {
779 if ((_d2.getX() - x) < _minWidth) {
780 newX = _d2.getX() - _minWidth;
781 }
782 }
783 // Check max X constraint
784 if (_maxWidth >= 0) {
785 if ((_d2.getX() - x) > _maxWidth) {
786 newX = _d2.getX() - _maxWidth;
787 }
788 }
789
790 if (!(src == _d4 && _d1.isFloating() && _d4.isFloating()))
791 _d1.setX(newX);
792 if (!(src == _d1 && _d4.isFloating() && _d1.isFloating()))
793 _d4.setX(newX);
794
795 } else if (src == _d2 || src == _d3) {
796
797 // Check min X constraint
798 if (_minWidth >= 0) {
799 if ((x - _d1.getX()) < _minWidth) {
800 newX = _d1.getX() + _minWidth;
801 }
802 }
803 // Check max X constraint
804 if (_maxWidth >= 0) {
805 if ((x - _d1.getX()) > _maxWidth) {
806 newX = _d1.getX() + _maxWidth;
807 }
808 }
809
810 if (!(src == _d3 && _d2.isFloating() && _d3.isFloating()))
811 _d2.setX(newX);
812 if (!(src == _d2 && _d3.isFloating() && _d2.isFloating()))
813 _d3.setX(newX);
814 }
815
816 float newY = y;
817
818 // Y Positions
819 if (src == _d1 || src == _d2) {
820
821 // Check min Y constraint
822 if (_minHeight >= 0) {
823 if ((_d4.getY() - y) < _minHeight) {
824 newY = _d4.getY() - _minHeight;
825 }
826 }
827 // Check max Y constraint
828 if (_maxHeight >= 0) {
829 if ((_d4.getY() - y) > _maxHeight) {
830 newY = _d4.getY() - _maxHeight;
831 }
832 }
833
834 if (!(src == _d2 && _d1.isFloating() && _d2.isFloating()))
835 _d1.setY(newY);
836 if (!(src == _d1 && _d2.isFloating() && _d1.isFloating()))
837 _d2.setY(newY);
838
839 } else if (src == _d3 || src == _d4) {
840
841 // Check min Y constraint
842 if (_minHeight >= 0) {
843 if ((y - _d1.getY()) < _minHeight) {
844 newY = _d1.getY() + _minHeight;
845 }
846 }
847 // Check max Y constraint
848 if (_maxHeight >= 0) {
849 if ((y - _d1.getY()) > _maxHeight) {
850 newY = _d1.getY() + _maxHeight;
851 }
852 }
853
854 if (!(src == _d4 && _d3.isFloating() && _d4.isFloating()))
855 _d3.setY(newY);
856 if (!(src == _d3 && _d4.isFloating() && _d3.isFloating()))
857 _d4.setY(newY);
858 }
859 }
860
861 // Update source text position so position is remembered from loading
862 float newTextX = getX();
863 float newTextY = getY();
864 if (_textRepresentation.getX() != newTextX
865 || _textRepresentation.getY() != newTextY)
866 _textRepresentation.setPosition(newTextX, newTextY);
867
868 _settingPositionFlag = false;
869
870 invalidateLink();
871
872 onMoved();
873
874 return true;
875 }
876
877 public int getX() {
878 return Math.min(_d1.getX(), _d2.getX());
879 }
880
881 public int getY() {
882 return Math.min(_d1.getY(), _d4.getY());
883 }
884
885 public int getWidth() {
886
887 return Math.abs(_d2.getX() - _d1.getX());
888 }
889
890 public int getHeight() {
891 return Math.abs(_d4.getY() - _d1.getY());
892 }
893
894 /**
895 * The order of the items in the list is as specified: _d1 _d2 _d3 _d4 _l1
896 * _l2 _l3 _l4
897 *
898 * @return All of the Expeditee items that form the bounderies of this
899 * widget in an unmodifiable list
900 */
901 public List<Item> getItems() {
902 return Collections.unmodifiableList(_expediteeItems);
903 }
904
905 public JComponent getComponant() {
906 return _swingComponent;
907 }
908
909 public final void onParentStateChanged(ItemParentStateChangedEvent e) {
910
911 // Because widgets are comprised of four corners - they all report this
912 // event one after the
913 // other. So must filter out redunant notifications like so:
914 if (_lastParentStateChangedEvent != null
915 && _lastParentStateChangedEvent.getEventType() == e
916 .getEventType()
917 && _lastParentStateChangedMouseEvent == MouseEventRouter
918 .getCurrentMouseEvent())
919 return; // already dealt with this event
920
921 _lastParentStateChangedEvent = e;
922 _lastParentStateChangedMouseEvent = MouseEventRouter.getCurrentMouseEvent();
923
924 switch (e.getEventType()) {
925
926 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
927 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
928 case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
929 if (_swingComponent.getParent() != null) {
930 _swingComponent.getParent().remove(_swingComponent);
931 }
932 break;
933
934 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
935 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
936 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
937 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
938 if (_swingComponent.getParent() == null) {
939 addJComponantToFrame(e);
940 }
941 break;
942
943 }
944
945 FrameGraphics.invalidateItem(_d1, _swingComponent.getBounds());
946
947 // Forward filtered event to upper classeses...
948 onParentStateChanged(e.getEventType());
949 }
950
951 /**
952 * Override to make use of. Internally this is reported once by all corners,
953 * but is filterted out so that this method is invoked once per event.
954 *
955 * @param eventType
956 * The {@link ItemParentStateChangedEvent#getEventType()} that
957 * occured.
958 *
959 */
960 protected void onParentStateChanged(int eventType) {
961 }
962
963 protected void addJComponantToFrame(ItemParentStateChangedEvent e) {
964
965 if ((e.getEventType() == ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY || e
966 .getEventType() == ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY)
967 && e.getOverlayLevel().equals(Permission.none)) {
968 return; // item belongs to a non-active overlay
969 }
970
971 if (_swingComponent.getParent() == null) {
972
973 if (Browser._theBrowser != null) {
974 // Due to precaching - before adding physical swing
975 // componant must check to see that this widget belongs to a
976 // frame that is
977 // considered current. If the widget is shown however this does
978 // not apply -
979 // since it has been explicitly made clear the the widget is
980 // shown.
981 if (e.getEventType() == ItemParentStateChangedEvent.EVENT_TYPE_SHOWN
982 || e.getEventType() == ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY
983 || e.getSource() == DisplayIO.getCurrentFrame()) {
984
985 onBoundsChanged();
986 Browser._theBrowser.getContentPane().add(_swingComponent);
987 layout(_swingComponent);
988 }
989
990 } else { // if widgets exist on startup frame this will occur
991
992 synchronized (_widgetsToAddLater) {
993 _widgetsToAddLater.add(new DelayedWidgetEvent(this, e));
994 }
995 SwingUtilities.invokeLater(new AddToFrameLater());
996 }
997
998 }
999
1000 }
1001
1002 /**
1003 * @return True if at least one corner is floating
1004 */
1005 public boolean isFloating() {
1006 return _d1.isFloating() || _d2.isFloating() || _d3.isFloating()
1007 || _d4.isFloating();
1008 }
1009
1010 public boolean areCornersFullyAnchored() {
1011 return _d1.getParent() != null && _d2.getParent() != null
1012 && _d3.getParent() != null && _d4.getParent() != null;
1013 }
1014
1015 /**
1016 * Used for passing info to the swing thread
1017 *
1018 * @author Brook Novak
1019 */
1020 private class DelayedWidgetEvent {
1021
1022 DelayedWidgetEvent(InteractiveWidget widget,
1023 ItemParentStateChangedEvent e) {
1024 _widget = widget;
1025 _e = e;
1026 }
1027
1028 InteractiveWidget _widget;
1029
1030 ItemParentStateChangedEvent _e;
1031 }
1032
1033 /**
1034 * Must be able to add widgets on first loaded frame: these are loaded
1035 * before the browser singleton is made available.
1036 */
1037 private static List<DelayedWidgetEvent> _widgetsToAddLater = new LinkedList<DelayedWidgetEvent>();
1038
1039 /**
1040 * Ensures widgets are added correctly to first loaded frame
1041 */
1042 class AddToFrameLater implements Runnable {
1043 public void run() {
1044 if (!_widgetsToAddLater.isEmpty()) {
1045 List<DelayedWidgetEvent> tmp = null;
1046 synchronized (_widgetsToAddLater) {
1047 tmp = new LinkedList<DelayedWidgetEvent>(_widgetsToAddLater);
1048 }
1049 _widgetsToAddLater.clear();
1050 for (DelayedWidgetEvent iwi : tmp) {
1051 iwi._widget.addJComponantToFrame(iwi._e);
1052 iwi._widget.invalidateSelf();
1053 }
1054 }
1055 }
1056 }
1057
1058 final void onBoundsChanged() {
1059 if (isFixedSize())
1060 _swingComponent.setBounds(getX(), getY(), _maxWidth, _maxHeight);
1061 else
1062 _swingComponent.setBounds(getX(), getY(), getWidth(), getHeight());
1063 }
1064
1065 /**
1066 * Due to absolute positioning...
1067 *
1068 * @param parent
1069 */
1070 private void layout(Component parent) {
1071
1072 parent.validate();
1073
1074 if (parent instanceof Container) {
1075 for (Component c : ((Container) parent).getComponents()) {
1076
1077 if (c instanceof Container)
1078 layout(c);
1079 else
1080 c.validate();
1081 }
1082 }
1083
1084 }
1085
1086 private void ignoreAWTPainting(Component c) {
1087
1088 if (c instanceof JComponent) {
1089 ((JComponent) c).setDoubleBuffered(false);
1090 }
1091
1092 c.setIgnoreRepaint(true);
1093
1094 if (c instanceof Container) {
1095 for (Component child : ((Container) c).getComponents()) {
1096
1097 if (child instanceof Container) {
1098 ignoreAWTPainting(child);
1099 } else {
1100 if (child instanceof JComponent) {
1101 ((JComponent) child).setDoubleBuffered(false);
1102 }
1103
1104 child.setIgnoreRepaint(true);
1105 }
1106 }
1107 }
1108 }
1109
1110 private void prepareToPaint() {
1111 _isReadyToPaint = true;
1112 layout(_swingComponent);
1113 ignoreAWTPainting(_swingComponent);
1114 }
1115
1116 /**
1117 * Paints the widget excluding the boundries. That is, the Swing graphics
1118 *
1119 * @param g
1120 */
1121 public void paint(Graphics g) {
1122
1123 if (!_isReadyToPaint) {
1124 prepareToPaint();
1125 }
1126
1127 Point loc = _swingComponent.getLocation();
1128
1129 g.translate(loc.x, loc.y - 1);
1130 _swingComponent.paint(g);
1131 g.translate(-loc.x, -(loc.y - 1));
1132
1133 paintLink((Graphics2D) g);
1134
1135 }
1136
1137 protected void paintLink(Graphics2D g) {
1138 // If this widget is linked .. then draw the link icon
1139 if (_textRepresentation.getLink() != null
1140 || _textRepresentation.hasAction()) {
1141
1142 _textRepresentation.paintLinkGraphic(g, getLinkX(), getLinkY());
1143
1144 }
1145
1146 }
1147
1148 private int getLinkX() {
1149 return getX() - Item.LEFT_MARGIN;
1150 }
1151
1152 private int getLinkY() {
1153 return getY() + (getHeight() / 2);
1154 }
1155
1156 /**
1157 * Invoked whenever the widget is to be repainted in free space.
1158 *
1159 * @param g
1160 */
1161 protected void paintInFreeSpace(Graphics g) {
1162 g.setColor(FREESPACE_BACKCOLOR);
1163 g.fillRect(getX(), getY(), getWidth(), getHeight());
1164 }
1165
1166 /**
1167 * Called from the widgets corners: Whenever one of the corners are invoked
1168 * for a refill of the enclosed area.
1169 *
1170 * If the widget is floating (e.g. currently picked up / rubberbanding) then
1171 * a shaded area is drawn instead of the actual widget so the manipulation
1172 * of the widget is as smooth as possible.
1173 *
1174 * @param g
1175 * @param notifier
1176 */
1177 void paintFill(Graphics g) {
1178 if (_swingComponent.getParent() == null) {
1179 // Note that frames with @f may want to paint the widgets so do not
1180 // paint over the widget interface in these cases: must only
1181 // paint if an object is floating
1182 if (isFloating()) {
1183 paintInFreeSpace(g);
1184 paintLink((Graphics2D) g);
1185 }
1186 }
1187 }
1188
1189 /**
1190 * @return True if this widget cannot be resized in either directions
1191 */
1192 public boolean isFixedSize() {
1193 return this._minHeight == this._maxHeight
1194 && this._minWidth == this._maxWidth && this._minHeight >= 0
1195 && this._minWidth >= 0;
1196 }
1197
1198 /**
1199 * Removes this widget from the parent frame or free space.
1200 *
1201 * @return True if removed from a parent frame. Thus a parent changed event
1202 * will be invoked.
1203 *
1204 * False if removed purely from free space.
1205 */
1206 protected boolean removeSelf() {
1207
1208 Frame parent = getParentFrame();
1209
1210 if (parent != null) {
1211 parent.removeAllItems(_expediteeItems);
1212 }
1213
1214 FreeItems.getInstance().removeAll(_expediteeItems);
1215
1216 return (parent != null);
1217
1218 }
1219
1220 /**
1221 * @return The parent frame. Null if has none. Note: Based on corners
1222 * parents.
1223 */
1224 public Frame getParentFrame() {
1225
1226 Frame parent = null;
1227 if (_d1.getParent() != null)
1228 parent = _d1.getParent();
1229 else if (_d2.getParent() != null)
1230 parent = _d2.getParent();
1231 else if (_d3.getParent() != null)
1232 parent = _d3.getParent();
1233 else if (_d4.getParent() != null)
1234 parent = _d4.getParent();
1235
1236 return parent;
1237 }
1238
1239 protected void invalidateSelf() {
1240 Rectangle dirty = new Rectangle((int) getX(), (int) getY(),
1241 (int) getWidth(), (int) getHeight());
1242 FrameGraphics.invalidateArea(dirty);
1243 invalidateLink();
1244 FrameGraphics.refresh(true);
1245 }
1246
1247 /**
1248 * Invalidates the link for this widget - if it has one.
1249 */
1250 void invalidateLink() {
1251 if (_textRepresentation.getLink() != null
1252 || _textRepresentation.hasAction()) {
1253 Rectangle linkArea = _textRepresentation.getLinkDrawArea(
1254 getLinkX(), getLinkY());
1255 FrameGraphics.invalidateArea(linkArea);
1256 }
1257
1258 }
1259
1260 /**
1261 * @see ItemUtils#isVisible(Item)
1262 *
1263 * @return True if this widget is visible from the current frame. Considers
1264 * overlays and vectors.
1265 *
1266 */
1267 public boolean isVisible() {
1268 return ItemUtils.isVisible(_d1);
1269 }
1270
1271 /**
1272 * Invoked whenever the widget have moved. Can override.
1273 *
1274 */
1275 protected void onMoved() {
1276 }
1277
1278 /**
1279 * Invoked whenever the widget have moved. Can override.
1280 *
1281 */
1282 protected void onSizeChanged() {
1283 }
1284
1285 /**
1286 * Override to have a custom min border thickness for your widget.
1287 *
1288 * @see #DEFAULT_MINIMUM_BORDER_THICKNESS
1289 *
1290 * @return The minimum border thickness. Should be larger or equal to zero.
1291 *
1292 */
1293 public float getMinimumBorderThickness() {
1294 return DEFAULT_MINIMUM_BORDER_THICKNESS;
1295 }
1296
1297 /**
1298 * Looks fors a dataline in the current representation of the widget.
1299 *
1300 * @see #getCurrentRepresentation
1301 * @see #getStrippedDataInt(String, int)
1302 * @see #getStrippedDataLong(String, long)
1303 *
1304 * @param tag
1305 * The prefix of a dataline that will be matched. Must be larger
1306 * the zero and not null.
1307 *
1308 * @return The <i>first</i> dataline that matched the prefix - without the
1309 * prefix. Null if their was no data that matched the given prefix.
1310 *
1311 * @throws IllegalArgumentException
1312 * If tag is null.
1313 *
1314 * @throws NullPointerException
1315 * If tag is empty.
1316 */
1317 protected String getStrippedDataString(String tag) {
1318 if (tag == null)
1319 throw new NullPointerException("tag");
1320 else if (tag.length() == 0)
1321 throw new IllegalArgumentException("tag is empty");
1322
1323 if (getCurrentRepresentation().getData() != null) {
1324 for (String str : getCurrentRepresentation().getData()) {
1325 if (str != null && str.startsWith(tag)
1326 && str.length() > tag.length()) {
1327 return str.substring(tag.length());
1328 }
1329 }
1330 }
1331 return null;
1332 }
1333
1334 /**
1335 * Looks fors a dataline in the current representation of the widget.
1336 *
1337 * @see #getCurrentRepresentation
1338 * @see #getStrippedDataString(String)
1339 * @see #getStrippedDataLong(String, long)
1340 *
1341 * @param tag
1342 * The prefix of a dataline that will be matched. Must be larger
1343 * the zero and not null.
1344 *
1345 * @param defaultValue
1346 * The default value if the tag does not exist or contains
1347 * invalid data.
1348 *
1349 * @return The <i>first</i> dataline that matched the prefix - parsed as an
1350 * int (after the prefix). defaultValue if their was no data that
1351 * matched the given prefix or the data was invalid.
1352 *
1353 * @throws IllegalArgumentException
1354 * If tag is null.
1355 *
1356 * @throws NullPointerException
1357 * If tag is empty.
1358 *
1359 */
1360 protected Integer getStrippedDataInt(String tag, Integer defaultValue) {
1361
1362 String strippedStr = getStrippedDataString(tag);
1363
1364 if (strippedStr != null) {
1365 strippedStr = strippedStr.trim();
1366 if (strippedStr.length() > 0) {
1367 try {
1368 return Integer.parseInt(strippedStr);
1369 } catch (NumberFormatException e) { /* Consume */
1370 }
1371 }
1372 }
1373
1374 return defaultValue;
1375 }
1376
1377 /**
1378 * Looks fors a dataline in the current representation of the widget.
1379 *
1380 * @see #getCurrentRepresentation
1381 * @see #getStrippedDataString(String)
1382 * @see #getStrippedDataInt(String, int)
1383 *
1384 * @param tag
1385 * The prefix of a dataline that will be matched. Must be larger
1386 * the zero and not null.
1387 *
1388 * @param defaultValue
1389 * The default value if the tag does not exist or contains
1390 * invalid data.
1391 *
1392 * @return The <i>first</i> dataline that matched the prefix - parsed as a
1393 * long (after the prefix). defaultValue if their was no data that
1394 * matched the given prefix or the data was invalid.
1395 *
1396 * @throws IllegalArgumentException
1397 * If tag is null.
1398 *
1399 * @throws NullPointerException
1400 * If tag is empty.
1401 *
1402 */
1403 protected Long getStrippedDataLong(String tag, Long defaultValue) {
1404 String strippedStr = getStrippedDataString(tag);
1405
1406 if (strippedStr != null) {
1407 strippedStr = strippedStr.trim();
1408 if (strippedStr.length() > 0) {
1409 try {
1410 return Long.parseLong(strippedStr);
1411 } catch (NumberFormatException e) { /* Consume */
1412 }
1413 }
1414 }
1415
1416 return defaultValue;
1417 }
1418
1419 /**
1420 * All data is removed that is prefixed with the given tag.
1421 *
1422 * @param tag
1423 * The prefix of the data lines to remove. Must be larger the
1424 * zero and not null.
1425 *
1426 * @throws IllegalArgumentException
1427 * If tag is null.
1428 *
1429 * @throws NullPointerException
1430 * If tag is empty.
1431 *
1432 */
1433 protected void removeData(String tag) {
1434 updateData(tag, null);
1435 }
1436
1437 /**
1438 * Updates the data with a given tag. All data is removed that is prefixed
1439 * with the given tag. Then a new line is added (if not null).
1440 *
1441 * Note that passing newData with null is the equivelant of removing tag
1442 * lines.
1443 *
1444 * @param tag
1445 * The prefix of the data lines to remove. Must be larger the
1446 * zero and not null.
1447 *
1448 * @param newData
1449 * The new line to add. Can be null - for not adding anything.
1450 *
1451 * @throws IllegalArgumentException
1452 * If tag is null.
1453 *
1454 * @throws NullPointerException
1455 * If tag is empty.
1456 *
1457 * @see #removeData(String)
1458 *
1459 */
1460 protected void updateData(String tag, String newData) {
1461 if (tag == null)
1462 throw new NullPointerException("tag");
1463 else if (tag.length() == 0)
1464 throw new IllegalArgumentException("tag is empty");
1465
1466 // Get current data
1467 List<String> data = getCurrentRepresentation().getData();
1468
1469 if (data != null) {
1470 for (int i = 0; i < data.size(); i++) {
1471 String str = data.get(i);
1472 if (str != null && str.startsWith(tag)) {
1473 data.remove(i);
1474 }
1475 }
1476 }
1477
1478 if (newData != null) {
1479 data.add(newData);
1480 }
1481 }
1482
1483 /**
1484 * Sets the link for this widget.
1485 *
1486 * @param link
1487 * The new frame link. Can be null (for no link)
1488 *
1489 */
1490 public void setLink(String link) {
1491 _textRepresentation.setLink(link);
1492 }
1493
1494 /**
1495 * @return
1496 * The link for this widget. Null if none.
1497 */
1498 public String getLink() {
1499 return _textRepresentation.getLink();
1500 }
1501
1502}
Note: See TracBrowser for help on using the repository browser.