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

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