source: trunk/src/org/expeditee/gui/Frame.java@ 1102

Last change on this file since 1102 was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

File size: 60.1 KB
Line 
1/**
2 * Frame.java
3 * Copyright (C) 2010 New Zealand Digital Library, http://expeditee.org
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19package org.expeditee.gui;
20
21import java.sql.Time;
22import java.util.ArrayList;
23import java.util.Collection;
24import java.util.Collections;
25import java.util.HashMap;
26import java.util.HashSet;
27import java.util.LinkedHashSet;
28import java.util.LinkedList;
29import java.util.List;
30import java.util.Map;
31import java.util.Stack;
32
33import org.expeditee.actions.Simple;
34import org.expeditee.core.Colour;
35import org.expeditee.core.Image;
36import org.expeditee.core.bounds.PolygonBounds;
37import org.expeditee.gio.gesture.StandardGestureActions;
38import org.expeditee.gio.input.StandardInputEventListeners;
39import org.expeditee.gio.input.KBMInputEvent.Key;
40import org.expeditee.io.Conversion;
41import org.expeditee.items.Constraint;
42import org.expeditee.items.Dot;
43import org.expeditee.items.Item;
44import org.expeditee.items.Item.HighlightMode;
45import org.expeditee.items.ItemAppearence;
46import org.expeditee.items.ItemParentStateChangedEvent;
47import org.expeditee.items.ItemUtils;
48import org.expeditee.items.Line;
49import org.expeditee.items.PermissionPair;
50import org.expeditee.items.Text;
51import org.expeditee.items.UserAppliedPermission;
52import org.expeditee.items.XRayable;
53import org.expeditee.items.widgets.Widget;
54import org.expeditee.items.widgets.WidgetCorner;
55import org.expeditee.settings.UserSettings;
56import org.expeditee.settings.templates.TemplateSettings;
57import org.expeditee.simple.UnitTestFailedException;
58import org.expeditee.stats.Formatter;
59import org.expeditee.stats.SessionStats;
60
61/**
62 * Represents a Expeditee Frame that is displayed on the screen. Also is a
63 * registered MouseListener on the Browser, and processes any MouseEvents
64 * directly.
65 *
66 * @author jdm18
67 *
68 */
69public class Frame {
70
71 /** The frame number to indicate this is a virtual frame. */
72 public static final int VIRTUAL_FRAME_NUMBER = -1;
73
74 /** The background colour the frame name should take if the frame has user permission level 'none'. */
75 public static final Colour FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_NONE = Colour.FromRGB255(255, 220, 220);
76 /** The background colour the frame name should take if the frame has user permission level 'followLinks'. */
77 public static final Colour FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_FOLLOW_LINKS = Colour.FromRGB255(255, 230, 135);
78 /** The background colour the frame name should take if the frame has user permission level 'copy'. */
79 public static final Colour FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_COPY = Colour.FromRGB255(255, 255, 155);
80 /** The background colour the frame name should take if the frame has user permission level 'createFrames'. */
81 public static final Colour FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_CREATE_FRAMES = Colour.FromRGB255(220, 255, 220);
82 /** The background colour the frame name should take if the frame has user permission level 'full'. */
83 public static final Colour FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_FULL = null;
84
85 private boolean _protectionChanged = false;
86
87 // The various attributes of this Frame
88 private String _frameset = null;
89
90 private int _number = -1;
91
92 private int _version = 0;
93
94 private PermissionPair _permissionPair = null;
95
96 private String _owner = null;
97
98 private String _creationDate = null;
99
100 private String _modifiedUser = null;
101
102 private String _modifiedDate = null;
103
104 private String _frozenDate = null;
105
106 // Background color is clear
107 private Colour _background = null;
108
109 // Foreground color is automatic by default
110 private Colour _foreground = null;
111
112 private String path;
113
114 private boolean _isLocal = true;
115
116 private boolean _sorted = true;
117
118 /** Whether the frame has changed and therefore needs saving. */
119 private boolean _change = false;
120
121 /** Whether the frame has been saved. */
122 private boolean _saved = false;
123
124 // list of deleted items that can be restored
125 private Stack<History> _undo = new Stack<History>();
126 private Stack<History> _redo = new Stack<History>();
127
128 // basically just a list of smaller objects?
129 // maybe a hashtable (id -> item?)
130 // Note: Needs to be able to be iterated through (for painting)
131 private List<Item> _body = new ArrayList<Item>();
132
133 // for drawing purposes
134 private List<Widget> _iWidgets = new ArrayList<Widget>();
135
136 private int _lineCount = 0;
137
138 private int _itemCount = 1;
139
140 // The frameName to display on the screen
141 private Text _frameName = null;
142
143 private Map<Overlay, Frame> _overlays = new HashMap<Overlay, Frame>();
144
145 private List<Vector> _vectors = new ArrayList<Vector>();
146
147 private Image _buffer = null;
148
149 private boolean _validBuffer = true;
150
151 private Time _activeTime = new Time(0);
152
153 private Time _darkTime = new Time(0);
154
155 private Collection<Item> _interactableItems = new LinkedHashSet<Item>();
156
157 private Collection<Item> _overlayItems = new LinkedHashSet<Item>();
158
159 private Collection<Item> _vectorItems = new LinkedHashSet<Item>();
160
161 private Text _dotTemplate = TemplateSettings.DotTemplate.get().copy();
162
163 Map<String, Text> _annotations = null;
164
165 private Collection<FrameObserver> _observers = new HashSet<FrameObserver>();
166
167 /** Default constructor, nothing is set. */
168 public Frame()
169 {
170 }
171
172 public boolean isReadOnly()
173 {
174 return !_frameName.hasPermission(UserAppliedPermission.full) && !_protectionChanged;
175 }
176
177 public void reset()
178 {
179 refreshItemPermissions(UserAppliedPermission.full);
180 resetDot();
181 SessionStats.NewFrameSession();
182 }
183
184 private void resetDot()
185 {
186 _dotTemplate.setColor(TemplateSettings.ColorWheel.getSafe(1));
187 _dotTemplate.setFillColor(TemplateSettings.FillColorWheel.getSafe(0));
188 }
189
190 public void nextDot()
191 {
192 _dotTemplate.setFillColor(ColorUtils.getNextColor(_dotTemplate.getFillColor(), TemplateSettings.FillColorWheel.get(), null));
193 _dotTemplate.setColor(ColorUtils.getNextColor(_dotTemplate.getColor(), TemplateSettings.ColorWheel.get(), null));
194 if (_dotTemplate.getColor() == null || _dotTemplate.getColor().equals(Colour.WHITE)) resetDot();
195 }
196
197 public Image getBuffer()
198 {
199 return _buffer;
200 }
201
202 public void setBuffer(Image newBuffer)
203 {
204 _buffer = newBuffer;
205 }
206
207 public boolean isBufferValid()
208 {
209 if (_buffer == null) return false;
210
211 return _validBuffer;
212 }
213
214 private void setBufferValid(boolean newValue)
215 {
216 _validBuffer = newValue;
217 }
218
219 public int getNextItemID()
220 {
221 return ++_itemCount;
222 }
223
224 public void updateIDs(List<Item> items)
225 {
226 for (Item i : items) {
227 if (!(i instanceof Line)) {
228 i.setID(getNextItemID());
229 } else {
230 i.setID(++_lineCount);
231 }
232 }
233 }
234
235 /**
236 *
237 * @return The interactive widgets that are currently anchored in this frame.
238 * Hence it excludes free-widgets. Returns a copy
239 */
240 public List<Widget> getInteractiveWidgets()
241 {
242 LinkedList<Widget> clone = new LinkedList<Widget>();
243 clone.addAll(this._iWidgets);
244 return clone;
245 }
246
247 /**
248 * Returns whether this Frame has been changed and required saving to disk.
249 *
250 * @return True if this Frame has been altered, false otherwise.
251 */
252 public boolean hasChanged()
253 {
254 // virtual frames are never saved
255 if (_number == VIRTUAL_FRAME_NUMBER) return false;
256
257 return _change;
258 }
259
260 /**
261 * Sets whether this Frame should be saved to disk.
262 *
263 * @param value
264 * True if this Frame should be saved to disk, False otherwise.
265 */
266 public void setChanged(boolean value)
267 {
268 if (_change == value) return;
269
270 _change = value;
271
272 if (_change) {
273 setBufferValid(false);
274 _saved = false;
275 }
276 }
277
278 /**
279 * Notify items observing the data on this frame that the frame content has
280 * changed.
281 *
282 * @param recalculate
283 * true if the frame should be recalculated first.
284 */
285 public void notifyObservers(boolean bRecalculate)
286 {
287 if (bRecalculate) recalculate();
288 // Notify the frame listeners that the frame has changed
289 /*
290 * Avoid ConcurrentMod Exceptions when user anchors an item onto this
291 * frame which is observing this frame, by NOT using foreach loop.
292 * Calling update on a dataFrameWidget resets its subjects hence
293 * changing this frames observer list.
294 */
295 Collection<FrameObserver> observersCopy = new LinkedList<FrameObserver>(_observers);
296 // System.out.println(++updateCount + " update");
297
298 for (FrameObserver fl : observersCopy) {
299 if (fl.isVisible()) fl.update();
300 }
301 }
302
303 // indicates the frame has changed
304 public void change()
305 {
306 setChanged(true);
307 _interactableItems.clear();
308 }
309
310 /**
311 * Returns an ArrayList of all Items currently on the Frame (excludes Items
312 * attached to the cursor).
313 *
314 * @return The list of Item objects that are on this Frame.
315 */
316 public List<Item> getItems(boolean visible)
317 {
318 if (!_sorted) {
319 for(int i = 0; i < _body.size();) {
320 if(_body.get(i) == null) {
321 _body.remove(i);
322 } else {
323 i++;
324 }
325 }
326 Collections.sort(_body);
327 _sorted = true;
328 }
329
330 List<Item> items = new ArrayList<Item>();
331
332 for (Item i : _body) {
333 if (i == null) continue;
334
335 if (i.isVisible() || (!visible && !i.isDeleted())) {
336 items.add(i);
337 }
338 }
339
340 return items;
341 }
342
343 /** TODO: Comment. cts16 */
344 public List<Item> getItems()
345 {
346 return getItems(false);
347 }
348
349 /**
350 * @param i
351 * Item to check if contained in this frame
352 * @return True if this frame contains i.
353 */
354 public boolean containsItem(Item i)
355 {
356 if (i == null) throw new NullPointerException("i");
357
358 return _body.contains(i);
359 }
360
361 /**
362 * Returns a list of all the non annotation text items on the frame which
363 * are not the title or frame name or special annotation items.
364 *
365 * @param includeAnnotations
366 * true if annotation items without special meaning should be
367 * included
368 * @param includeLineEnds
369 * true if text on the end of lines should be included in the
370 * list
371 * @return the list of body text items.
372 */
373 public List<Text> getBodyTextItems(boolean includeAnnotations)
374 {
375 List<Text> bodyTextItems = new ArrayList<Text>();
376
377 for (Item i : getItems(true)) {
378 // only add up normal body text items
379 if ((i instanceof Text) && ((includeAnnotations && !((Text) i).isSpecialAnnotation()) || !i.isAnnotation()) && !i.isLineEnd()) {
380 bodyTextItems.add((Text) i);
381 }
382 }
383
384 bodyTextItems.remove(getTitleItem());
385
386 return bodyTextItems;
387 }
388
389 public Collection<Item> getNonAnnotationItems(boolean removeTitle)
390 {
391 Collection<Item> items = new ArrayList<Item>();
392 for (Item i : getItems(true)) {
393 // only add up normal body text items
394 if (!i.isAnnotation()) items.add(i);
395 }
396
397 if (removeTitle) {
398 items.remove(getTitleItem());
399 }
400
401 return items;
402 }
403
404 /**
405 * Gets the last item on the frame that is a non annotation item but is also
406 * text.
407 *
408 * @return the last non annotation text item.
409 */
410 public Item getLastNonAnnotationTextItem()
411 {
412 List<Item> items = getItems();
413
414 // find the last non-annotation text item
415 for (int i = (items.size() - 1); i >= 0; i--) {
416 Item it = items.get(i);
417
418 if (it instanceof Text && !it.isAnnotation()) {
419 return (Item) it;
420 }
421 }
422 return null;
423 }
424
425 /**
426 * Iterates through the list of items on the frame, and returns one with the
427 * given id if one exists, otherwise returns null.
428 *
429 * @param id
430 * The id to search for in the list of items
431 * @return The item on this frame with the given ID, or null if one is not
432 * found.
433 */
434 public Item getItemWithID(int id)
435 {
436 for (Item i : _body) {
437 if (i.getID() == id) {
438 return i;
439 }
440 }
441 return null;
442 }
443
444 /**
445 * Sets this Frame's Title which is displayed in the top left corner.
446 *
447 * @param title
448 * The title to assign to this Frame
449 */
450 public void setTitle(String title)
451 {
452 if (title == null || title.equals("")) return;
453
454 boolean oldchange = _change;
455
456 // remove any numbering this title has
457 title = title.replaceAll("^\\d*[.] *", "");
458 Text frameTitle = getTitleItem();
459
460 if (frameTitle == null) {
461 if (TemplateSettings.TitleTemplate.get() == null) {
462 frameTitle = new Text(getNextItemID(), title);
463 } else {
464 frameTitle = TemplateSettings.TitleTemplate.get().copy();
465 frameTitle.setID(this.getNextItemID());
466 frameTitle.setText(title);
467 }
468 /*
469 * Need to set the parent otherwise an exception is thrown when
470 * new profile is created
471 */
472 frameTitle.setParent(this);
473 frameTitle.resetTitlePosition();
474 addItem(frameTitle);
475 } else {
476 // If it begins with a tag remove it
477
478 // Remove the @ symbol if it is there
479 // title = ItemUtils.StripTagSymbol(title);
480 frameTitle.setText(title);
481 // If the @ symbol is followed by numbering or a bullet remove that too
482 String autoBulletText = StandardGestureActions.getAutoBullet(title);
483 if (autoBulletText.length() > 0)
484 frameTitle.stripFirstWord();
485 }
486 // TODO Widgets... check this out
487 // Brook: Cannot figure what is going on above... widget annot titles
488 // should be stripped always
489 if (ItemUtils.startsWithTag(frameTitle, ItemUtils.GetTag(ItemUtils.TAG_IWIDGET))) {
490 frameTitle.stripFirstWord();
491 }
492
493 FrameUtils.Parse(this);
494
495 // do not save if this is the only change
496 setChanged(oldchange);
497 }
498
499 public Text getTitleItem()
500 {
501 List<Item> items = getVisibleItems();
502
503 for (Item i : items) {
504 if (i instanceof Text && i.getX() < UserSettings.TitlePosition.get() && i.getY() < UserSettings.TitlePosition.get()) {
505 return (Text) i;
506 }
507 }
508
509 return null;
510 }
511
512 public String getTitle()
513 {
514 Text title = getTitleItem();
515 if (title == null) return getName();
516
517 return title.getFirstLine();
518 }
519
520 public Item getNameItem()
521 {
522 return _frameName;
523 }
524
525 public Text getItemTemplate()
526 {
527 return getTemplate(TemplateSettings.ItemTemplate.get(), ItemUtils.TAG_ITEM_TEMPLATE);
528 }
529
530 public Text getAnnotationTemplate()
531 {
532 Text t = getTemplate(TemplateSettings.AnnotationTemplate.get(), ItemUtils.TAG_ANNOTATION_TEMPLATE);
533
534 if (t == null) {
535 t = getItemTemplate();
536 }
537
538 return t;
539 }
540
541 public Text getStatTemplate()
542 {
543 SessionStats.CreatedText();
544 Text t = getTemplate(TemplateSettings.StatTemplate.get(), ItemUtils.TAG_STAT_TEMPLATE);
545
546 if (t == null) {
547 t = getItemTemplate();
548 }
549
550 return t;
551 }
552
553 public Item getTooltipTextItem(String tooltipText)
554 {
555 return getTextItem(tooltipText, TemplateSettings.TooltipTemplate.get().copy());
556 }
557
558 public Item getStatsTextItem(String itemText)
559 {
560 return getTextItem(itemText, getStatTemplate());
561 }
562
563 public Item getTextItem(String itemText)
564 {
565 return getTextItem(itemText, getItemTemplate());
566 }
567
568 private Item getTextItem(String itemText, Text template)
569 {
570 Text t = template;
571 // We dont want the stats to wrap at all
572 // t.setMaxWidth(Integer.MAX_VALUE);
573 t.setPosition(DisplayController.getMousePosition());
574 // The next line is needed to make sure the item is removed from the
575 // frame when picked up
576 t.setParent(this);
577 t.setText(itemText);
578 return t;
579 }
580
581 public Text getCodeCommentTemplate()
582 {
583 Text t = getTemplate(TemplateSettings.CommentTemplate.get(), ItemUtils.TAG_CODE_COMMENT_TEMPLATE);
584
585 if (t == null) {
586 t = getItemTemplate();
587 }
588
589 return t;
590 }
591
592
593 /**
594 * Returns any items on this frame that are within the given Shape. Also
595 * returns any Items on overlay frames that are within the Shape.
596 *
597 * @param shape
598 * The Shape to search for Items in
599 * @return All Items on this Frame or overlayed Frames for which
600 * Item.intersects(shape) return true.
601 */
602 public Collection<Item> getItemsWithin(PolygonBounds poly)
603 {
604 Collection<Item> results = new LinkedHashSet<Item>();
605 for (Item i : getVisibleItems()) {
606 if (i.intersects(poly)) {
607 if (i instanceof XRayable) {
608 results.addAll(i.getConnected());
609 // Dont add circle centers
610 // TODO change this to be isCircle center
611 } else if (!i.hasEnclosures()) {
612 results.add(i);
613 }
614 }
615 }
616
617 for (Overlay o : _overlays.keySet()) {
618 results.addAll(o.Frame.getItemsWithin(poly));
619 }
620
621 for (Item i : getVectorItems()) {
622 if (i.intersects(poly)) {
623 // This assumes a results is a set
624 results.add(i.getEditTarget());
625 }
626 }
627
628 return results;
629 }
630
631 /**
632 * Sets the name of this Frame to the given String, to be displayed in the
633 * upper right corner.
634 *
635 * @param name
636 * The name to use for this Frame.
637 */
638 public void setFrameset(String name)
639 {
640 _frameset = name;
641 }
642
643 public void setName(String framename)
644 {
645 int num = Conversion.getFrameNumber(framename);
646 String frameset = Conversion.getFramesetName(framename, false);
647
648 setName(frameset, num);
649 }
650
651 /**
652 * Sets the frame number of this Frame to the given integer
653 *
654 * @param number
655 * The number to set as the frame number
656 */
657 public void setFrameNumber(int number)
658 {
659 assert (number >= 0);
660
661 if (_number == number) return;
662
663 _number = number;
664 boolean oldchange = _change;
665
666 int id;
667
668 if (_frameName != null) {
669 id = _frameName.getID();
670 } else {
671 id = -1 * getNextItemID();
672 }
673
674 _frameName = new Text(id);
675 _frameName.setParent(this);
676 _frameName.setText(getFramesetName() + _number);
677 _frameName.resetFrameNamePosition();
678 setChanged(oldchange);
679 }
680
681 /**
682 * Returns the number of this Frame.
683 *
684 * @return The Frame number of this Frame or -1 if it is not set.
685 */
686 public int getNumber()
687 {
688 return _number;
689 }
690
691 /**
692 * Increments the version of this Frame to the given String.
693 *
694 * @param version
695 * The version to use for this Frame.
696 */
697 public void setVersion(int version)
698 {
699 _version = version;
700 }
701
702 /**
703 * Sets the protection of this Frame to the given String.
704 *
705 * @param protection
706 * The protection to use for this Frame.
707 */
708 public void setPermission(PermissionPair permission)
709 {
710 if (_permissionPair != null && _permissionPair.getPermission(this._owner).equals(permission)) {
711 _protectionChanged = true;
712 }
713
714 _permissionPair = new PermissionPair(permission);
715
716 if (_body.size() > 0) refreshItemPermissions(permission.getPermission(_owner));
717 }
718
719 /**
720 * Sets the owner of this Frame to the given String.
721 *
722 * @param owner
723 * The owner to use for this Frame.
724 */
725 public void setOwner(String owner)
726 {
727 _owner = owner;
728 }
729
730 /**
731 * Sets the created date of this Frame to the given String.
732 *
733 * @param date
734 * The date to use for this Frame.
735 */
736 public void setDateCreated(String date)
737 {
738 _creationDate = date;
739 _modifiedDate = date;
740 for (Item i : _body) {
741 i.setDateCreated(date);
742 }
743 }
744
745 /**
746 * Resets the dates and version numbers for newly created frames.
747 *
748 */
749 public void resetDateCreated()
750 {
751 setDateCreated(Formatter.getDateTime());
752 resetTimes();
753 setVersion(0);
754 }
755
756 private void resetTimes()
757 {
758 setActiveTime(new Time(0));
759 setDarkTime(new Time(0));
760 }
761
762 /**
763 * Sets the last modifying user of this Frame to the given String.
764 *
765 * @param user
766 * The user to set as the last modifying user.
767 */
768 public void setLastModifyUser(String user)
769 {
770 _modifiedUser = user;
771 }
772
773 /**
774 * Sets the last modified date of this Frame to the given String.
775 *
776 * @param date
777 * The date to set as the last modified date.
778 */
779 public void setLastModifyDate(String date)
780 {
781 _modifiedDate = date;
782 }
783
784 /**
785 * Sets the last frozen date of this Frame to the given String.
786 *
787 * @param date
788 * The date to set as the last frozen date.
789 */
790 public void setFrozenDate(String date)
791 {
792 _frozenDate = date;
793 }
794
795 public void setResort(boolean value)
796 {
797 _sorted = !value;
798 }
799
800 /**
801 * Adds the given Item to the body of this Frame.
802 *
803 * @param item
804 * The Item to add to this Frame.
805 */
806 public void addItem(Item item)
807 {
808 addItem(item, true);
809 }
810
811 public void addItem(Item item, boolean recalculate)
812 {
813 if (item == null || item.equals(_frameName) || _body.contains(item)) return;
814
815 // When an annotation item is anchored the annotation list must be
816 // refreshed
817 if (item.isAnnotation()) {
818 clearAnnotations();
819 }
820
821 if (item instanceof Line) _lineCount++;
822
823 _itemCount = Math.max(_itemCount, item.getID());
824
825 _body.add(item);
826 item.setParent(this);
827 item.setFloating(false); // esnure that it is anchored
828
829 item.invalidateCommonTrait(ItemAppearence.Added);
830
831 // If the item is a line end and has constraints with items already
832 // on the frame then make sure the constraints hold
833 if (item.isLineEnd()) {
834 item.setPosition(item.getPosition());
835 }
836
837 _sorted = false;
838
839 // item.setMaxWidth(FrameGraphics.getMaxFrameSize().width);
840 // add widget items to the list of widgets
841 if (item instanceof WidgetCorner) {
842 Widget iw = ((WidgetCorner) item).getWidgetSource();
843 if (!this._iWidgets.contains(iw)) { // A set would have been
844 if (StandardInputEventListeners.kbmStateListener.isKeyDown(Key.CTRL)) {
845 _iWidgets.add(iw);
846 } else {
847 _iWidgets.add(0, iw);
848 }
849 }
850 }
851
852 item.onParentStateChanged(new ItemParentStateChangedEvent(this, ItemParentStateChangedEvent.EVENT_TYPE_ADDED));
853
854 // if (recalculate && item.recalculateWhenChanged())
855 // recalculate();
856
857 change();
858 }
859
860 public void refreshSize()
861 {
862 boolean bReparse = false;
863
864 for (Item i : getItems()) {
865 Integer anchorLeft = i.getAnchorLeft();
866 Integer anchorRight = i.getAnchorRight();
867 Integer anchorTop = i.getAnchorTop();
868 Integer anchorBottom = i.getAnchorBottom();
869
870
871 if (anchorLeft != null) {
872 i.setAnchorLeft(anchorLeft);
873 if (i.hasVector()) {
874 bReparse = true;
875 }
876 }
877
878 if (anchorRight != null) {
879 i.setAnchorRight(anchorRight);
880 if (i.hasVector()) {
881 bReparse = true;
882 }
883 }
884
885 if (anchorTop != null) {
886 i.setAnchorTop(anchorTop);
887 if (i.hasVector()) {
888 bReparse = true;
889 }
890 }
891
892 if (anchorBottom != null) {
893 i.setAnchorBottom(anchorBottom);
894 if (i.hasVector()) {
895 bReparse = true;
896 }
897 }
898 }
899
900 // Do the anchors on the overlays
901 for (Overlay o : getOverlays()) {
902 o.Frame.refreshSize();
903 }
904
905 if (bReparse) {
906 FrameUtils.Parse(this, false);
907 }
908
909 _frameName.resetFrameNamePosition();
910 }
911
912 public void addAllItems(Collection<Item> toAdd)
913 {
914 for (Item i : toAdd) {
915 // If an annotation is being deleted clear the annotation list
916 if (i.isAnnotation()) i.getParentOrCurrentFrame().clearAnnotations();
917 // TODO Improve efficiency when addAll is called
918 addItem(i);
919 }
920 }
921
922 public void removeAllItems(Collection<Item> toRemove)
923 {
924 for (Item i : toRemove) {
925 // If an annotation is being deleted clear the annotation list
926 if (i.isAnnotation()) i.getParentOrCurrentFrame().clearAnnotations();
927 removeItem(i);
928 }
929 }
930
931 public void removeItem(Item item)
932 {
933 removeItem(item, true);
934 }
935
936 public void removeItem(Item item, boolean recalculate)
937 {
938 // If an annotation is being deleted clear the annotation list
939 if (item.isAnnotation()) item.getParentOrCurrentFrame().clearAnnotations();
940
941 if (_body.remove(item)) {
942 change();
943 // Remove widgets from the widget list
944 if (item != null) {
945 item.onParentStateChanged(new ItemParentStateChangedEvent(this, ItemParentStateChangedEvent.EVENT_TYPE_REMOVED));
946
947 if (item instanceof WidgetCorner) {
948 _iWidgets.remove(((WidgetCorner) item).getWidgetSource());
949 }
950
951 item.invalidateCommonTrait(ItemAppearence.Removed);
952 }
953 // TODO Improve efficiency when removeAll is called
954 // if (recalculate && item.recalculateWhenChanged())
955 // recalculate();
956 }
957 }
958
959 /**
960 * Adds the given History event to the stack.
961 *
962 * @param stack The stack to add to
963 * @param items The items to put in the event
964 * @param type The type of event that occurred
965 */
966 private void addToUndo(Collection<Item> items, History.Type type)
967 {
968 if (items.size() < 1) return;
969
970 _undo.push(new History(items, type));
971 }
972
973 public void addToUndoDelete(Collection<Item> items)
974 {
975 addToUndo(items, History.Type.deletion);
976 }
977
978 public void addToUndoMove(Collection<Item> items)
979 {
980 addToUndo(items, History.Type.movement);
981 }
982
983 public void undo()
984 {
985 boolean bReparse = false;
986 boolean bRecalculate = false;
987
988 if (_undo.size() <= 0) return;
989
990 History undo = _undo.pop();
991
992 // System.out.println("Undoing: " + undo);
993
994 switch(undo.type) {
995 case deletion:
996 _redo.push(undo);
997 for(Item i : undo.items) {
998 _body.add(i);
999 bReparse |= i.hasOverlay();
1000 bRecalculate |= i.recalculateWhenChanged();
1001 if (i instanceof Line) {
1002 Line line = (Line) i;
1003 line.getStartItem().addLine(line);
1004 line.getEndItem().addLine(line);
1005 } else {
1006 i.setOffset(0, 0);
1007 }
1008 }
1009 break;
1010 case movement:
1011 List<Item> changed = new LinkedList<Item>(_body);
1012 changed.retainAll(undo.items);
1013 _redo.push(new History(changed, History.Type.movement));
1014 for(Item i : undo.items) {
1015 int index;
1016 if(i.isVisible() && (index = _body.indexOf(i)) != -1) {
1017 _body.set(index, i);
1018 }
1019 }
1020 break;
1021 }
1022
1023 change();
1024
1025 StandardGestureActions.refreshHighlights();
1026
1027 if (bReparse) {
1028 FrameUtils.Parse(this, false, false);
1029 } else {
1030 notifyObservers(bRecalculate);
1031 }
1032
1033 // always request a refresh otherwise filled shapes
1034 // that were broken by a deletion and then reconnected by the undo
1035 // don't get filled until the user otherwise causes them to redraw
1036 DisplayController.requestRefresh(false);
1037 // ItemUtils.EnclosedCheck(_body);
1038 ItemUtils.Justify(this);
1039 }
1040
1041 public void redo()
1042 {
1043 boolean bReparse = false;
1044 boolean bRecalculate = false;
1045
1046 if (_redo.size() <= 0) return;
1047
1048 History redo = _redo.pop();
1049
1050 // System.out.println("Redoing: " + redo);
1051
1052 switch(redo.type) {
1053 case deletion:
1054 _undo.push(redo);
1055 for(Item i : redo.items) {
1056 _body.remove(i);
1057 bReparse |= i.hasOverlay();
1058 bRecalculate |= i.recalculateWhenChanged();
1059 if (i instanceof Line) {
1060 Line line = (Line) i;
1061 line.getStartItem().removeLine(line);
1062 line.getEndItem().removeLine(line);
1063 } else {
1064 i.setOffset(0, 0);
1065 }
1066 }
1067 break;
1068 case movement:
1069 List<Item> changed = new LinkedList<Item>(_body);
1070 changed.retainAll(redo.items);
1071 _undo.push(new History(changed, History.Type.movement));
1072 for(Item i : redo.items) {
1073 int index;
1074 if(i.isVisible() && (index = _body.indexOf(i)) != -1) {
1075 _body.set(index, i);
1076 }
1077 }
1078 break;
1079 }
1080
1081 change();
1082
1083 StandardGestureActions.refreshHighlights();
1084
1085 if (bReparse) {
1086 FrameUtils.Parse(this, false, false);
1087 } else {
1088 notifyObservers(bRecalculate);
1089 }
1090
1091 // always request a refresh otherwise filled shapes
1092 // that were broken by a deletion and then reconnected by the undo
1093 // don't get filled until the user otherwise causes them to redraw
1094 DisplayController.requestRefresh(false);
1095 // ItemUtils.EnclosedCheck(_body);
1096 ItemUtils.Justify(this);
1097 }
1098
1099 /**
1100 * Returns the frameset of this Frame
1101 *
1102 * @return The name of this Frame's frameset.
1103 */
1104 public String getFramesetName()
1105 {
1106 return _frameset;
1107 }
1108
1109 public String getName()
1110 {
1111 return getFramesetName() + _number;
1112 }
1113
1114 /**
1115 * Returns the format version of this Frame
1116 *
1117 * @return The version of this Frame.
1118 */
1119 public int getVersion()
1120 {
1121 return _version;
1122 }
1123
1124 public PermissionPair getPermission()
1125 {
1126 return _permissionPair;
1127 }
1128
1129 public UserAppliedPermission getUserAppliedPermission()
1130 {
1131 return getUserAppliedPermission(UserAppliedPermission.full);
1132 }
1133
1134 public UserAppliedPermission getUserAppliedPermission(UserAppliedPermission defaultPermission)
1135 {
1136 if (_permissionPair == null) return defaultPermission;
1137
1138 return _permissionPair.getPermission(_owner);
1139 }
1140
1141 public String getOwner()
1142 {
1143 return _owner;
1144 }
1145
1146 public String getDateCreated()
1147 {
1148 return _creationDate;
1149 }
1150
1151 public String getLastModifyUser()
1152 {
1153 return _modifiedUser;
1154 }
1155
1156 public String getLastModifyDate()
1157 {
1158 return _modifiedDate;
1159 }
1160
1161 public String getFrozenDate()
1162 {
1163 return _frozenDate;
1164 }
1165
1166 public void setBackgroundColor(Colour back)
1167 {
1168 _background = back;
1169
1170 change();
1171
1172 if (this == DisplayController.getCurrentFrame()) {
1173 DisplayController.requestRefresh(false);
1174 }
1175 }
1176
1177 public Colour getBackgroundColor()
1178 {
1179 return _background;
1180 }
1181
1182 public Colour getPaintBackgroundColor()
1183 {
1184 // If null... return white
1185 if (_background == null) {
1186 return Item.DEFAULT_BACKGROUND;
1187 }
1188
1189 return _background;
1190 }
1191
1192 public void setForegroundColor(Colour front)
1193 {
1194 _foreground = front;
1195
1196 change();
1197 }
1198
1199 public Colour getForegroundColor()
1200 {
1201 return _foreground;
1202 }
1203
1204 public Colour getPaintForegroundColor()
1205 {
1206 final int GRAY = Colour.GREY.getBlue();
1207 final int THRESHOLD = Colour.FromComponent255(10);
1208
1209 if (_foreground == null) {
1210 Colour back = getPaintBackgroundColor();
1211 if (Math.abs(back.getRed() - GRAY) < THRESHOLD
1212 && Math.abs(back.getBlue() - GRAY) < THRESHOLD
1213 && Math.abs(back.getGreen() - GRAY) < THRESHOLD)
1214 {
1215 return Colour.WHITE;
1216 }
1217
1218 Colour fore = back.inverse();
1219
1220 return fore;
1221 }
1222
1223 return _foreground;
1224 }
1225
1226 public String toString()
1227 {
1228 StringBuilder s = new StringBuilder();
1229 s.append(String.format("Name: %s%d%n", _frameset, _number));
1230 s.append(String.format("Version: %d%n", _version));
1231 // s.append(String.format("Permission: %s%n", _permission.toString()));
1232 // s.append(String.format("Owner: %s%n", _owner));
1233 // s.append(String.format("Date Created: %s%n", _creationDate));
1234 // s.append(String.format("Last Mod. User: %s%n", _modifiedUser));
1235 // s.append(String.format("Last Mod. Date: %s%n", _modifiedDate));
1236 s.append(String.format("Items: %d%n", _body.size()));
1237 return s.toString();
1238 }
1239
1240 public Text getTextAbove(Text current)
1241 {
1242 Collection<Text> currentTextItems = FrameUtils.getCurrentTextItems();
1243 List<Text> toCheck = new ArrayList<Text>();
1244
1245 if (currentTextItems.contains(current)) {
1246 toCheck.addAll(currentTextItems);
1247 } else {
1248 toCheck.addAll(getTextItems());
1249 }
1250
1251 // Make sure the items are sorted
1252 Collections.sort(toCheck);
1253
1254 int ind = toCheck.indexOf(current);
1255 if (ind == -1) return null;
1256
1257 // loop through all items above this one, return the first match
1258 for (int i = ind - 1; i >= 0; i--) {
1259 Text check = toCheck.get(i);
1260 if (FrameUtils.inSameColumn(check, current)) return check;
1261 }
1262
1263 return null;
1264 }
1265
1266 /**
1267 * Gets the text items that are in the same column and below a specified
1268 * item. Frame title and name are excluded from the column list.
1269 *
1270 * @param from
1271 * The Item to get the column for.
1272 */
1273 public List<Text> getColumn(Item from)
1274 {
1275 // Check that this item is on the current frame
1276 if (!_body.contains(from)) return null;
1277
1278 if (from == null) {
1279 from = getLastNonAnnotationTextItem();
1280 }
1281
1282 if (from == null) return null;
1283
1284 // Get the enclosedItems
1285 Collection<Text> enclosed = FrameUtils.getCurrentTextItems();
1286 List<Text> toCheck = null;
1287
1288 if (enclosed.contains(from)) {
1289 toCheck = new ArrayList<Text>();
1290 toCheck.addAll(enclosed);
1291 } else {
1292 toCheck = getBodyTextItems(true);
1293 }
1294
1295 List<Text> column = new ArrayList<Text>();
1296
1297 if (toCheck.size() > 0) {
1298 // Make sure the items are sorted
1299 Collections.sort(toCheck);
1300
1301 // Create a list of items consisting of the item 'from' and all the
1302 // items below it which are also in the same column as it
1303 int index = toCheck.indexOf(from);
1304
1305 // If its the title index will be 0
1306 if (index < 0) index = 0;
1307
1308 for (int i = index; i < toCheck.size(); i++) {
1309 Text item = toCheck.get(i);
1310 if (FrameUtils.inSameColumn(from, item)) column.add(item);
1311 }
1312 }
1313
1314 return column;
1315 }
1316
1317 /**
1318 * Adds the given Vector to the list of vector Frames being drawn with this
1319 * Frame.
1320 *
1321 * @param vector
1322 * The Vector to add
1323 *
1324 * @throws NullPointerException
1325 * If overlay is null.
1326 */
1327 protected boolean addVector(Vector toAdd)
1328 {
1329 // make sure we dont add this frame as an overlay of itself
1330 if (toAdd.Frame == this) return false;
1331
1332 _vectors.add(toAdd);
1333
1334 // Items must be notified that they have been added or removed from this
1335 // frame via the vector...
1336 int maxX = 0;
1337 int maxY = 0;
1338
1339 HighlightMode mode = toAdd.Source.getHighlightMode();
1340 if (mode != HighlightMode.None) mode = HighlightMode.Connected;
1341
1342 Colour highlightColor = toAdd.Source.getHighlightColor();
1343
1344 for (Item i : ItemUtils.CopyItems(toAdd.Frame.getVectorItems(), toAdd)) {
1345 i.onParentStateChanged(new ItemParentStateChangedEvent(this, ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY, toAdd.permission));
1346 i.setEditTarget(toAdd.Source);
1347 i.setHighlightModeAndColour(mode, highlightColor);
1348 _vectorItems.add(i);
1349 i.invalidateAll();
1350 i.invalidateFill();
1351
1352 // Get the right most x and bottom most y pos
1353 int itemRight = i.getX() + i.getBoundsWidth();
1354 if (itemRight > maxX) maxX = itemRight;
1355
1356 int itemBottom = i.getY() + i.getBoundsHeight();
1357 if (itemBottom > maxY) maxY = itemBottom;
1358 }
1359
1360 toAdd.setSize(maxX, maxY);
1361
1362 return true;
1363 }
1364
1365 public Collection<Vector> getVectors()
1366 {
1367 Collection<Vector> l = new LinkedList<Vector>();
1368 l.addAll(_vectors);
1369 return l;
1370 }
1371
1372 public Collection<Overlay> getOverlays()
1373 {
1374 return new LinkedList<Overlay>(_overlays.keySet());
1375 }
1376
1377 /**
1378 * @return All vectors seen by this frame (including its vector's vectors).
1379 */
1380 public List<Vector> getVectorsDeep()
1381 {
1382 List<Vector> l = new LinkedList<Vector>();
1383 getVectorsDeep(l, this, new LinkedList<Frame>());
1384 return l;
1385 }
1386
1387 private boolean getVectorsDeep(List<Vector> vectors, Frame vector, List<Frame> seenVectors)
1388 {
1389 if (seenVectors.contains(vector)) return false;
1390
1391 seenVectors.add(vector);
1392
1393 for (Vector v : vector.getVectors()) {
1394 if (getVectorsDeep(vectors, v.Frame, seenVectors)) {
1395 vectors.add(v);
1396 }
1397 }
1398
1399 return true;
1400 }
1401
1402 public List<Overlay> getOverlaysDeep()
1403 {
1404 List<Overlay> ret = new LinkedList<Overlay>();
1405
1406 getOverlaysDeep(ret, new LinkedList<Frame>());
1407
1408 return ret;
1409 }
1410
1411 private boolean getOverlaysDeep(List<Overlay> overlays, List<Frame> seenOverlays)
1412 {
1413 if (seenOverlays.contains(this)) return false;
1414
1415 seenOverlays.add(this);
1416
1417 for (Overlay o : this.getOverlays()) {
1418 if (o.Frame.getOverlaysDeep(overlays, seenOverlays)) {
1419 overlays.add(o);
1420 }
1421 }
1422 return true;
1423 }
1424
1425 /**
1426 * Recursive function similar to AddAllOverlayItems.
1427 *
1428 * @param widgets
1429 * The collection the widgets will be added to
1430 * @param overlay
1431 * An "overlay" frame - this initially will be the parent frame
1432 * @param seenOverlays
1433 * Used for state in the recursion stack. Pass as an empty
1434 * (non-null) list.
1435 */
1436 public List<Widget> getAllOverlayWidgets()
1437 {
1438 List<Widget> widgets = new LinkedList<Widget>();
1439
1440 for (Overlay o : getOverlaysDeep()) widgets.addAll(o.Frame.getInteractiveWidgets());
1441
1442 return widgets;
1443 }
1444
1445 /**
1446 * Gets the overlay on this frame which owns the given item.
1447 *
1448 * @param item
1449 * The item - must not be null.
1450 * @return The overlay that contains the item. Null if no overlay owns the
1451 * item.
1452 */
1453 public Overlay getOverlayOwner(Item item)
1454 {
1455 if (item == null) throw new NullPointerException("item");
1456
1457 for (Overlay l : getOverlays()) {
1458 if (item.getParent() == l.Frame) return l;
1459 }
1460
1461 // TODO return the correct vector... not just the first vector matching
1462 // the vector frame
1463 for (Vector v : getVectors()) {
1464 if (item.getParent() == v.Frame) return v;
1465 }
1466
1467 return null;
1468 }
1469
1470 public void clearVectors()
1471 {
1472 _vectors.clear();
1473
1474 for (Item i : _vectorItems) { // TODO: Rethink where this should live
1475 i.invalidateAll();
1476 i.invalidateFill();
1477 }
1478 _vectorItems.clear();
1479
1480 }
1481
1482 protected boolean removeVector(Vector toRemove)
1483 {
1484 if (!_vectors.remove(toRemove)) return false;
1485
1486 for (Item i : toRemove.Frame.getVectorItems()) {
1487 i.invalidateAll();
1488 i.invalidateFill();
1489 _vectorItems.remove(i);
1490 i.onParentStateChanged(new ItemParentStateChangedEvent(this,
1491 ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY,
1492 toRemove.permission));
1493
1494 }
1495
1496 return true;
1497 }
1498
1499 public void clearOverlays()
1500 {
1501 for (Overlay o : _overlays.keySet()) {
1502 for (Item i : o.Frame.getItems()) {
1503 i.onParentStateChanged(new ItemParentStateChangedEvent(
1504 this,
1505 ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY,
1506 o.permission));
1507 }
1508 }
1509 _overlayItems.clear();
1510 _overlays.clear();
1511 assert (_overlays.isEmpty());
1512 }
1513
1514 protected boolean removeOverlay(Frame f)
1515 {
1516 for (Overlay o : _overlays.keySet()) {
1517 if (o.Frame == f) {
1518 _overlays.remove(o);
1519
1520 for (Item i : f.getItems()) {
1521 _overlayItems.remove(i);
1522 i.onParentStateChanged(new ItemParentStateChangedEvent(
1523 this,
1524 ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY,
1525 o.permission));
1526 }
1527
1528 return true;
1529 }
1530 }
1531
1532 return false;
1533 }
1534
1535 public void addAllVectors(List<Vector> vectors)
1536 {
1537 for (Vector v : vectors) {
1538 addVector(v);
1539 }
1540 }
1541
1542 public void addAllOverlays(Collection<Overlay> overlays)
1543 {
1544 for (Overlay o : overlays) {
1545 addOverlay(o);
1546 }
1547 }
1548
1549 protected boolean addOverlay(Overlay toAdd)
1550 {
1551 // make sure we dont add this frame as an overlay of itself
1552 if (toAdd.Frame == this) return false;
1553
1554 // Dont add the overlay if there is already one for this frame
1555 if (_overlays.values().contains(toAdd.Frame)) return false;
1556
1557 // Add the overlay to the map of overlays on this frame
1558 _overlays.put(toAdd, toAdd.Frame);
1559
1560 // Add all the overlays from the overlay frame to this frame
1561 // TODO: Can this cause a recursion loop? If A and B are overlays of each other? cts16
1562 for (Overlay o : toAdd.Frame.getOverlays()) addOverlay(o);
1563
1564 // Add all the vectors from the overlay frame to this frame
1565 for (Vector v : toAdd.Frame.getVectors()) addVector(v);
1566
1567 // Now add the items for this overlay
1568 UserAppliedPermission permission = UserAppliedPermission.min(toAdd.Frame.getUserAppliedPermission(), toAdd.permission);
1569
1570 // Items must be notified that they have been added or removed from this
1571 // frame via the overlay...
1572 for (Item i : toAdd.Frame.getVisibleItems()) {
1573 i.onParentStateChanged(new ItemParentStateChangedEvent(this, ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY, permission));
1574 _overlayItems.add(i);
1575 }
1576
1577 return true;
1578 }
1579
1580 @Override
1581 public boolean equals(Object o)
1582 {
1583 if (o instanceof String) {
1584 return (String.CASE_INSENSITIVE_ORDER.compare((String) o, getName()) == 0);
1585 }
1586
1587 if (o instanceof Frame) {
1588 return getName().equals(((Frame) o).getName());
1589 }
1590
1591 return super.equals(o);
1592 }
1593
1594 /**
1595 * Merge one frames contents into another.
1596 *
1597 * @param toMergeWith
1598 */
1599 private void merge(Frame toMergeWith)
1600 {
1601 if (toMergeWith == null) return;
1602
1603 List<Item> copies = ItemUtils.CopyItems(toMergeWith.getItems());
1604 copies.remove(toMergeWith.getNameItem());
1605
1606 for (Item i : copies) {
1607 if (i.getID() >= 0) {
1608 i.setID(this.getNextItemID());
1609 addItem(i);
1610 }
1611 }
1612 }
1613
1614 /**
1615 * This method is for merging frames or setting frame attributes via
1616 * injecting a text item into the frameName item.
1617 *
1618 * @param toMerge
1619 * @return the items that cant be merged
1620 */
1621 public List<Item> merge(List<Item> toMerge)
1622 {
1623 ArrayList<Item> remain = new ArrayList<Item>(0);
1624
1625 for (Item i : toMerge) {
1626 if (!(i instanceof Text)) {
1627 remain.add(i);
1628 } else {
1629 if (!AttributeUtils.setAttribute(this, (Text) i)) {
1630 if (i.getLink() != null) {
1631 merge(FrameIO.LoadFrame(i.getAbsoluteLink()));
1632 } else if (FrameIO.isValidFrameName(((Text) i).getFirstLine())) {
1633 // If we get hear we are merging frames
1634 merge(FrameIO.LoadFrame(((Text) i).getFirstLine()));
1635 }
1636 }
1637 }
1638 }
1639
1640 return remain;
1641 }
1642
1643 /**
1644 * Removes all non-title non-annotation items from this Frame. All removed
1645 * items are added to the backup-stack.
1646 */
1647 public void clear(boolean keepAnnotations)
1648 {
1649 List<Item> newBody = new ArrayList<Item>(0);
1650
1651 Item title = getTitleItem();
1652
1653 if (title != null) {
1654 newBody.add(title);
1655 _body.remove(title);
1656 }
1657
1658 if (keepAnnotations) {
1659 for (Item i : _body) {
1660 if (i.isAnnotation()) newBody.add(i);
1661 }
1662 }
1663
1664 _body.removeAll(newBody);
1665 addToUndoDelete(_body);
1666 _body = newBody;
1667 change();
1668
1669 if (!keepAnnotations && _annotations != null) _annotations.clear();
1670 }
1671
1672 /**
1673 * Creates a new text item with the given text.
1674 *
1675 * @param text
1676 * @return
1677 */
1678 public Text createNewText(String text)
1679 {
1680 Text t = createBlankText(text);
1681 t.setText(text);
1682 return t;
1683 }
1684
1685 /**
1686 * Creates a new Text Item with no text. The newly created Item is a copy
1687 * the ItemTemplate if one is present, and inherits all the attributes of
1688 * the Template
1689 *
1690 * @return The newly created Text Item
1691 */
1692 public Text createBlankText(String templateType)
1693 {
1694 SessionStats.CreatedText();
1695 Text t;
1696
1697 if (templateType.length() == 0) {
1698 t = getItemTemplate().copy();
1699 } else {
1700 t = getItemTemplate(templateType.charAt(0));
1701 }
1702
1703 // reset attributes
1704 t.setID(getNextItemID());
1705 t.setPosition(DisplayController.getMousePosition());
1706 t.setText("");
1707 t.setParent(this);
1708
1709 // Set the width if the template doesnt have a width
1710 // Make it the width of the page
1711 // t.setMaxWidth(FrameGraphics.getMaxFrameSize().width);
1712 // if (t.getWidth() <= 0) {
1713 // String maxWidthString = getAnnotationValue("maxwidth");
1714 // int width = FrameGraphics.getMaxFrameSize().width;
1715 // if (maxWidthString != null) {
1716 // try {
1717 // width = Math.min(width, Integer.parseInt(maxWidthString));
1718 // } catch (NumberFormatException nfe) {
1719 // }
1720 // }
1721 //
1722 // t.setRightMargin(width);
1723 // }
1724 addItem(t);
1725 return t;
1726 }
1727
1728 public Item createDot()
1729 {
1730 Item dot = new Dot(DisplayController.getMouseX(), DisplayController.getMouseY(), getNextItemID());
1731
1732 Item template = getTemplate(_dotTemplate, ItemUtils.TAG_DOT_TEMPLATE);
1733 float thickness = template.getThickness();
1734 if (thickness > 0) dot.setThickness(template.getThickness());
1735 if (template.getLinePattern() != null) dot.setLinePattern(template.getLinePattern());
1736 dot.setColor(template.getColor());
1737 dot.setFillColor(template.getFillColor());
1738 // reset attributes
1739 dot.setParent(this);
1740 return dot;
1741 }
1742
1743 private Text getTemplate(Text defaultTemplate, int templateTag)
1744 {
1745 Text t = null;
1746
1747 // check for an updated template...
1748 for (Item i : this.getItems()) {
1749 if (ItemUtils.startsWithTag(i, templateTag)) {
1750 t = (Text) i;
1751 break;
1752 }
1753 }
1754
1755 if (t == null) {
1756 if (defaultTemplate == null) return null;
1757
1758 t = defaultTemplate;
1759 }
1760
1761 // If the item is linked apply any attribute pairs on the child frame
1762 String link = t.getAbsoluteLink();
1763
1764 // need to get link first because copy doesnt copy the link
1765 t = t.copy();
1766 t.setTooltip(null);
1767 if (link != null) {
1768 t.setLink(null);
1769 Frame childFrame = FrameIO.LoadFrame(link);
1770 if (childFrame != null) {
1771 // read in attribute value pairs
1772 for (Text attribute : childFrame.getBodyTextItems(false)) {
1773 AttributeUtils.setAttribute(t, attribute);
1774 }
1775 }
1776 }
1777 return t;
1778 }
1779
1780 /**
1781 * TODO: Comment. cts16
1782 * TODO: Remove magic constants. cts16
1783 */
1784 public Text getItemTemplate(char firstChar)
1785 {
1786 switch (firstChar) {
1787 case '@':
1788 return getAnnotationTemplate();
1789 case '/':
1790 case '#':
1791 return getCodeCommentTemplate();
1792 default:
1793 return getItemTemplate();
1794 }
1795 }
1796
1797 public Text createNewText()
1798 {
1799 return createNewText("");
1800 }
1801
1802 public Text addText(int x, int y, String text, String action)
1803 {
1804 Text t = createNewText(text);
1805 t.setPosition(x, y);
1806 t.addAction(action);
1807 return t;
1808 }
1809
1810 public Text addText(int x, int y, String text, String action, String link)
1811 {
1812 Text t = addText(x, y, text, action);
1813 t.setLink(link);
1814 return t;
1815 }
1816
1817 public Dot addDot(int x, int y)
1818 {
1819 Dot d = new Dot(x, y, getNextItemID());
1820 addItem(d);
1821 return d;
1822 }
1823
1824 /**
1825 * Adds a rectangle to the frame
1826 *
1827 * @param x
1828 * X coordinate of the top-left corner of the rectangle
1829 * @param y
1830 * Y coordinate of the top-left corner of the rectangle
1831 * @param width
1832 * Width of the rectangle
1833 * @param height
1834 * Height of the rectangle
1835 * @param borderThickness
1836 * Thickness, in pixels, of the rectangle's border/outline
1837 * @param borderColor
1838 * Color of the rectangle's border/outline
1839 * @param fillColor
1840 * Color to fill the rectangle with
1841 */
1842 public List<Item> addRectangle(int x, int y, int width, int height, float borderThickness, Colour borderColor, Colour fillColor)
1843 {
1844 List<Item> rectComponents = new ArrayList<Item>();
1845 Item[] corners = new Item[4];
1846
1847 // Top Left
1848 corners[0] = this.createDot();
1849 corners[0].setPosition(x, y);
1850
1851 // Top Right
1852 corners[1] = this.createDot();
1853 corners[1].setPosition(x + width, y);
1854
1855 // Bottom Right
1856 corners[2] = this.createDot();
1857 corners[2].setPosition(x + width, y + height);
1858
1859 // Bottom Left
1860 corners[3] = this.createDot();
1861 corners[3].setPosition(x, y + height);
1862
1863 // Add corners to the collection and setting their attributes
1864 for (int i = 0; i < corners.length; i++) {
1865 corners[i].setThickness(borderThickness);
1866 corners[i].setColor(borderColor);
1867 corners[i].setFillColor(fillColor);
1868 rectComponents.add(corners[i]);
1869 }
1870
1871 // create lines between the corners
1872 rectComponents.add(new Line(corners[0], corners[1], this.getNextItemID()));
1873 rectComponents.add(new Line(corners[1], corners[2], this.getNextItemID()));
1874 rectComponents.add(new Line(corners[2], corners[3], this.getNextItemID()));
1875 rectComponents.add(new Line(corners[3], corners[0], this.getNextItemID()));
1876
1877 // Add constraints between each corner
1878 new Constraint(corners[0], corners[1], this.getNextItemID(), Constraint.HORIZONTAL);
1879 new Constraint(corners[2], corners[3], this.getNextItemID(), Constraint.HORIZONTAL);
1880 new Constraint(corners[1], corners[2], this.getNextItemID(), Constraint.VERTICAL);
1881 new Constraint(corners[3], corners[0], this.getNextItemID(), Constraint.VERTICAL);
1882
1883 List<Item> rect = new ArrayList<Item>(rectComponents);
1884 this.addAllItems(rectComponents);
1885 StandardGestureActions.anchor(rectComponents);
1886 return rect;
1887 }
1888
1889 public boolean isSaved()
1890 {
1891 return _saved;
1892 }
1893
1894 public void setSaved()
1895 {
1896 _saved = true;
1897 _change = false;
1898 }
1899
1900 public static boolean rubberbandingLine()
1901 {
1902 return FreeItems.getInstance().size() == 2 &&
1903 (FreeItems.getInstance().get(0) instanceof Line || FreeItems.getInstance().get(1) instanceof Line);
1904 }
1905
1906 /**
1907 * Tests if an item is a non title, non frame name, non special annotation
1908 * text item.
1909 *
1910 * @param it
1911 * the item to be tested
1912 * @return true if the item is a normal text item
1913 */
1914 public boolean isNormalTextItem(Item it)
1915 {
1916 if (it instanceof Text && it != getTitleItem() && it != _frameName && !((Text) it).isSpecialAnnotation()) {
1917 return true;
1918 }
1919
1920 return false;
1921 }
1922
1923 /**
1924 * Moves the mouse to the end of the text item with a specified index.
1925 *
1926 * @param index
1927 */
1928 public boolean moveMouseToTextItem(int index)
1929 {
1930 List<Item> items = getItems();
1931 int itemsFound = 0;
1932 for (int i = 0; i < items.size(); i++) {
1933 Item it = items.get(i);
1934 if (isNormalTextItem(it)) itemsFound++;
1935 if (itemsFound > index) {
1936 DisplayController.setCursorPosition(((Text) it).getParagraphEndPosition().x, it.getY());
1937 DisplayController.resetCursorOffset();
1938 DisplayController.requestRefresh(true);
1939 return true;
1940 }
1941 }
1942
1943 return false;
1944 }
1945
1946 /**
1947 * Searches for an annotation item called start to be used as the default
1948 * cursor location when TDFC occurs.
1949 *
1950 * TODO: Remove magic constants. cts16
1951 */
1952 public boolean moveMouseToDefaultLocation()
1953 {
1954 List<Item> items = getItems();
1955
1956 for (Item it : items) {
1957 if (it instanceof Text) {
1958 Text t = (Text) it;
1959 if (t.getText().toLowerCase().startsWith("@start") || t.getText().toLowerCase().equals("@start:")) {
1960 // Used to allow users the option of putting an initial
1961 // bullet after the @start
1962 // This was replaced by width
1963 // t.stripFirstWord();
1964 t.setText("");
1965
1966 if (t.getText().equals("")) DisplayController.getCurrentFrame().removeItem(t);
1967
1968 if (!FreeItems.hasItemsAttachedToCursor()) {
1969 DisplayController.setCursorPosition(((Text) it).getParagraphEndPosition());
1970 DisplayController.resetCursorOffset();
1971 }
1972
1973 DisplayController.requestRefresh(true);
1974
1975 return true;
1976 }
1977 }
1978 }
1979
1980 return false;
1981 }
1982
1983 /**
1984 * Gets the file name that actions should use to export files created by
1985 * running actions from this frame.
1986 *
1987 * @return the fileName if the frame contains an '@file' tag. Returns the
1988 * name of the frame if the tag isnt on the frame.
1989 */
1990 public String getExportFileName()
1991 {
1992 String fileName = getExportFileTagValue();
1993
1994 if (fileName == null) {
1995 fileName = getTitle();
1996
1997 if (fileName == null) {
1998 fileName = getName();
1999 }
2000 }
2001
2002 return fileName;
2003 }
2004
2005 public void toggleBackgroundColor()
2006 {
2007 setBackgroundColor(ColorUtils.getNextColor(_background, TemplateSettings.BackgroundColorWheel.get(), null));
2008 }
2009
2010 public void setName(String frameset, int i)
2011 {
2012 setFrameset(frameset);
2013 setFrameNumber(i);
2014 }
2015
2016 /**
2017 * Sets the item permissions to match the protection for the frame.
2018 * No longer sets item permissions, since items can have their own permissions now (but still default to frame permissions)
2019 *
2020 */
2021 public void refreshItemPermissions(UserAppliedPermission maxPermission)
2022 {
2023 if(_frameName == null) return;
2024
2025 UserAppliedPermission permission = UserAppliedPermission.min(maxPermission, getUserAppliedPermission());
2026
2027 switch (permission) {
2028 case none:
2029 _frameName.setBackgroundColor(FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_NONE);
2030 break;
2031 case followLinks:
2032 _frameName.setBackgroundColor(FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_FOLLOW_LINKS);
2033 break;
2034 case copy:
2035 _frameName.setBackgroundColor(FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_COPY);
2036 break;
2037 case createFrames:
2038 _frameName.setBackgroundColor(FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_CREATE_FRAMES);
2039 break;
2040 case full:
2041 _frameName.setBackgroundColor(FRAME_NAME_BACKGROUND_COLOUR_FOR_PERMISSION_FULL);
2042 break;
2043 default:
2044 assert (false);
2045 break;
2046 }
2047
2048 for (Overlay o : getOverlays()) {
2049 for(Item i : o.Frame._body) {
2050 i.setOverlayPermission(o.permission);
2051 }
2052 o.Frame.refreshItemPermissions(o.permission);
2053 }
2054 }
2055
2056 public boolean isTestFrame()
2057 {
2058 Text title = getTitleItem();
2059 if (title == null) return false;
2060 String action = title.getFirstAction();
2061 if (action == null) return false;
2062 action = action.toLowerCase();
2063 return action.startsWith(Simple.RUN_FRAME_ACTION) || action.startsWith(Simple.DEBUG_FRAME_ACTION);
2064 }
2065
2066 public void setActiveTime(String activeTime)
2067 {
2068 try {
2069 _activeTime = new Time(Time.valueOf(activeTime).getTime() + 12 * 60 * 60 * 1000);
2070 } catch (Exception e) {
2071 _activeTime = new Time(0);
2072 }
2073 }
2074
2075 public void setActiveTime(Time activeTime)
2076 {
2077 _activeTime = activeTime;
2078 }
2079
2080 public void setDarkTime(Time darkTime)
2081 {
2082 _darkTime = darkTime;
2083 }
2084
2085 public void setDarkTime(String darkTime)
2086 {
2087 try {
2088 _darkTime = new Time(Time.valueOf(darkTime).getTime() + 12 * 60 * 60 * 1000);
2089 } catch (Exception e) {
2090 _darkTime = new Time(0);
2091 }
2092 }
2093
2094 /**
2095 * Returns null if their is no backup frame or if it is invalid.
2096 *
2097 * @return the backup frame for this frame
2098 */
2099 public Frame getBackupFrame()
2100 {
2101 Text backupTag = _annotations.get("old");
2102 if (backupTag == null) return null;
2103
2104 // TODO want another way to deal with updating of annotations items
2105 // without F12 refresh
2106 // Reparse the frame if annotation item has been modified
2107 String[] processedText = backupTag.getProcessedText();
2108 if (processedText == null) {
2109 // Reparse the frame if this item has not yet been parsed
2110 FrameUtils.Parse(this);
2111 return getBackupFrame();
2112 }
2113
2114 // Now return the name of the backed up frame
2115 String link = backupTag.getAbsoluteLink();
2116 if (link == null || link.equalsIgnoreCase(getName())) return null;
2117
2118 Frame backup = FrameIO.LoadFrame(link);
2119 return backup;
2120 }
2121
2122 public Time getDarkTime()
2123 {
2124 return _darkTime;
2125 }
2126
2127 public Time getActiveTime()
2128 {
2129 return _activeTime;
2130 }
2131
2132 /**
2133 * Gets the number of backed up versions of this frame are saved plus 1 for
2134 * this frame.
2135 *
2136 * @return the number of frames in the backed up comet
2137 */
2138 public int getCometLength()
2139 {
2140 Frame backup = getBackupFrame();
2141 return 1 + (backup == null ? 0 : backup.getCometLength());
2142 }
2143
2144 public void addAnnotation(Text item)
2145 {
2146 if (_annotations == null) {
2147 _annotations = new HashMap<String, Text>();
2148 }
2149
2150 // Check if this item has already been processed
2151 String[] tokens = item.getProcessedText();
2152 if (tokens != null) {
2153 if (tokens.length > 0) {
2154 _annotations.put(tokens[0], item);
2155 }
2156 return;
2157 }
2158
2159 String text = item.getText().trim();
2160 assert (text.charAt(0) == '@');
2161
2162 // Ignore annotations with spaces after the tag symbol
2163 if (text.length() < 2 || !Character.isLetter(text.charAt(1))) {
2164 item.setProcessedText(new String[0]);
2165 return;
2166 }
2167
2168 // The separator char must come before the first non letter otherwise we
2169 // ignore the annotation item
2170 for (int i = 2; i < text.length(); i++) {
2171 char ch = text.charAt(i);
2172 if (!Character.isLetterOrDigit(ch)) {
2173 // Must have an attribute value pair
2174 if (ch == AttributeValuePair.SEPARATOR_CHAR) {
2175 // Get the attribute
2176 String attribute = text.substring(1, i).toLowerCase();
2177 String value = "";
2178 if (text.length() > 1 + i) {
2179 value = text.substring(i + 1).trim();
2180 }
2181 item.setProcessedText(new String[] { attribute, value });
2182 _annotations.put(attribute, item);
2183 return;
2184 } else {
2185 item.setProcessedText(new String[0]);
2186 return;
2187 }
2188 }
2189 }
2190
2191 // If it was nothing but letters and digits save the tag
2192 String lowerCaseText = text.substring(1).toLowerCase();
2193 item.setProcessedText(new String[] { lowerCaseText });
2194 _annotations.put(lowerCaseText, item);
2195 }
2196
2197 public boolean hasAnnotation(String annotation)
2198 {
2199 if (_annotations == null) refreshAnnotationList();
2200
2201 return _annotations.containsKey(annotation.toLowerCase());
2202 }
2203
2204 /**
2205 * Returns the annotation value in full case.
2206 *
2207 * @param annotation
2208 * the annotation to retrieve the value of.
2209 * @return the annotation item value in full case or null if the annotation
2210 * is not on the frame or has no value.
2211 */
2212 public String getAnnotationValue(String annotation)
2213 {
2214 if (_annotations == null) refreshAnnotationList();
2215
2216 Text text = _annotations.get(annotation.toLowerCase());
2217 if (text == null) return null;
2218
2219 String[] tokens = text.getProcessedText();
2220
2221 if (tokens != null && tokens.length > 1) return tokens[1];
2222
2223 return null;
2224 }
2225
2226 public void clearAnnotations()
2227 {
2228 _annotations = null;
2229 }
2230
2231 public List<Item> getVisibleItems()
2232 {
2233 return getItems(true);
2234 }
2235
2236 private void refreshAnnotationList()
2237 {
2238 if (_annotations == null) {
2239 _annotations = new HashMap<String, Text>();
2240 } else {
2241 _annotations.clear();
2242 }
2243
2244 for (Text text : getTextItems()) {
2245 if (text.isAnnotation()) {
2246 addAnnotation(text);
2247 }
2248 }
2249 }
2250
2251 public Collection<Text> getAnnotationItems()
2252 {
2253 if (_annotations == null) {
2254 refreshAnnotationList();
2255 }
2256
2257 return _annotations.values();
2258 }
2259
2260 /**
2261 * Gets a list of items to be saved to file by text file writers.
2262 *
2263 * @return the list of items to be saved to a text file
2264 */
2265 public List<Item> getItemsToSave()
2266 {
2267 if (!_sorted) {
2268 Collections.sort(_body);
2269 _sorted = true;
2270 }
2271
2272 // iWidgets are handled specially since 8 items are written as one
2273 Collection<Widget> seenWidgets = new LinkedHashSet<Widget>();
2274
2275 List<Item> toSave = new ArrayList<Item>();
2276
2277 for (Item i : _body) {
2278 if (i == null || i.dontSave()) continue;
2279
2280 // Ensure only one of the WidgetCorners represent a single widget
2281 if (i instanceof WidgetCorner) {
2282 Widget iw = ((WidgetCorner) i).getWidgetSource();
2283 if (seenWidgets.contains(iw)) continue;
2284 seenWidgets.add(iw);
2285 toSave.add(iw.getSource());
2286 } else if (i instanceof XRayable) {
2287 XRayable x = (XRayable) i;
2288 toSave.addAll(x.getItemsToSave());
2289 // Circle centers are items with attached enclosures
2290 } else if (i.hasEnclosures()) {
2291 continue;
2292 } else {
2293 toSave.add(i);
2294 }
2295 }
2296
2297 for (Vector v : getVectors()) {
2298 toSave.add(v.Source);
2299 }
2300
2301 return toSave;
2302 }
2303
2304 public Collection<Item> getOverlayItems()
2305 {
2306 return _overlayItems;
2307 }
2308
2309 /**
2310 * Returns true if this frame has and overlays for the specified frame.
2311 *
2312 * @param frame
2313 * @return
2314 */
2315 public boolean hasOverlay(Frame frame)
2316 {
2317 return _overlays.containsValue(frame);
2318 }
2319
2320 public Collection<Item> getAllItems()
2321 {
2322 Collection<Item> allItems = new LinkedHashSet<Item>(_body);
2323 allItems.addAll(_overlayItems);
2324 allItems.addAll(_vectorItems);
2325 return allItems;
2326 }
2327
2328 public Collection<Item> getVectorItems()
2329 {
2330 Collection<Item> vectorItems = new LinkedHashSet<Item>(_vectorItems);
2331 vectorItems.addAll(getNonAnnotationItems(false));
2332 return vectorItems;
2333 }
2334
2335 /**
2336 * Gets a list of all the text items on the frame.
2337 *
2338 * @return
2339 */
2340 public Collection<Text> getTextItems()
2341 {
2342 Collection<Text> textItems = new ArrayList<Text>();
2343
2344 for (Item i : getItems(true)) {
2345 // only add up normal body text items
2346 if ((i instanceof Text)) {
2347 textItems.add((Text) i);
2348 }
2349 }
2350
2351 return textItems;
2352 }
2353
2354 public Text getAnnotation(String annotation)
2355 {
2356 if (_annotations == null) refreshAnnotationList();
2357
2358 return _annotations.get(annotation.toLowerCase());
2359 }
2360
2361 public void recalculate()
2362 {
2363 for (Item i : getItems()) {
2364 if (i.hasFormula() && !i.isAnnotation()) {
2365 i.calculate(i.getFormula());
2366 }
2367 }
2368 }
2369
2370 public void removeObserver(FrameObserver observer)
2371 {
2372 _observers.remove(observer);
2373 }
2374
2375 public void addObserver(FrameObserver observer)
2376 {
2377 _observers.add(observer);
2378 }
2379
2380 public void clearObservers()
2381 {
2382 for (FrameObserver fl : _observers) {
2383 fl.removeSubject(this);
2384 }
2385
2386 // The frame listener will call the frames removeListener method
2387 assert (_observers.size() == 0);
2388 }
2389
2390 public Collection<Text> getNonAnnotationText(boolean removeTitle)
2391 {
2392 Collection<Text> items = new LinkedHashSet<Text>();
2393
2394 for (Item i : getItems(true)) {
2395 // only add up normal body text items
2396 if (i instanceof Text && !i.isAnnotation()) {
2397 items.add((Text) i);
2398 }
2399 }
2400
2401 if (removeTitle) {
2402 items.remove(getTitleItem());
2403 }
2404
2405 return items;
2406 }
2407
2408 public void dispose()
2409 {
2410 clearObservers();
2411
2412 for (Item i : _body) {
2413 i.dispose();
2414 }
2415
2416 _frameName.dispose();
2417 _body = null;
2418 _frameName = null;
2419 }
2420
2421 public void parse()
2422 {
2423 for (Overlay o : getOverlays()) {
2424 o.Frame.parse();
2425 }
2426
2427 // Must parse the frame AFTER the overlays
2428 FrameUtils.Parse(this);
2429 }
2430
2431 public void setPath(String path)
2432 {
2433 this.path = path;
2434 }
2435
2436 public String getPath()
2437 {
2438 return path;
2439 }
2440
2441 public void setLocal(boolean isLocal)
2442 {
2443 this._isLocal = isLocal;
2444 }
2445
2446 public boolean isLocal()
2447 {
2448 return _isLocal;
2449 }
2450
2451 public String getExportFileTagValue()
2452 {
2453 return getAnnotationValue("file");
2454 }
2455
2456 public void assertEquals(Frame frame2)
2457 {
2458 // Check that all the items on the frame are the same
2459 List<Item> items1 = getVisibleItems();
2460 List<Item> items2 = frame2.getVisibleItems();
2461
2462 if (items1.size() != items2.size()) {
2463 throw new UnitTestFailedException(items1.size() + " items", items2.size() + " items");
2464 } else {
2465 for (int i = 0; i < items1.size(); i++) {
2466 Item i1 = items1.get(i);
2467 Item i2 = items2.get(i);
2468 String s1 = i1.getText();
2469 String s2 = i2.getText();
2470 if (!s1.equals(s2)) {
2471 throw new UnitTestFailedException(s1, s2);
2472 }
2473 }
2474 }
2475 }
2476
2477 public boolean hasObservers()
2478 {
2479 return _observers != null && _observers.size() > 0;
2480 }
2481
2482 public Collection<? extends Item> getInteractableItems()
2483 {
2484 /*
2485 * TODO: Cache the interactableItems list so we dont have to recreate it
2486 * every time this method is called
2487 */
2488 if (_interactableItems.size() > 0) return _interactableItems;
2489
2490 for (Item i : _body) {
2491 if (i == null) continue;
2492 if (i.isVisible()) {
2493 _interactableItems.add(i);
2494 }
2495 }
2496
2497 for (Item i : _overlayItems) {
2498 if (i.hasPermission(UserAppliedPermission.followLinks)) {
2499 _interactableItems.add(i);
2500 }
2501 }
2502
2503 for (Item i : _vectorItems) {
2504 if (i.hasPermission(UserAppliedPermission.none)) {
2505 _interactableItems.add(i);
2506 }
2507 }
2508
2509 return _interactableItems;
2510 }
2511
2512 private static final class History {
2513
2514 public enum Type {
2515 deletion,
2516 movement
2517 }
2518
2519 public final List<Item> items;
2520
2521 public final Type type;
2522
2523 public History(Collection<Item> items, Type type)
2524 {
2525 this.items = new LinkedList<Item>(items);
2526 this.type = type;
2527 }
2528
2529 public String toString()
2530 {
2531 return this.type.toString() + ":\n" + this.items.toString();
2532 }
2533 }
2534}
Note: See TracBrowser for help on using the repository browser.