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

Last change on this file since 963 was 919, checked in by jts21, 10 years ago

Added license headers to all files, added full GPL3 license file, moved license header generator script to dev/bin/scripts

File size: 59.2 KB
Line 
1/**
2 * InteractiveWidget.java
3 * Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19package org.expeditee.items.widgets;
20
21import java.awt.Color;
22import java.awt.Component;
23import java.awt.Container;
24import java.awt.Graphics;
25import java.awt.Graphics2D;
26import java.awt.Point;
27import java.awt.Rectangle;
28import java.awt.event.ContainerEvent;
29import java.awt.event.ContainerListener;
30import java.awt.event.KeyEvent;
31import java.awt.event.KeyListener;
32import java.awt.event.MouseEvent;
33import java.io.PrintWriter;
34import java.io.StringWriter;
35import java.lang.reflect.Constructor;
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.Collection;
39import java.util.Collections;
40import java.util.LinkedList;
41import java.util.List;
42import java.util.StringTokenizer;
43
44import javax.swing.JComponent;
45import javax.swing.SwingUtilities;
46
47import org.apache.commons.cli.CommandLine;
48import org.apache.commons.cli.CommandLineParser;
49import org.apache.commons.cli.GnuParser;
50import org.apache.commons.cli.HelpFormatter;
51import org.apache.commons.cli.Option;
52import org.apache.commons.cli.OptionBuilder;
53import org.apache.commons.cli.Options;
54import org.apache.commons.cli.ParseException;
55import org.expeditee.actions.Actions;
56import org.expeditee.gui.Browser;
57import org.expeditee.gui.DisplayIO;
58import org.expeditee.gui.Frame;
59import org.expeditee.gui.FrameGraphics;
60import org.expeditee.gui.FrameIO;
61import org.expeditee.gui.FrameKeyboardActions;
62import org.expeditee.gui.FreeItems;
63import org.expeditee.gui.MouseEventRouter;
64import org.expeditee.items.Item;
65import org.expeditee.items.ItemParentStateChangedEvent;
66import org.expeditee.items.ItemUtils;
67import org.expeditee.items.Text;
68import org.expeditee.items.UserAppliedPermission;
69
70/**
71 * The bridge between swing space and Expeditee space
72 *
73 * @author Brook
74 *
75 */
76public abstract class InteractiveWidget implements ContainerListener, KeyListener {
77
78 protected JComponent _swingComponent;
79
80 /** A widget is comprised of dots and lines that basically form a rectangle */
81 private WidgetCorner _d1, _d2, _d3, _d4;
82
83 private WidgetEdge _l1, _l2, _l3, _l4;
84
85 /*
86 * GUIDE: l1 d1-------d2 | | l4 | X | 12 | | d4-------d3 13
87 */
88 private List<Item> _expediteeItems; // used for quickly returning item list
89
90 // Widget size restrictions
91 private int _minWidth = 50;
92
93 private int _minHeight = 50;
94
95 private int _maxWidth = 300;
96
97 private int _maxHeight = 300;
98
99 // The Expeditee item that is used for saving widget state in expeditee
100 // world
101 protected Text _textRepresentation;
102
103 protected Float _anchorLeft;
104 protected Float _anchorRight;
105 protected Float _anchorTop;
106 protected Float _anchorBottom;
107
108 protected final static Color FREESPACE_BACKCOLOR = new Color(100, 100, 100);
109
110 // A flag for signifying whether the swing components are ready to paint.
111 // If the swing components has not been layed out, if they are painted they
112 // will not draw in the correct positions.
113 // Also for setting AWT and Swing -Related drawing options after
114 // construction
115 private boolean _isReadyToPaint = false;
116
117 /** For ensuring only one event is listened to - instead of four. */
118 private MouseEvent _lastParentStateChangedMouseEvent = null;
119
120 /** For ensuring only one event is listened to - instead of four. */
121 private ItemParentStateChangedEvent _lastParentStateChangedEvent = null;
122
123 /** The minum border thickness for widgets. */
124 public final static float DEFAULT_MINIMUM_BORDER_THICKNESS = 1.0f;
125
126 /**
127 * Creates a InteractiveWidget from a text item.
128 *
129 * @param source
130 * Must not be null, first line of text used - which the format
131 * must be as follows: "@iw: <<widget_class_name>> [<<width>>] [<<height>>] [: [<<arg1>>] [<<arg2>>]
132 * [...]]".
133 *
134 * e.g: "@iw: org.expeditee.items.SampleWidget1 100 20 : 2" creates a
135 * SampleWidget1 with width = 100 and height = 20 with 1 argument = "2"
136 *
137 * @return An InteractiveWidget instance. Never null.
138 *
139 * @throws NullPointerException
140 * if source is null
141 *
142 * @throws IllegalArgumentException
143 * if source's text is in the incorrect format or if source's
144 * parent is null
145 *
146 * @throws InteractiveWidgetNotAvailableException
147 * If the given widget class name in the source text doesn't
148 * exist or not an InteractiveWidget or the widget does not
149 * supply a valid constructor for creation.
150 *
151 * @throws InteractiveWidgetInitialisationFailedException
152 * The sub-constructor failed - depends on the type of widget
153 * being instantainiated.
154 *
155 * class names beginning with $, the $ will be replaced with
156 * "org.expeditee.items."
157 */
158 public static InteractiveWidget createWidget(Text source)
159 throws InteractiveWidgetNotAvailableException,
160 InteractiveWidgetInitialisationFailedException {
161
162 if (source == null)
163 throw new NullPointerException("source");
164 if (source.getParent() == null)
165 throw new IllegalArgumentException(
166 "source's parent is null, InteractiveWidget's must be created from Text items with non-null parents");
167
168 String TAG = ItemUtils.GetTag(ItemUtils.TAG_IWIDGET);
169
170 String text = source.getText();
171 if (text == null) {
172 throw new IllegalArgumentException("source does not have any text");
173 }
174
175 text = text.trim();
176
177 // Check starts with the widget tag and separator
178 if (!text.startsWith(TAG + ":"))
179 throw new IllegalArgumentException("Source text must begin with \"" + TAG + ":\"");
180
181 // skip over the '@iw:' preamble
182 text = text.substring(TAG.length() + 1).trim();
183
184 int index = 0;
185 if (text.length() > 0) {
186 // Having removed @iw:, then next ':' is used for signifying start of arguments
187 index = text.indexOf(':');
188 }
189
190 if (index == 0) {
191 throw new IllegalArgumentException("Source text must begin with \"" + TAG + "\"");
192 }
193
194 //
195 // Step 1:
196 // For an X-rayable text item in the form:
197 // @iw: <class name> [options] width height : <rest...>
198 //
199 // Parse the 'core' part of the X-rayable text item
200 // i.e, the text between the first ':' and the second ':'
201 //
202 //
203
204 String tokens_str = (index == -1) ? text : text.substring(0, index);
205
206 String[] tokens = parseArgsApache(tokens_str);
207
208 // make anything starting with a '-' lowercase
209 for (int i=0; i<tokens.length; i++) {
210 if (tokens[i].startsWith("-")) {
211 tokens[i] = tokens[i].toLowerCase();
212 }
213 }
214
215 // create the command line parser
216 CommandLineParser parser = new GnuParser();
217
218 // create the Options
219 Options options = new Options();
220 options.addOption( "al", "anchorleft", true, "Anchor the vertical left-hand edge of the interactive widget to the value provided " );
221 options.addOption( "ar", "anchorright", true, "Anchor the vertical right-hand edge of the interactive widget to the value provided " );
222 options.addOption( "at", "anchortop", true, "Anchor the vertical top edge of the interactive widget to the value provided " );
223 options.addOption( "ab", "anchorbottom", true, "Anchor the vertical bottom edge of the interactive widget to the value provided " );
224
225 /*
226 * Alternative way to do the above, but with more control over how the values are set up and can be used
227 *
228 @SuppressWarnings("static-access")
229 Option anchor_left_opt = OptionBuilder.withLongOpt( "anchorleft" )
230 .withDescription( "use <x-pos> as left anchor for the vertical lefthand edge of the interactive widget" )
231 .hasArg()
232 .withArgName("<x-pos>")
233 .create("al");
234
235 @SuppressWarnings("static-access")
236 Option anchor_right_opt = OptionBuilder.withLongOpt( "anchorright" )
237 .withDescription( "use <x-pos> as right anchor for the vertical righthand edge of the interactive widget" )
238 .hasArg()
239 .withArgName("<x-pos>")
240 .create("ar");
241
242
243 @SuppressWarnings("static-access")
244 Option anchor_top_opt = OptionBuilder.withLongOpt( "anchortop" )
245 .withDescription( "use <y-pos> as top anchor for the horizontal top end of the interactive widget" )
246 .hasArg()
247 .withArgName("<x-pos>")
248 .create("at");
249
250 @SuppressWarnings("static-access")
251 Option anchor_bottom_opt = OptionBuilder.withLongOpt( "anchorbottom" )
252 .withDescription( "use <y-pos> as bottom anchor for the horizontal bottom edge of the interactive widget" )
253 .hasArg()
254 .withArgName("<x-pos>")
255 .create("ab");
256
257 options.addOption(anchor_left_opt);
258 options.addOption(anchor_right_opt);
259 options.addOption(anchor_top_opt);
260 options.addOption(anchor_bottom_opt);
261 */
262
263 CommandLine core_line;
264 try {
265 // parse the command line arguments
266 core_line = parser.parse( options, tokens );
267
268 // Update tokens to be version with the options removed
269 tokens = core_line.getArgs();
270
271 }
272 catch( ParseException exp ) {
273 System.err.println( "Unexpected exception:" + exp.getMessage() );
274 core_line = null;
275
276 }
277
278 HelpFormatter formatter = new HelpFormatter();
279 StringWriter usage_str_writer = new StringWriter();
280 PrintWriter printWriter = new PrintWriter(usage_str_writer);
281
282 String widget_name = tokens[0];
283 final String widget_prefix = "org.expeditee.items.widgets";
284 if (widget_name.startsWith(widget_prefix)) {
285 widget_name = widget_name.substring(widget_prefix.length());
286 }
287
288 formatter.printHelp(printWriter, 80, TAG + ":" + widget_name + " [options] width height", null, options, 4, 0, null);
289 //System.out.println(usage_str_writer.toString());
290
291 float width = -1, height = -1;
292
293 if (tokens.length < 1) {
294 throw new IllegalArgumentException("Missing widget class name in source text");
295 }
296
297 try {
298
299 if (tokens.length >= 2) { // parse optional width
300 width = Integer.parseInt(tokens[1]);
301 width = (width <= 0) ? width = -1 : width;
302 }
303
304 if (tokens.length >= 3) { // parse optional height
305 height = Integer.parseInt(tokens[2]);
306 height = (height <= 0) ? height = -1 : height;
307 }
308
309 } catch (NumberFormatException nfe) {
310 throw new IllegalArgumentException(
311 "Bad width or height given in source text", nfe);
312 }
313
314 if (tokens.length > 3)
315 throw new IllegalArgumentException(
316 "to many arguments given before \":\" in source text");
317
318 String classname = tokens[0];
319 if (classname.charAt(0) == '$'){
320 classname = classname.substring(1);
321 }
322
323 // Attempt to locate the class using reflection
324 Class<?> iwclass = findIWidgetClass(classname);
325
326 if (iwclass == null) // ensure it exists
327 throw new InteractiveWidgetNotAvailableException(classname
328 + " does not exist or is not an InteractiveWidget");
329
330
331 //
332 // Step 2:
333 // For an X-rayable text item in the form:
334 // @iw: <class name> [options] width height : <rest...>
335 //
336 // Parse the <rest ...> part
337
338 // Extract out the parameters - if any
339 String[] args = null;
340 if (index > 0) { // index of the first ":"
341 if (text.length()>(index+1)) {
342 String args_str = text.substring(index + 1);
343
344 args = parseArgsApache(args_str);
345
346 }
347 }
348
349 InteractiveWidget inst = null;
350 try {
351 // Instantiate the widget - passing the params
352 Class parameterTypes[] = new Class[] { Text.class, String[].class };
353 Constructor ct = iwclass.getConstructor(parameterTypes);
354
355 Object arglist[] = new Object[] { source, args };
356
357 inst = (InteractiveWidget) ct.newInstance(arglist);
358 } catch (Exception e) {
359 throw new InteractiveWidgetNotAvailableException(
360 "Failed to create instance via reflection: " + e.toString(),
361 e);
362 }
363
364 // Step 3:
365 // Set up the size and position of the widget
366
367 // Use default dimensions if not provided (or provided as negative
368 // values)
369 if (width <= 0) {
370 width = inst.getWidth();
371 }
372 if (height <= 0) {
373 height = inst.getHeight();
374 }
375
376 inst.setSize(width, height);
377
378 // Apply any anchor values supplied in the core part of the @iw item
379 Float anchor_left = null;
380 Float anchor_right = null;
381 Float anchor_top = null;
382 Float anchor_bottom = null;
383
384 if(core_line.hasOption( "anchorleft" ) ) {
385 String al_str = core_line.getOptionValue( "anchorleft" );
386
387 anchor_left = Float.parseFloat(al_str);
388 }
389
390 if(core_line.hasOption( "anchorright" ) ) {
391 String ar_str = core_line.getOptionValue( "anchorright" );
392
393 anchor_right = Float.parseFloat(ar_str);
394 }
395
396 if(core_line.hasOption( "anchortop" ) ) {
397 String at_str = core_line.getOptionValue( "anchortop" );
398
399 anchor_top = Float.parseFloat(at_str);
400 }
401
402 if(core_line.hasOption( "anchorbottom" ) ) {
403 String ab_str = core_line.getOptionValue( "anchorbottom" );
404
405 anchor_bottom = Float.parseFloat(ab_str);
406 }
407
408 inst.setAnchorCorners(anchor_left,anchor_right,anchor_top,anchor_bottom);
409
410
411
412 return inst;
413 }
414
415 /**
416 * Locates the class from the classname of an InteractiveWidget class
417 *
418 * @param classname
419 * The name of the class to search
420 * @return Null if doesn't exist or not an InteractiveWidget
421 */
422 private static Class findIWidgetClass(String classname) {
423 if(classname == null)
424 return null;
425 // try just the classname
426 try {
427 Class c = Class.forName(classname); // attempt to find the class
428
429 // If one is found, ensure that it is a descendant of an
430 // InteractiveWidget
431 for (Class superclass = c.getSuperclass(); superclass != null
432 && superclass != Item.class; superclass = superclass
433 .getSuperclass()) {
434 if (superclass == InteractiveWidget.class)
435 return c;
436 }
437
438 } catch (ClassNotFoundException e) {
439 }
440 // see if the class is a widget with invalid capitalisation, or missing the widget package prefix
441 if(classname.startsWith(Actions.WIDGET_PACKAGE)) {
442 classname = classname.substring(Actions.WIDGET_PACKAGE.length());
443 }
444 try {
445 Class c = Class.forName(Actions.getClassName(classname)); // attempt to find the class
446
447 // If one is found, ensure that it is a descendant of an
448 // InteractiveWidget
449 for (Class superclass = c.getSuperclass(); superclass != null
450 && superclass != Item.class; superclass = superclass
451 .getSuperclass()) {
452 if (superclass == InteractiveWidget.class)
453 return c;
454 }
455
456 } catch (ClassNotFoundException e) {
457 }
458
459 // Doesn't exist or not an InteractiveWidget
460 return null;
461 }
462
463 /**
464 * Using Microsofts commandline convention: Args seperated with white
465 * spaces. Options with white spaces enclosed with quotes. Args with quotes
466 * must be double quoted args1 args2=sfasas args3="option with spaces"
467 * arg""4""
468 *
469 * @param args
470 * Null and empty excepted
471 * @return An array of args. null if none provided
472 */
473 public static String[] parseArgs(String args) {
474
475 if (args == null)
476 return null;
477
478 args = args.trim();
479 if (args.length() == 0)
480 return null;
481
482 List<String> vargs = new LinkedList<String>();
483 StringBuilder sb = new StringBuilder();
484 boolean quoteOn = false;
485 for (int i = 0; i < args.length(); i++) {
486
487 char c = args.charAt(i);
488 if (c == ' ' && !quoteOn) {
489
490 // Extract arg
491 vargs.add(sb.toString());
492 sb = new StringBuilder();
493
494 // Consume white spaces
495 while (args.charAt(++i) == ' ' && i < args.length()) {
496 }
497 i--;
498
499 } else if (c == '\"') {
500 // If double quoted
501 if (args.length() >= (i + 2) && args.charAt(i + 1) == '\"') {
502
503 sb.append(c); // add escaped
504 i++;
505
506 } else {
507 quoteOn = !quoteOn;
508 }
509
510 } else {
511 sb.append(c);
512 }
513 }
514
515 if (sb.length() > 0)
516 vargs.add(sb.toString());
517
518 if (vargs.size() == 0)
519 return null;
520 else
521 return vargs.toArray(new String[vargs.size()]);
522 }
523
524 /**
525 * Reverse of parseArgs
526 *
527 * @return Null if args is null or empty / all whitespace
528 */
529 public static String formatArgs(String[] args) {
530 if (args == null)
531 return null;
532
533 StringBuilder sb = new StringBuilder();
534
535 for (String s : args) {
536 if (s == null)
537 continue;
538
539 // Escape quotes
540 StringBuilder formatted = new StringBuilder(s.replaceAll("\"",
541 "\"\""));
542
543 // Encapsulate spaces
544 int index = formatted.indexOf(" ");
545 if (index >= 0) {
546 formatted.insert(index, "\"");
547 formatted.append('\"');
548 }
549
550 if (sb.length() > 0)
551 sb.append(' ');
552 sb.append(formatted);
553 }
554
555 return sb.length() > 0 ? sb.toString() : null;
556 }
557
558
559
560
561 /**
562 * Similar to parseArgs() above.
563 * Code based on routine used in Apache Ant:
564 * org.apache.tools.ant.types.Commandline::translateCommandline()
565 * @param toProcess the command line to process.
566 * @return the command line broken into strings.
567 * An empty or null toProcess parameter results in a zero sized array.
568 */
569 public static String[] parseArgsApache(String toProcess) {
570 if (toProcess == null || toProcess.length() == 0) {
571 //no command? no string
572 return new String[0];
573 }
574 // parse with a simple finite state machine
575
576 final int normal = 0;
577 final int inQuote = 1;
578 final int inDoubleQuote = 2;
579 int state = normal;
580 final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true);
581 final ArrayList<String> result = new ArrayList<String>();
582 final StringBuilder current = new StringBuilder();
583 boolean lastTokenHasBeenQuoted = false;
584
585 while (tok.hasMoreTokens()) {
586 String nextTok = tok.nextToken();
587 switch (state) {
588 case inQuote:
589 if ("\'".equals(nextTok)) {
590 lastTokenHasBeenQuoted = true;
591 state = normal;
592 } else {
593 current.append(nextTok);
594 }
595 break;
596 case inDoubleQuote:
597 if ("\"".equals(nextTok)) {
598 lastTokenHasBeenQuoted = true;
599 state = normal;
600 } else {
601 current.append(nextTok);
602 }
603 break;
604 default:
605 if ("\'".equals(nextTok)) {
606 state = inQuote;
607 } else if ("\"".equals(nextTok)) {
608 state = inDoubleQuote;
609 } else if (" ".equals(nextTok)) {
610 if (lastTokenHasBeenQuoted || current.length() != 0) {
611 result.add(current.toString());
612 current.setLength(0);
613 }
614 } else {
615 current.append(nextTok);
616 }
617 lastTokenHasBeenQuoted = false;
618 break;
619 }
620 }
621 if (lastTokenHasBeenQuoted || current.length() != 0) {
622 result.add(current.toString());
623 }
624 if (state == inQuote || state == inDoubleQuote) {
625 System.err.println("Error: Unbalanced quotes -- failed to parse '" + toProcess + "'");
626 return null;
627 }
628
629 return result.toArray(new String[result.size()]);
630 }
631
632
633
634
635 /**
636 * Arguments represent the widgets <i>current state</i> state. They are
637 * used for saving, loading, creating and cloning Special formatting is done
638 * for you.
639 *
640 * @see #getData()
641 *
642 * @return Can be null for no params.
643 */
644 protected abstract String[] getArgs();
645
646 /**
647 * Data represents the widgets <i>current state</i> state. For any state
648 * information you do not wish to be defined as arguments (e.g. metadata),
649 * you can set as the widgets source items data.
650 *
651 * The default implementation returns null. Override to make use of.
652 *
653 * @see #getArgs()
654 *
655 * @return Null for for data. Otherwise the data that represent this widgets
656 * <i>current state</i>
657 */
658 protected List<String> getData() {
659 return null;
660 }
661
662 /**
663 * Constructor
664 *
665 * @param source
666 * Must not be null. Neither must it's parent
667 *
668 * @param component
669 * Must not be null.
670 *
671 * @param minWidth
672 * The min width restriction for the widget. If negative, then
673 * there is no restriction.
674 *
675 * @param maxWidth
676 * The max width restriction for the widget. If negative, then
677 * there is no restriction.
678 *
679 * @param minHeight
680 * The min height restriction for the widget. If negative, then
681 * there is no restriction.
682 *
683 * @param maxHeight
684 * The max height restriction for the widget. If negative, then
685 * there is no restriction.
686 *
687 * @throws NullPointerException
688 * If source, component if null.
689 *
690 * @throws IllegalArgumentException
691 * If source's parent is null. If maxWidth smaller than minWidth
692 * and maxWidth larger or equal to zero or if maxHeight smaller
693 * than minHeight && maxHeight is larger or equal to zero
694 *
695 */
696 protected InteractiveWidget(Text source, JComponent component,
697 int minWidth, int maxWidth, int minHeight, int maxHeight) {
698
699
700 if (component == null)
701 throw new NullPointerException("component");
702 if (source == null)
703 throw new NullPointerException("source");
704 if (source.getParent() == null)
705 throw new IllegalArgumentException(
706 "source's parent is null, InteractiveWidget's must be created from Text items with non-null parents");
707
708 _swingComponent = component;
709 _swingComponent.addContainerListener(this);
710 keyListenerToChildren(_swingComponent, true);
711
712 _textRepresentation = source;
713
714 setSizeRestrictions(minWidth, maxWidth, minHeight, maxHeight); // throws IllegalArgumentException's
715
716 int x = source.getX();
717 int y = source.getY();
718 int width = (int) ((_minWidth < 0) ? 10 : _minWidth);
719 int height = (int) ((_minHeight < 0) ? 10 : _minHeight);
720
721 Frame idAllocator = _textRepresentation.getParent();
722
723 // create WidgetCorners
724 _d1 = new WidgetCorner(x, y, idAllocator.getNextItemID(), this);
725 _d2 = new WidgetCorner(x + width, y, idAllocator.getNextItemID(), this);
726 _d3 = new WidgetCorner(x + width, y + height, idAllocator.getNextItemID(), this);
727 _d4 = new WidgetCorner(x, y + height, idAllocator.getNextItemID(), this);
728
729 // create WidgetEdges
730 _l1 = new WidgetEdge(_d1, _d2, idAllocator.getNextItemID(), this);
731 _l2 = new WidgetEdge(_d2, _d3, idAllocator.getNextItemID(), this);
732 _l3 = new WidgetEdge(_d3, _d4, idAllocator.getNextItemID(), this);
733 _l4 = new WidgetEdge(_d4, _d1, idAllocator.getNextItemID(), this);
734
735 Collection<Item> enclist = new ArrayList<Item>(4);
736 enclist.add(_d1);
737 enclist.add(_d2);
738 enclist.add(_d3);
739 enclist.add(_d4);
740 _d1.setEnclosedList(enclist);
741 _d2.setEnclosedList(enclist);
742 _d3.setEnclosedList(enclist);
743 _d4.setEnclosedList(enclist);
744
745 _expediteeItems = new ArrayList<Item>(8); // Note: order important
746 _expediteeItems.add(_d1);
747 _expediteeItems.add(_d2);
748 _expediteeItems.add(_d3);
749 _expediteeItems.add(_d4);
750 _expediteeItems.add(_l1);
751 _expediteeItems.add(_l2);
752 _expediteeItems.add(_l3);
753 _expediteeItems.add(_l4);
754
755 setWidgetEdgeColor(source.getBorderColor());
756 setWidgetEdgeThickness(source.getThickness());
757 }
758
759 /**
760 * Sets the restrictions - checks values.
761 *
762 * @param minWidth
763 * The min width restriction for the widget. If negative, then
764 * there is no restriction.
765 *
766 * @param maxWidth
767 * The max width restriction for the widget. If negative, then
768 * there is no restriction.
769 *
770 * @param minHeight
771 * The min height restriction for the widget. If negative, then
772 * there is no restriction.
773 *
774 * @param maxHeight
775 * The max height restriction for the widget. If negative, then
776 * there is no restriction.
777 *
778 * @throws IllegalArgumentException
779 * If maxWidth smaller than minWidth and maxWidth larger or
780 * equal to zero or if maxHeight smaller than minHeight &&
781 * maxHeight is larger or equal to zero
782 *
783 */
784 private void setSizeRestrictions(int minWidth, int maxWidth, int minHeight,
785 int maxHeight) {
786
787 _minWidth = minWidth;
788
789 if (maxWidth < _minWidth && maxWidth >= 0)
790 throw new IllegalArgumentException(
791 "maxWidth smaller than the min Width");
792 _maxWidth = maxWidth;
793
794 _minHeight = minHeight;
795 if (maxHeight < _minHeight && maxHeight >= 0)
796 throw new IllegalArgumentException(
797 "maxHeight smaller than the min Height");
798 _maxHeight = maxHeight;
799 }
800
801 /**
802 * This can be overrided for creating custom copies. The default
803 * implementation creates a new widget based on the current state of the
804 * widget (via getArgs).
805 *
806 * @see InteractiveWidget#getArgs().
807 *
808 * @return A copy of this widget.
809 *
810 */
811 public InteractiveWidget copy()
812 throws InteractiveWidgetNotAvailableException,
813 InteractiveWidgetInitialisationFailedException {
814
815 Text t = _textRepresentation.copy();
816 String clonedAnnotation = getAnnotationString();
817 t.setText(clonedAnnotation);
818 t.setData(getData());
819 return InteractiveWidget.createWidget(t);
820
821 }
822
823 /**
824 * Notifies widget of delete
825 */
826 public void onDelete() {
827
828 // Allocate new ID's
829 Frame parent = getParentFrame();
830 if (parent == null)
831 parent = DisplayIO.getCurrentFrame();
832
833 if (parent != null) {
834 for (Item i : _expediteeItems) {
835 i.setID(parent.getNextItemID());
836 }
837 }
838
839 invalidateLink();
840
841 }
842
843 /**
844 * Note updates the source text with current state info
845 *
846 * @return The Text item that this widget was created from.
847 */
848 public Item getSource() {
849
850 // Build the annotation string such that it represents this widgets
851 // current state
852 String newAnnotation = getAnnotationString();
853
854 // Set the new text
855 _textRepresentation.setText(newAnnotation);
856
857 // Set the data
858 _textRepresentation.setData(getData());
859
860 return _textRepresentation;
861 }
862
863 /**
864 * @return The current representation for this widget. The representation
865 * stores link information, data etc... It is used for saving and
866 * loading of the widget. Never null.
867 *
868 */
869 protected Item getCurrentRepresentation() {
870 return _textRepresentation;
871 }
872
873 /**
874 * @return The Expeditee annotation string.
875 */
876 protected String getAnnotationString() {
877
878 // Create tag and append classname
879 StringBuilder sb = new StringBuilder(ItemUtils
880 .GetTag(ItemUtils.TAG_IWIDGET));
881 sb.append(':');
882 sb.append(' ');
883 sb.append(getClass().getName());
884
885 if (_anchorLeft != null) {
886 sb.append(" --anchorLeft " + Math.round(_anchorLeft));
887 }
888 if (_anchorRight != null) {
889 sb.append(" --anchorRight " + Math.round(_anchorRight));
890 }
891
892 if (_anchorTop != null) {
893 sb.append(" --anchorTop " + Math.round(_anchorTop));
894 }
895
896 if (_anchorBottom != null) {
897 sb.append(" --anchorBottom " + Math.round(_anchorBottom));
898 }
899
900 // Append size information if needed (not an attribute of text items)
901 if (!isFixedSize()) {
902 sb.append(' ');
903 sb.append(getWidth());
904 sb.append(' ');
905 sb.append(getHeight());
906 }
907
908 // Append arguments if any
909 String stateArgs = InteractiveWidget.formatArgs(getArgs());
910 if (stateArgs != null) {
911 sb.append(':');
912 sb.append(stateArgs);
913 }
914
915 return sb.toString();
916 }
917
918 /**
919 * Sets both the new size as well as the new min/max widget/height
920 * restrictions.
921 *
922 * @param minWidth
923 * The min width restriction for the widget. If negative, then
924 * there is no restriction.
925 *
926 * @param maxWidth
927 * The max width restriction for the widget. If negative, then
928 * there is no restriction.
929 *
930 * @param minHeight
931 * The min height restriction for the widget. If negative, then
932 * there is no restriction.
933 *
934 * @param maxHeight
935 * The max height restriction for the widget. If negative, then
936 * there is no restriction.
937 *
938 * @param newWidth
939 * Clamped to new restrictions.
940 *
941 * @param newHeight
942 * Clamped to new restrictions.
943 *
944 * @throws IllegalArgumentException
945 * If maxWidth smaller than minWidth and maxWidth larger or
946 * equal to zero or if maxHeight smaller than minHeight &&
947 * maxHeight is larger or equal to zero
948 *
949 * @see #setSize(float, float)
950 *
951 */
952 public void setSize(int minWidth, int maxWidth, int minHeight,
953 int maxHeight, float newWidth, float newHeight) {
954
955 setSizeRestrictions(minWidth, maxWidth, minHeight, maxHeight); // throws
956 // IllegalArgumentException's
957 setSize(newWidth, newHeight);
958 }
959
960 /**
961 * Clamped to current min/max width/height.
962 *
963 * @param width
964 * Clamped to current restrictions.
965 * @param height
966 * Clamped to current restrictions.
967 *
968 * @see #setSize(int, int, int, int, float, float)
969 */
970 public void setSize(float width, float height) {
971
972 // If 'width' and 'height' exceed the min/max values for width/height
973 // => clamp to the relevant min/max value
974 if (width < _minWidth && _minWidth >= 0) {
975 width = _minWidth;
976 }
977 else if (width > _maxWidth && _maxWidth >= 0) {
978 width = _maxWidth;
979 }
980
981 if (height < _minHeight && _minHeight >= 0) {
982 height = _minHeight;
983 }
984 else if (height > _maxHeight && _maxHeight >= 0) {
985 height = _maxHeight;
986 }
987
988 // Remember current isFloating() values
989 boolean vfloating[] = new boolean[] { _d1.isFloating(),
990 _d2.isFloating(), _d3.isFloating(), _d4.isFloating() };
991
992 _d1.setFloating(true);
993 _d2.setFloating(true);
994 _d3.setFloating(true);
995 _d4.setFloating(true);
996
997 float xr = _d1.getX() + width;
998 float yb = _d1.getY() + height;
999
1000 _d2.setX(xr);
1001 _d3.setX(xr);
1002 _d3.setY(yb);
1003 _d4.setY(yb);
1004
1005 // Restore isFloating() values
1006 _d1.setFloating(vfloating[0]);
1007 _d2.setFloating(vfloating[1]);
1008 _d3.setFloating(vfloating[2]);
1009 _d4.setFloating(vfloating[3]);
1010
1011 onSizeChanged();
1012 }
1013
1014 public void setAnchorCorners(Float left, Float right, Float top, Float bottom)
1015 {
1016 setAnchorLeft(left);
1017 setAnchorRight(right);
1018 setAnchorTop(top);
1019 setAnchorBottom(bottom);
1020 }
1021
1022
1023 public void setPosition(int x, int y) {
1024 if (x == getX() && y == getY())
1025 return;
1026
1027 // Remember current isFloating() values
1028 boolean vfloating[] = new boolean[] { _d1.isFloating(),
1029 _d2.isFloating(), _d3.isFloating(), _d4.isFloating() };
1030
1031 int width = getWidth();
1032 int height = getHeight();
1033
1034 invalidateLink();
1035
1036 _d1.setFloating(true);
1037 _d2.setFloating(true);
1038 _d3.setFloating(true);
1039 _d4.setFloating(true);
1040
1041 _d1.setPosition(x, y);
1042 _d2.setPosition(x + width, y);
1043 _d3.setPosition(x + width, y + height);
1044 _d4.setPosition(x, y + height);
1045
1046 // Restore isFloating() values
1047 _d1.setFloating(vfloating[0]);
1048 _d2.setFloating(vfloating[1]);
1049 _d3.setFloating(vfloating[2]);
1050 _d4.setFloating(vfloating[3]);
1051
1052 invalidateLink();
1053
1054 onMoved();
1055
1056 }
1057
1058 private boolean _settingPositionFlag = false; // used for recursion
1059
1060 /**
1061 * Updates position of given WidgetCorner to the given (x,y),
1062 * and updates related values (connected corners, width and height)
1063 *
1064 * @param src
1065 * @param x
1066 * @param y
1067 * @return False if need to call super.setPosition
1068 */
1069 boolean setPositions(WidgetCorner src, float x, float y) {
1070
1071 if (_settingPositionFlag)
1072 return false;
1073 _settingPositionFlag = true;
1074
1075 invalidateLink();
1076
1077 // Check to see if the widget is fully being picked up
1078 boolean isAllPickedUp = (_d1.isFloating() && _d2.isFloating()
1079 && _d3.isFloating() && _d4.isFloating());
1080
1081 // If so, then this will be called one by one ..
1082 if (isAllPickedUp) {
1083 src.setPosition(x, y);
1084 } else {
1085
1086 float newX = x;
1087
1088 // Reference:
1089 // D1 D2
1090 // D3 D4
1091
1092 //
1093 // GUIDE:
1094 // l1
1095 // d1-------d2
1096 // | |
1097 // l4 | X | 12
1098 // | |
1099 // d4-------d3
1100 // 13
1101 //
1102
1103 // X Positions
1104 if (src == _d1 || src == _d4) {
1105
1106 // Check min X constraint
1107 if (_minWidth >= 0) {
1108 if ((_d2.getX() - x) < _minWidth) {
1109 newX = _d2.getX() - _minWidth;
1110 }
1111 }
1112 // Check max X constraint
1113 if (_maxWidth >= 0) {
1114 if ((_d2.getX() - x) > _maxWidth) {
1115 newX = _d2.getX() - _maxWidth;
1116 }
1117 }
1118
1119 if (!(src == _d4 && _d1.isFloating() && _d4.isFloating()))
1120 _d1.setX(newX);
1121 if (!(src == _d1 && _d4.isFloating() && _d1.isFloating()))
1122 _d4.setX(newX);
1123
1124 } else if (src == _d2 || src == _d3) {
1125
1126 // Check min X constraint
1127 if (_minWidth >= 0) {
1128 if ((x - _d1.getX()) < _minWidth) {
1129 newX = _d1.getX() + _minWidth;
1130 }
1131 }
1132 // Check max X constraint
1133 if (_maxWidth >= 0) {
1134 if ((x - _d1.getX()) > _maxWidth) {
1135 newX = _d1.getX() + _maxWidth;
1136 }
1137 }
1138
1139 if (!(src == _d3 && _d2.isFloating() && _d3.isFloating()))
1140 _d2.setX(newX);
1141 if (!(src == _d2 && _d3.isFloating() && _d2.isFloating()))
1142 _d3.setX(newX);
1143 }
1144
1145 float newY = y;
1146
1147 // Y Positions
1148 if (src == _d1 || src == _d2) {
1149
1150 // Check min Y constraint
1151 if (_minHeight >= 0) {
1152 if ((_d4.getY() - y) < _minHeight) {
1153 newY = _d4.getY() - _minHeight;
1154 }
1155 }
1156 // Check max Y constraint
1157 if (_maxHeight >= 0) {
1158 if ((_d4.getY() - y) > _maxHeight) {
1159 newY = _d4.getY() - _maxHeight;
1160 }
1161 }
1162
1163 if (!(src == _d2 && _d1.isFloating() && _d2.isFloating()))
1164 _d1.setY(newY);
1165 if (!(src == _d1 && _d2.isFloating() && _d1.isFloating()))
1166 _d2.setY(newY);
1167
1168 } else if (src == _d3 || src == _d4) {
1169
1170 // Check min Y constraint
1171 if (_minHeight >= 0) {
1172 if ((y - _d1.getY()) < _minHeight) {
1173 newY = _d1.getY() + _minHeight;
1174 }
1175 }
1176 // Check max Y constraint
1177 if (_maxHeight >= 0) {
1178 if ((y - _d1.getY()) > _maxHeight) {
1179 newY = _d1.getY() + _maxHeight;
1180 }
1181 }
1182
1183 if (!(src == _d4 && _d3.isFloating() && _d4.isFloating()))
1184 _d3.setY(newY);
1185 if (!(src == _d3 && _d4.isFloating() && _d3.isFloating()))
1186 _d4.setY(newY);
1187 }
1188 }
1189
1190 // Update source text position so position is remembered from loading
1191 float newTextX = getX();
1192 float newTextY = getY();
1193 if (_textRepresentation.getX() != newTextX
1194 || _textRepresentation.getY() != newTextY)
1195 _textRepresentation.setPosition(newTextX, newTextY);
1196
1197 _settingPositionFlag = false;
1198
1199 invalidateLink();
1200
1201 onMoved();
1202
1203 return true;
1204 }
1205
1206 public int getX() {
1207 return Math.min(_d1.getX(), _d2.getX());
1208 }
1209
1210 public int getY() {
1211 return Math.min(_d1.getY(), _d4.getY());
1212 }
1213
1214 public int getWidth() {
1215
1216 return Math.abs(_d2.getX() - _d1.getX());
1217 }
1218
1219 public int getHeight() {
1220 return Math.abs(_d4.getY() - _d1.getY());
1221 }
1222
1223 public Point getPosition() {
1224 return new Point(getX(), getY());
1225 }
1226
1227 /**
1228 * The order of the items in the list is as specified: _d1 _d2 _d3 _d4 _l1
1229 * _l2 _l3 _l4
1230 *
1231 * @return All of the Expeditee items that form the bounderies of this
1232 * widget in an unmodifiable list
1233 */
1234 public List<Item> getItems() {
1235 return Collections.unmodifiableList(_expediteeItems);
1236 }
1237
1238 public JComponent getComponant() {
1239 return _swingComponent;
1240 }
1241
1242 public final void onParentStateChanged(ItemParentStateChangedEvent e) {
1243
1244 // Because widgets are comprised of four corners - they all report this
1245 // event one after the
1246 // other. So must filter out redundant notifications like so:
1247 if (_lastParentStateChangedEvent != null
1248 && _lastParentStateChangedEvent.getEventType() == e
1249 .getEventType()
1250 && _lastParentStateChangedMouseEvent == MouseEventRouter
1251 .getCurrentMouseEvent())
1252 return; // already dealt with this event
1253
1254 _lastParentStateChangedEvent = e;
1255 _lastParentStateChangedMouseEvent = MouseEventRouter
1256 .getCurrentMouseEvent();
1257
1258 switch (e.getEventType()) {
1259
1260 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
1261 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
1262 case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
1263 if (_swingComponent.getParent() != null) {
1264 _swingComponent.getParent().remove(_swingComponent);
1265 }
1266 break;
1267
1268 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
1269 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
1270 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
1271 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
1272 if (_swingComponent.getParent() == null) {
1273 addJComponantToFrame(e);
1274 }
1275 break;
1276
1277 }
1278
1279 FrameGraphics.invalidateItem(_d1, _swingComponent.getBounds());
1280
1281 // Forward filtered event to upper classeses...
1282 onParentStateChanged(e.getEventType());
1283 }
1284
1285 /**
1286 * Override to make use of. Internally this is reported once by all corners,
1287 * but is filterted out so that this method is invoked once per event.
1288 *
1289 * @param eventType
1290 * The {@link ItemParentStateChangedEvent#getEventType()} that
1291 * occured.
1292 *
1293 */
1294 protected void onParentStateChanged(int eventType) {
1295 }
1296
1297 protected void addJComponantToFrame(ItemParentStateChangedEvent e) {
1298
1299 if ((e.getEventType() == ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY || e
1300 .getEventType() == ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY)
1301 && e.getOverlayLevel().equals(UserAppliedPermission.none)) {
1302 return; // item belongs to a non-active overlay
1303 }
1304
1305 if (_swingComponent.getParent() == null) {
1306
1307 if (Browser._theBrowser != null) {
1308 // Due to precaching - before adding physical swing
1309 // componant must check to see that this widget belongs to a
1310 // frame that is
1311 // considered current. If the widget is shown however this does
1312 // not apply -
1313 // since it has been explicitly made clear the the widget is
1314 // shown.
1315 if (e.getEventType() == ItemParentStateChangedEvent.EVENT_TYPE_SHOWN
1316 || e.getEventType() == ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY
1317 || e.getSource() == DisplayIO.getCurrentFrame()) {
1318
1319 onBoundsChanged();
1320 Browser._theBrowser.getContentPane().add(_swingComponent);
1321 layout(_swingComponent);
1322 }
1323
1324 } else { // if widgets exist on startup frame this will occur
1325
1326 synchronized (_widgetsToAddLater) {
1327 _widgetsToAddLater.add(new DelayedWidgetEvent(this, e));
1328 }
1329 SwingUtilities.invokeLater(new AddToFrameLater());
1330 }
1331
1332 }
1333
1334 }
1335
1336 /**
1337 * @return True if at least one corner is floating
1338 */
1339 public boolean isFloating() {
1340 return _d1.isFloating() || _d2.isFloating() || _d3.isFloating()
1341 || _d4.isFloating();
1342 }
1343
1344 public boolean areCornersFullyAnchored() {
1345 return _d1.getParent() != null && _d2.getParent() != null
1346 && _d3.getParent() != null && _d4.getParent() != null;
1347 }
1348
1349 /**
1350 * Used for passing info to the swing thread
1351 *
1352 * @author Brook Novak
1353 */
1354 private class DelayedWidgetEvent {
1355
1356 DelayedWidgetEvent(InteractiveWidget widget,
1357 ItemParentStateChangedEvent e) {
1358 _widget = widget;
1359 _e = e;
1360 }
1361
1362 InteractiveWidget _widget;
1363
1364 ItemParentStateChangedEvent _e;
1365 }
1366
1367 /**
1368 * Must be able to add widgets on first loaded frame: these are loaded
1369 * before the browser singleton is made available.
1370 */
1371 private static List<DelayedWidgetEvent> _widgetsToAddLater = new LinkedList<DelayedWidgetEvent>();
1372
1373 /**
1374 * Ensures widgets are added correctly to first loaded frame
1375 */
1376 class AddToFrameLater implements Runnable {
1377 public void run() {
1378 if (!_widgetsToAddLater.isEmpty()) {
1379 List<DelayedWidgetEvent> tmp = null;
1380 synchronized (_widgetsToAddLater) {
1381 tmp = new LinkedList<DelayedWidgetEvent>(_widgetsToAddLater);
1382 }
1383 _widgetsToAddLater.clear();
1384 for (DelayedWidgetEvent iwi : tmp) {
1385 iwi._widget.addJComponantToFrame(iwi._e);
1386 iwi._widget.invalidateSelf();
1387 }
1388 }
1389 }
1390 }
1391
1392 final void onBoundsChanged() {
1393 if (isFixedSize())
1394 _swingComponent.setBounds(getX(), getY(), _maxWidth, _maxHeight);
1395 else
1396 _swingComponent.setBounds(getX(), getY(), getWidth(), getHeight());
1397 }
1398
1399 /**
1400 *
1401 * @return The current bounds for this widget. Never null.
1402 */
1403 public Rectangle getBounds() {
1404 return new Rectangle(getX(), getY(), getWidth(), getHeight());
1405 }
1406
1407 /**
1408 * Due to absolute positioning...
1409 *
1410 * @param parent
1411 */
1412 protected void layout(Component parent) {
1413
1414 parent.validate();
1415
1416 if (parent instanceof Container) {
1417 for (Component c : ((Container) parent).getComponents()) {
1418
1419 if (c instanceof Container)
1420 layout(c);
1421 else
1422 c.validate();
1423 }
1424 }
1425
1426 }
1427
1428 private void ignoreAWTPainting(Component c) {
1429
1430 if (c instanceof JComponent) {
1431 ((JComponent) c).setDoubleBuffered(false);
1432 }
1433
1434 c.setIgnoreRepaint(true);
1435
1436 if (c instanceof Container) {
1437 for (Component child : ((Container) c).getComponents()) {
1438
1439 if (child instanceof Container) {
1440 ignoreAWTPainting(child);
1441 } else {
1442 if (child instanceof JComponent) {
1443 ((JComponent) child).setDoubleBuffered(false);
1444 }
1445
1446 child.setIgnoreRepaint(true);
1447 }
1448 }
1449 }
1450 }
1451
1452 private void prepareToPaint() {
1453 _isReadyToPaint = true;
1454 layout(_swingComponent);
1455 ignoreAWTPainting(_swingComponent);
1456 }
1457
1458 /**
1459 * Paints the widget excluding the boundries. That is, the Swing graphics
1460 *
1461 * @param g
1462 */
1463 public void paint(Graphics g) {
1464
1465 if (!_isReadyToPaint) {
1466 prepareToPaint();
1467 }
1468
1469 Point loc = _swingComponent.getLocation();
1470
1471 g.translate(loc.x, loc.y);
1472 _swingComponent.paint(g);
1473 g.translate(-loc.x, -loc.y);
1474
1475 paintLink((Graphics2D) g);
1476
1477 }
1478
1479 protected void paintLink(Graphics2D g) {
1480 // If this widget is linked .. then draw the link icon
1481 if (_textRepresentation.getLink() != null
1482 || _textRepresentation.hasAction()) {
1483 // System.out.println("Painted link");
1484 _textRepresentation.paintLinkGraphic(g, getLinkX(), getLinkY());
1485
1486 }
1487
1488 }
1489
1490 private int getLinkX() {
1491 return getX() - Item.LEFT_MARGIN;
1492 }
1493
1494 private int getLinkY() {
1495 return getY() + (getHeight() / 2);
1496 }
1497
1498 /**
1499 * Invoked whenever the widget is to be repainted in free space.
1500 *
1501 * @param g
1502 */
1503 protected void paintInFreeSpace(Graphics g) {
1504 g.setColor(FREESPACE_BACKCOLOR);
1505 g.fillRect(getX(), getY(), getWidth(), getHeight());
1506 }
1507
1508 /**
1509 * Called from the widgets corners: Whenever one of the corners are invoked
1510 * for a refill of the enclosed area.
1511 *
1512 * If the widget is floating (e.g. currently picked up / rubberbanding) then
1513 * a shaded area is drawn instead of the actual widget so the manipulation
1514 * of the widget is as smooth as possible.
1515 *
1516 * @param g
1517 * @param notifier
1518 */
1519 void paintFill(Graphics g) {
1520 if (_swingComponent.getParent() == null) {
1521 // Note that frames with @f may want to paint the widgets so do not
1522 // paint over the widget interface in these cases: must only
1523 // paint if an object is floating
1524 if (isFloating()) {
1525 paintInFreeSpace(g);
1526 paintLink((Graphics2D) g);
1527 }
1528 }
1529 }
1530
1531 /**
1532 * @return True if this widget cannot be resized in either directions
1533 */
1534 public boolean isFixedSize() {
1535 return this._minHeight == this._maxHeight
1536 && this._minWidth == this._maxWidth && this._minHeight >= 0
1537 && this._minWidth >= 0;
1538 }
1539
1540 /**
1541 * Removes this widget from the parent frame or free space.
1542 *
1543 * @return True if removed from a parent frame. Thus a parent changed event
1544 * will be invoked.
1545 *
1546 * False if removed purely from free space.
1547 */
1548 protected boolean removeSelf() {
1549
1550 Frame parent = getParentFrame();
1551
1552 if (parent != null) {
1553 parent.removeAllItems(_expediteeItems);
1554 }
1555
1556 FreeItems.getInstance().removeAll(_expediteeItems);
1557
1558 return (parent != null);
1559
1560 }
1561
1562 /**
1563 * @return The parent frame. Null if has none. Note: Based on corners
1564 * parents.
1565 */
1566 public Frame getParentFrame() {
1567
1568 Frame parent = null;
1569 if (_d1.getParent() != null)
1570 parent = _d1.getParent();
1571 else if (_d2.getParent() != null)
1572 parent = _d2.getParent();
1573 else if (_d3.getParent() != null)
1574 parent = _d3.getParent();
1575 else if (_d4.getParent() != null)
1576 parent = _d4.getParent();
1577
1578 return parent;
1579 }
1580
1581 protected void invalidateSelf() {
1582 Rectangle dirty = new Rectangle((int) getX(), (int) getY(),
1583 (int) getWidth(), (int) getHeight());
1584 FrameGraphics.invalidateArea(dirty);
1585 invalidateLink();
1586 //FrameGraphics.refresh(true);
1587 }
1588
1589 /**
1590 * Invalidates the link for this widget - if it has one.
1591 */
1592 protected void invalidateLink() {
1593 if (_textRepresentation.getLink() != null
1594 || _textRepresentation.hasAction()) {
1595 Rectangle linkArea = _textRepresentation.getLinkDrawArea(
1596 getLinkX(), getLinkY());
1597 FrameGraphics.invalidateArea(linkArea);
1598 }
1599
1600 }
1601
1602 /**
1603 * @see ItemUtils#isVisible(Item)
1604 *
1605 * @return True if this widget is visible from the current frame. Considers
1606 * overlays and vectors.
1607 *
1608 */
1609 public boolean isVisible() {
1610 return ItemUtils.isVisible(_d1);
1611 }
1612
1613 /**
1614 * Invoked whenever the widget have moved. Can override.
1615 *
1616 */
1617 protected void onMoved() {
1618 }
1619
1620 /**
1621 * Invoked whenever the widget have moved. Can override.
1622 *
1623 */
1624 protected void onSizeChanged() {
1625 }
1626
1627 /**
1628 * Override to have a custom min border thickness for your widget.
1629 *
1630 * @see #DEFAULT_MINIMUM_BORDER_THICKNESS
1631 *
1632 * @return The minimum border thickness. Should be larger or equal to zero.
1633 *
1634 */
1635 public float getMinimumBorderThickness() {
1636 return DEFAULT_MINIMUM_BORDER_THICKNESS;
1637 }
1638
1639 /**
1640 * Looks fors a dataline in the current representation of the widget.
1641 *
1642 * @see #getCurrentRepresentation
1643 * @see #getStrippedDataInt(String, int)
1644 * @see #getStrippedDataLong(String, long)
1645 *
1646 * @param tag
1647 * The prefix of a dataline that will be matched. Must be larger
1648 * the zero and not null.
1649 *
1650 * @return The <i>first</i> dataline that matched the prefix - without the
1651 * prefix. Null if their was no data that matched the given prefix.
1652 *
1653 * @throws IllegalArgumentException
1654 * If tag is null.
1655 *
1656 * @throws NullPointerException
1657 * If tag is empty.
1658 */
1659 protected String getStrippedDataString(String tag) {
1660 if (tag == null)
1661 throw new NullPointerException("tag");
1662 else if (tag.length() == 0)
1663 throw new IllegalArgumentException("tag is empty");
1664
1665 if (getCurrentRepresentation().getData() != null) {
1666 for (String str : getCurrentRepresentation().getData()) {
1667 if (str != null && str.startsWith(tag)
1668 && str.length() > tag.length()) {
1669 return str.substring(tag.length());
1670 }
1671 }
1672 }
1673 return null;
1674 }
1675
1676 /**
1677 * Looks fors a dataline in the current representation of the widget.
1678 *
1679 * @see #getCurrentRepresentation
1680 * @see #getStrippedDataString(String)
1681 * @see #getStrippedDataLong(String, long)
1682 *
1683 * @param tag
1684 * The prefix of a dataline that will be matched. Must be larger
1685 * the zero and not null.
1686 *
1687 * @param defaultValue
1688 * The default value if the tag does not exist or contains
1689 * invalid data.
1690 *
1691 * @return The <i>first</i> dataline that matched the prefix - parsed as an
1692 * int (after the prefix). defaultValue if their was no data that
1693 * matched the given prefix or the data was invalid.
1694 *
1695 * @throws IllegalArgumentException
1696 * If tag is null.
1697 *
1698 * @throws NullPointerException
1699 * If tag is empty.
1700 *
1701 */
1702 protected Integer getStrippedDataInt(String tag, Integer defaultValue) {
1703
1704 String strippedStr = getStrippedDataString(tag);
1705
1706 if (strippedStr != null) {
1707 strippedStr = strippedStr.trim();
1708 if (strippedStr.length() > 0) {
1709 try {
1710 return Integer.parseInt(strippedStr);
1711 } catch (NumberFormatException e) { /* Consume */
1712 }
1713 }
1714 }
1715
1716 return defaultValue;
1717 }
1718
1719 /**
1720 * Looks fors a dataline in the current representation of the widget.
1721 *
1722 * @see #getCurrentRepresentation
1723 * @see #getStrippedDataString(String)
1724 * @see #getStrippedDataInt(String, int)
1725 *
1726 * @param tag
1727 * The prefix of a dataline that will be matched. Must be larger
1728 * the zero and not null.
1729 *
1730 * @param defaultValue
1731 * The default value if the tag does not exist or contains
1732 * invalid data.
1733 *
1734 * @return The <i>first</i> dataline that matched the prefix - parsed as a
1735 * long (after the prefix). defaultValue if their was no data that
1736 * matched the given prefix or the data was invalid.
1737 *
1738 * @throws IllegalArgumentException
1739 * If tag is null.
1740 *
1741 * @throws NullPointerException
1742 * If tag is empty.
1743 *
1744 */
1745 protected Long getStrippedDataLong(String tag, Long defaultValue) {
1746 String strippedStr = getStrippedDataString(tag);
1747
1748 if (strippedStr != null) {
1749 strippedStr = strippedStr.trim();
1750 if (strippedStr.length() > 0) {
1751 try {
1752 return Long.parseLong(strippedStr);
1753 } catch (NumberFormatException e) { /* Consume */
1754 }
1755 }
1756 }
1757
1758 return defaultValue;
1759 }
1760
1761 /**
1762 * All data is removed that is prefixed with the given tag.
1763 *
1764 * @param tag
1765 * The prefix of the data lines to remove. Must be larger the
1766 * zero and not null.
1767 *
1768 * @throws IllegalArgumentException
1769 * If tag is null.
1770 *
1771 * @throws NullPointerException
1772 * If tag is empty.
1773 *
1774 */
1775 protected void removeData(String tag) {
1776 updateData(tag, null);
1777 }
1778
1779 protected void addDataIfCaseInsensitiveNotExists(String tag) {
1780 if (tag == null) throw new NullPointerException("tag");
1781
1782 List<String> data = getCurrentRepresentation().getData();
1783
1784 if (data == null) {
1785 data = new LinkedList<String>();
1786 }
1787
1788 for (String s : data) {
1789 if (s != null && s.equalsIgnoreCase(tag)) {
1790 return;
1791 }
1792 }
1793
1794 data.add(tag);
1795 getCurrentRepresentation().setData(data);
1796 }
1797
1798
1799 /**
1800 * Updates the data with a given tag. All data is removed that is prefixed
1801 * with the given tag. Then a new line is added (if not null).
1802 *
1803 * Note that passing newData with null is the equivelant of removing tag
1804 * lines.
1805 *
1806 * @param tag
1807 * The prefix of the data lines to remove. Must be larger the
1808 * zero and not null.
1809 *
1810 * @param newData
1811 * The new line to add. Can be null - for not adding anything.
1812 *
1813 * @throws IllegalArgumentException
1814 * If tag is null.
1815 *
1816 * @throws NullPointerException
1817 * If tag is empty.
1818 *
1819 * @see #removeData(String)
1820 *
1821 */
1822 protected void updateData(String tag, String newData) {
1823 if (tag == null)
1824 throw new NullPointerException("tag");
1825 else if (tag.length() == 0)
1826 throw new IllegalArgumentException("tag is empty");
1827
1828 // Get current data
1829 List<String> data = getCurrentRepresentation().getData();
1830
1831 if (data != null) {
1832 for (int i = 0; i < data.size(); i++) {
1833 String str = data.get(i);
1834 if (str != null && str.startsWith(tag)) {
1835 data.remove(i);
1836 }
1837 }
1838 }
1839
1840 if (newData != null) {
1841 if (data != null)
1842 data.add(newData);
1843 else {
1844 data = new LinkedList<String>();
1845 data.add(newData);
1846 getCurrentRepresentation().setData(data);
1847
1848 }
1849 }
1850 }
1851
1852 public boolean containsData(String str) {
1853 assert(str != null);
1854 if (getCurrentRepresentation().getData() != null)
1855 return getCurrentRepresentation().getData().contains(str);
1856 return false;
1857 }
1858
1859 public boolean containsDataTrimmedIgnoreCase(String str) {
1860 assert(str != null);
1861 if (getCurrentRepresentation().getData() != null) {
1862 for (String data : getCurrentRepresentation().getData()) {
1863 if (data != null && data.trim().equalsIgnoreCase(str)) {
1864 return true;
1865 }
1866 }
1867 }
1868
1869 return false;
1870 }
1871
1872 /**
1873 * Sets the link for this widget.
1874 *
1875 * @param link
1876 * The new frame link. Can be null (for no link)
1877 *
1878 * @param linker
1879 * The text item creating the link. Null if not created from
1880 * a text item.
1881 */
1882 public void setLink(String link, Text linker) {
1883 // Make sure the link is redrawn when a link is added
1884 if (link == null)
1885 invalidateLink();
1886 getSource().setLink(link);
1887 if (link != null)
1888 invalidateLink();
1889 }
1890
1891 public void setBackgroundColor(Color c) {
1892 getSource().setBackgroundColor(c);
1893 }
1894
1895 /**
1896 * @return The link for this widget. Null if none.
1897 */
1898 public String getLink() {
1899 return _textRepresentation.getLink();
1900 }
1901
1902 /**
1903 * <b>Note:</b> That if the widget has no parent (e.g. the widget is a
1904 * free-item) then the absolute link returned will be for the frameset of
1905 * the current frame.
1906 *
1907 * @return The absolute link for this item. Null if there is no link, or if
1908 * there is no parent for this widget and the current frame is
1909 * unavailable.
1910 *
1911 */
1912 public String getAbsoluteLink() {
1913
1914 // Note: cannot return the source absolute link since it does not have
1915 // a parent ... thus must manually format link
1916
1917 String link = getLink();
1918
1919 if (link == null || link.length() == 0)
1920 return null;
1921
1922 if (FrameIO.isPositiveInteger(link)) { // relative - convert to
1923 // absolute
1924
1925 // Get the frameset of this item
1926 Frame parent = getParentFrame();
1927 if (parent == null)
1928 parent = DisplayIO.getCurrentFrame();
1929 if (parent == null)
1930 return null;
1931
1932 String framesetName = parent.getFramesetName();
1933
1934 if (framesetName == null)
1935 return null;
1936
1937 return framesetName + link;
1938
1939 } else if (FrameIO.isValidFrameName(link)) { // already absolute
1940 return link;
1941 }
1942
1943 return null;
1944 }
1945
1946 /**
1947 * Sets the border color for the widget.
1948 * That is, for the source (so it is remembered) and also for all the
1949 * corners/edges.
1950 *
1951 * @param c
1952 * The color to set.
1953 */
1954 public void setWidgetEdgeColor(Color c) {
1955 for (Item i : _expediteeItems) i.setColor(c);
1956 // Above indirectly invokes setSourceBorderColor accordingly
1957 }
1958
1959 /**
1960 * Sets the thickness of the widget edge.
1961 *
1962 * @see Item#setThickness(float)
1963 *
1964 * @param thickness
1965 * The new thickness to set.
1966 */
1967 public void setWidgetEdgeThickness(float thickness) {
1968 _l1.setThickness(thickness, true);
1969 //for (Item i : _expediteeItems) i.setThickness(thickness);
1970// Above indirectly invokes setSourceThickness accordingly
1971 }
1972
1973 /**
1974 * Override to dis-allow widget thickness manipulation from the user.
1975 * @return
1976 */
1977 public boolean isWidgetEdgeThicknessAdjustable() {
1978 return true;
1979 }
1980
1981 // TODO: Maybe rename setSource* .. to update* ... These should actually be friendly!
1982 public void setSourceColor(Color c) {
1983 _textRepresentation.setColor(c);
1984 }
1985
1986 public void setSourceBorderColor(Color c) {
1987 _textRepresentation.setBorderColor(c);
1988 }
1989
1990 public void setSourceFillColor(Color c) {
1991 _textRepresentation.setFillColor(c);
1992 }
1993
1994 public void setSourceThickness(float newThickness, boolean setConnected) {
1995 _textRepresentation.setThickness(newThickness, setConnected);
1996 }
1997
1998 public void setSourceData(List<String> data) {
1999 _textRepresentation.setData(data);
2000 }
2001
2002 protected Point getOrigin() {
2003 return _d1.getPosition(); // NOTE FROM BROOK: This flips around ... the origin can be any point
2004 }
2005
2006 protected Item getFirstCorner() {
2007 return _d1;
2008 }
2009
2010 public void setAnchorLeft(Float anchor) {
2011 _anchorLeft = anchor;
2012 // Anchor left-edge corners (dots) as well
2013 _d1.setAnchorCornerX(anchor,null);
2014 _d4.setAnchorCornerX(anchor,null);
2015
2016 if (anchor != null) {
2017 setPositions(_d1, anchor, _d1.getY());
2018 onResized();
2019 }
2020
2021 // Move X-rayable item as well
2022 getCurrentRepresentation().setAnchorLeft(anchor);
2023 }
2024
2025 public void setAnchorRight(Float anchor) {
2026 _anchorRight = anchor;
2027 // Anchor right-edge corners (dots) as well
2028 _d2.setAnchorCornerX(null,anchor); // right
2029 _d3.setAnchorCornerX(null,anchor); // right
2030
2031 if (anchor != null) {
2032 setPositions(_d2, FrameGraphics.getMaxFrameSize().width - anchor, _d2.getY());
2033 onResized();
2034 }
2035
2036 if (_anchorLeft == null) {
2037 // Prefer having the X-rayable item at anchorLeft position (if defined) over moving to anchorRight
2038 getCurrentRepresentation().setAnchorRight(anchor);
2039 }
2040 }
2041
2042 public void setAnchorTop(Float anchor) {
2043 _anchorTop = anchor;
2044 // Anchor top-edge corners (dots) as well
2045 _d1.setAnchorCornerY(anchor,null);
2046 _d2.setAnchorCornerY(anchor,null);
2047
2048 if (anchor != null) {
2049 setPositions(_d2, _d2.getX(), anchor);
2050 onResized();
2051 }
2052
2053 // Move X-rayable item as well
2054 getCurrentRepresentation().setAnchorTop(anchor);
2055 }
2056
2057 public void setAnchorBottom(Float anchor) {
2058 _anchorBottom = anchor;
2059 // Anchor bottom-edge corners (dots) as well
2060 _d3.setAnchorCornerY(null,anchor);
2061 _d4.setAnchorCornerY(null,anchor);
2062
2063 if (anchor != null) {
2064 setPositions(_d3, _d3.getX(), FrameGraphics.getMaxFrameSize().height - anchor);
2065 onResized();
2066 }
2067
2068 if (_anchorTop == null) {
2069 // Prefer having the X-rayable item at anchorTop position (if defined) over moving to anchorBottom
2070 getCurrentRepresentation().setAnchorBottom(anchor);
2071 }
2072 }
2073
2074
2075 public boolean isAnchored() {
2076 return (isAnchoredX()) || (isAnchoredY());
2077 }
2078
2079 public boolean isAnchoredX() {
2080 return (_anchorLeft != null) || (_anchorRight != null);
2081 }
2082
2083 public boolean isAnchoredY() {
2084 return (_anchorTop != null) || (_anchorBottom != null);
2085 }
2086
2087 /**
2088 * Call from expeditee for representing the name of the item.
2089 * Override to return custom name.
2090 *
2091 * Note: Used for the new frame title when creating links for widgets.
2092 *
2093 * @return
2094 * The name representing this widget
2095 */
2096 public String getName() {
2097 return this.toString();
2098 }
2099
2100 /**
2101 * Event called when the widget is left clicked while there are items attached to the FreeItems buffer.
2102 * Used to enable expeditee like text-widget interaction for left mouse clicks.
2103 * @return true if event was handled (no pass through), otherwise false.
2104 */
2105 public boolean ItemsLeftClickDropped() {
2106 return false;
2107 }
2108
2109 /**
2110 * Event called when the widget is middle clicked while there are items attached to the FreeItems buffer.
2111 * Used to enable expeditee like text-widget interaction for middle mouse clicks.
2112 * @return true if event was handled (no pass through), otherwise false.
2113 */
2114 public boolean ItemsMiddleClickDropped() {
2115 return false;
2116 }
2117
2118 /**
2119 * Event called when the widget is left clicked while there are items attached to the FreeItems buffer.
2120 * Used to enable expeditee like text-widget interaction for right mouse clicks
2121 * @return true if event was handled (no pass through), otherwise false.
2122 */
2123 public boolean ItemsRightClickDropped() {
2124 return false;
2125 }
2126
2127 /**
2128 * Makes sure we add our KeyListener to every child component,
2129 * since it seems that's the only way to make sure we capture key events in Java
2130 * (without using KeyBindings which seem to only support the keyTyped event)
2131 */
2132 @Override
2133 public void componentAdded(ContainerEvent e) {
2134 if(e == null || e.getChild() == null) {
2135 return;
2136 }
2137 keyListenerToChildren(e.getChild(), true);
2138 }
2139
2140 @Override
2141 public void componentRemoved(ContainerEvent e) {
2142 if(e == null || e.getChild() == null) {
2143 return;
2144 }
2145 keyListenerToChildren(e.getChild(), false);
2146 }
2147
2148 @Override
2149 public void keyTyped(KeyEvent e) {
2150
2151 int keyCode = e.getKeyCode();
2152
2153 if (keyCode >= KeyEvent.VK_F1 && keyCode <= KeyEvent.VK_F12) {
2154 FrameKeyboardActions.getInstance().keyTyped(e);
2155 }
2156 }
2157
2158 @Override
2159 public void keyPressed(KeyEvent e) {
2160 int keyCode = e.getKeyCode();
2161
2162 if (keyCode >= KeyEvent.VK_F1 && keyCode <= KeyEvent.VK_F12) {
2163 FrameKeyboardActions.getInstance().keyPressed(e);
2164 }
2165 }
2166
2167 @Override
2168 public void keyReleased(KeyEvent e) {
2169 int keyCode = e.getKeyCode();
2170
2171 if (keyCode >= KeyEvent.VK_F1 && keyCode <= KeyEvent.VK_F12) {
2172 FrameKeyboardActions.getInstance().keyReleased(e);
2173 }
2174 }
2175
2176 private void keyListenerToChildren(Component parent, boolean add) {
2177 List<Component> components = new LinkedList<Component>();
2178 components.add(parent);
2179 while(!components.isEmpty()) {
2180 Component c = components.remove(0);
2181 if(c instanceof Container) {
2182 components.addAll(Arrays.asList(((Container)c).getComponents()));
2183 }
2184 if(add && !Arrays.asList(c.getKeyListeners()).contains(this)) {
2185 c.addKeyListener(this);
2186 } else if (!add && Arrays.asList(c.getKeyListeners()).contains(this)) {
2187 c.removeKeyListener(this);
2188 }
2189 }
2190 }
2191
2192 public void onResized() {
2193 invalidateSelf();
2194 onBoundsChanged();
2195 layout(_swingComponent);
2196 }
2197
2198}
Note: See TracBrowser for help on using the repository browser.