001: /* *************************************************************************
002:
003: Millstone(TM)
004: Open Sourced User Interface Library for
005: Internet Development with Java
006:
007: Millstone is a registered trademark of IT Mill Ltd
008: Copyright (C) 2000-2005 IT Mill Ltd
009:
010: *************************************************************************
011:
012: This library is free software; you can redistribute it and/or
013: modify it under the terms of the GNU Lesser General Public
014: license version 2.1 as published by the Free Software Foundation.
015:
016: This library is distributed in the hope that it will be useful,
017: but WITHOUT ANY WARRANTY; without even the implied warranty of
018: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
019: Lesser General Public License for more details.
020:
021: You should have received a copy of the GNU Lesser General Public
022: License along with this library; if not, write to the Free Software
023: Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
024:
025: *************************************************************************
026:
027: For more information, contact:
028:
029: IT Mill Ltd phone: +358 2 4802 7180
030: Ruukinkatu 2-4 fax: +358 2 4802 7181
031: 20540, Turku email: info@itmill.com
032: Finland company www: www.itmill.com
033:
034: Primary source for MillStone information and releases: www.millstone.org
035:
036: ********************************************************************** */
037:
038: package org.millstone.base.ui;
039:
040: import java.util.Collections;
041: import java.util.Iterator;
042: import java.util.HashMap;
043: import java.util.LinkedList;
044: import org.millstone.base.terminal.PaintTarget;
045: import org.millstone.base.terminal.PaintException;
046:
047: /** <p>A container that consists of components with certain coordinates on a
048: * grid. It also maintains cursor for adding component in left to right,
049: * top to bottom order.</p>
050: *
051: * <p>Each component in a <code>GridLayout</code> uses a certain
052: * {@link GridLayout.Area area} (x1,y1,x2,y2) from the grid. One should not
053: * add components that would overlap with the existing components because in
054: * such case an {@link OverlapsException} is thrown. Adding component with
055: * cursor automatically extends the grid by increasing the grid height.</p>
056: *
057: * @author IT Mill Ltd.
058: * @version 3.1.1
059: * @since 3.0
060: */
061: public class GridLayout extends AbstractComponentContainer implements
062: Layout {
063:
064: /** Initial grid x size */
065: private int width = 0;
066:
067: /** Initial grid y size */
068: private int height = 0;
069:
070: /** Cursor X position: this is where the next component with
071: * unspecified x,y is inserted
072: */
073: private int cursorX = 0;
074:
075: /** Cursor Y position: this is where the next component with
076: * unspecified x,y is inserted
077: */
078: private int cursorY = 0;
079:
080: /** Contains all items that are placed on the grid.
081: * These are components with grid area definition.
082: */
083: private LinkedList areas = new LinkedList();
084:
085: /** Mapping from components to threir respective areas. */
086: private LinkedList components = new LinkedList();
087:
088: /** Constructor for grid of given size.
089: * Note that grid's final size depends on the items that are added into the grid.
090: * Grid grows if you add components outside the grid's area.
091: * @param width Width of the grid.
092: * @param height Height of the grid.
093: */
094: public GridLayout(int width, int height) {
095: setWidth(width);
096: setHeight(height);
097: }
098:
099: /** Constructs an empty grid layout that is extended as needed. */
100: public GridLayout() {
101: this (1, 1);
102: }
103:
104: /** <p>Adds a component with a specified area to the grid. The area the
105: * new component should take is defined by specifying the upper left
106: * corner (x1, y1) and the lower right corner (x2, y2) of the area.</p>
107: *
108: * <p>If the new component overlaps with any of the existing components
109: * already present in the grid the operation will fail and an
110: * {@link OverlapsException} is thrown.</p>
111: *
112: * @param c The component to be added.
113: * @param x1 The X-coordinate of the upper left corner of the area
114: * <code>c</code> is supposed to occupy
115: * @param y1 The Y-coordinate of the upper left corner of the area
116: * <code>c</code> is supposed to occupy
117: * @param x2 The X-coordinate of the lower right corner of the area
118: * <code>c</code> is supposed to occupy
119: * @param y2 The Y-coordinate of the lower right corner of the area
120: * <code>c</code> is supposed to occupy
121: * @throws OverlapsException if the new component overlaps with any
122: * of the components already in the grid
123: * @throws OutOfBoundsException if the coordinates are outside of the
124: * grid area.
125: */
126: public void addComponent(Component component, int x1, int y1,
127: int x2, int y2) throws OverlapsException,
128: OutOfBoundsException {
129:
130: if (component == null)
131: throw new NullPointerException("Component must not be null");
132:
133: // Check that the component does not already exist in the container
134: if (components.contains(component))
135: throw new IllegalArgumentException(
136: "Component is already in the container");
137:
138: // Create area
139: Area area = new Area(component, x1, y1, x2, y2);
140:
141: // Check the validity of the coordinates
142: if (x2 < x1 || y2 < y2)
143: throw new IllegalArgumentException(
144: "Illegal coordinates for the component");
145: if (x1 < 0 || y1 < 0 || x2 >= width || y2 >= height)
146: throw new OutOfBoundsException(area);
147:
148: // Check that newItem does not overlap with existing items
149: checkExistingOverlaps(area);
150:
151: // Insert the component to right place at the list
152: // Respect top-down, left-right ordering
153: component.setParent(this );
154: Iterator i = areas.iterator();
155: int index = 0;
156: boolean done = false;
157: while (!done && i.hasNext()) {
158: Area existingArea = (Area) i.next();
159: if ((existingArea.y1 >= y1 && existingArea.x1 > x1)
160: || existingArea.y1 > y1) {
161: areas.add(index, area);
162: components.add(index, component);
163: done = true;
164: }
165: index++;
166: }
167: if (!done) {
168: areas.addLast(area);
169: components.addLast(component);
170: }
171:
172: super .addComponent(component);
173: requestRepaint();
174: }
175:
176: /** Tests if the given area overlaps with any of the items already on
177: * the grid.
178: *
179: * @param area Area to be checked for overlapping
180: * @throws OverlapsException if <code>area</code> overlaps with
181: * any existing area
182: */
183: private void checkExistingOverlaps(Area area)
184: throws OverlapsException {
185: for (Iterator i = areas.iterator(); i.hasNext();) {
186: Area existingArea = (Area) i.next();
187: if (existingArea.overlaps(area))
188:
189: // Component not added, overlaps with existing component
190: throw new OverlapsException(existingArea);
191: }
192: }
193:
194: /** Add component into this container to coordinates x1,y1 (NortWest corner of the area.)
195: * End coordinates (SouthEast corner of the area) are the same as x1,y1. Component width
196: * and height is 1.
197: * @param c The component to be added.
198: * @param x X-coordinate
199: * @param y Y-coordinate
200: */
201: public void addComponent(Component c, int x, int y) {
202: this .addComponent(c, x, y, x, y);
203: }
204:
205: /** Force the next component to be added to the beginning of the next line.
206: * By calling this function user can ensure that no more components are
207: * added to the right of the previous component.
208: *
209: * @see #space()
210: */
211: public void newLine() {
212: cursorX = 0;
213: cursorY++;
214: }
215:
216: /** Move cursor forwards by one. If the cursor goes out of the right grid border,
217: * move it to next line.
218: *
219: * @see #newLine()
220: */
221: public void space() {
222: cursorX++;
223: if (cursorX >= width) {
224: cursorX = 0;
225: cursorY++;
226: }
227: }
228:
229: /** Add a component into this container to the cursor position.
230: * If the cursor position is already occupied, the cursor is
231: * moved forwards to find free position. If the cursor goes out
232: * from the bottom of the grid, the grid is automaticly extended.
233: * @param c The component to be added.
234: */
235: public void addComponent(Component component) {
236:
237: // Find first available place from the grid
238: Area area;
239: boolean done = false;
240: while (!done)
241: try {
242: area = new Area(component, cursorX, cursorY, cursorX,
243: cursorY);
244: checkExistingOverlaps(area);
245: done = true;
246: } catch (OverlapsException ignored) {
247: space();
248: }
249:
250: // Extend the grid if needed
251: width = cursorX >= width ? cursorX + 1 : width;
252: height = cursorY >= height ? cursorY + 1 : height;
253:
254: addComponent(component, cursorX, cursorY);
255: }
256:
257: /** Removes the given component from this
258: * container.
259: *
260: * @param c The component to be removed.
261: */
262: public void removeComponent(Component component) {
263:
264: // Check that the component is contained in the container
265: if (component == null || !components.contains(component))
266: return;
267:
268: super .removeComponent(component);
269:
270: Area area = null;
271: for (Iterator i = areas.iterator(); area == null && i.hasNext();) {
272: Area a = (Area) i.next();
273: if (a.getComponent() == component)
274: area = a;
275: }
276:
277: components.remove(component);
278: if (area != null)
279: areas.remove(area);
280:
281: requestRepaint();
282: }
283:
284: /** Removes a component specified with it's top-left corner coordinates
285: * from this grid.
286: *
287: * @param x Component's top-left corner's X-coordinate
288: * @param y Component's top-left corner's Y-coordinate
289: */
290: public void removeComponent(int x, int y) {
291:
292: // Find area
293: for (Iterator i = areas.iterator(); i.hasNext();) {
294: Area area = (Area) i.next();
295: if (area.getX1() == x && area.getY1() == y) {
296: removeComponent(area.getComponent());
297: return;
298: }
299: }
300: }
301:
302: /** Gets an Iterator to the component container contents. Using the
303: * Iterator it's possible to step through the contents of the container.
304: *
305: * @return Iterator of the components inside the container.
306: */
307: public Iterator getComponentIterator() {
308: return Collections.unmodifiableCollection(components)
309: .iterator();
310: }
311:
312: /** Paints the contents of this component.
313: *
314: * @param event PaintEvent.
315: * @throws PaintException The paint operation failed.
316: */
317: public void paintContent(PaintTarget target) throws PaintException {
318:
319: target.addAttribute("h", height);
320: target.addAttribute("w", width);
321:
322: // Area iterator
323: Iterator areaiterator = areas.iterator();
324:
325: // Current item to be processed (fetch first item)
326: Area area = areaiterator.hasNext() ? (Area) areaiterator.next()
327: : null;
328:
329: // Collect rowspan related information here
330: HashMap cellUsed = new HashMap();
331:
332: // Empty cell collector
333: int emptyCells = 0;
334:
335: // Iterate every applicable row
336: for (int cury = 0; cury < height; cury++) {
337: target.startTag("gr");
338:
339: // Iterate every applicable column
340: for (int curx = 0; curx < width; curx++) {
341:
342: // Check if current item is located at curx,cury
343: if (area != null && (area.y1 == cury)
344: && (area.x1 == curx)) {
345:
346: // First check if empty cell needs to be rendered
347: if (emptyCells > 0) {
348: target.startTag("gc");
349: target.addAttribute("x", curx - emptyCells);
350: target.addAttribute("y", cury);
351: if (emptyCells > 1) {
352: target.addAttribute("w", emptyCells);
353: }
354: target.endTag("gc");
355: emptyCells = 0;
356: }
357:
358: // Now proceed rendering current item
359: int cols = (area.x2 - area.x1) + 1;
360: int rows = (area.y2 - area.y1) + 1;
361: target.startTag("gc");
362:
363: target.addAttribute("x", curx);
364: target.addAttribute("y", cury);
365:
366: if (cols > 1) {
367: target.addAttribute("w", cols);
368: }
369: if (rows > 1) {
370: target.addAttribute("h", rows);
371: }
372: area.getComponent().paint(target);
373:
374: target.endTag("gc");
375:
376: // Fetch next item
377: if (areaiterator.hasNext()) {
378: area = (Area) areaiterator.next();
379: } else {
380: area = null;
381: }
382:
383: // Update cellUsed if rowspan needed
384: if (rows > 1) {
385: int spannedx = curx;
386: for (int j = 1; j <= cols; j++) {
387: cellUsed.put(new Integer(spannedx),
388: new Integer(cury + rows - 1));
389: spannedx++;
390: }
391: }
392:
393: // Skip current item's spanned columns
394: if (cols > 1) {
395: curx += cols - 1;
396: }
397:
398: } else {
399:
400: // Check against cellUsed, render space or ignore cell
401: if (cellUsed.containsKey(new Integer(curx))) {
402:
403: // Current column contains already an item,
404: // check if rowspan affects at current x,y position
405: int rowspanDepth = ((Integer) cellUsed
406: .get(new Integer(curx))).intValue();
407:
408: if (rowspanDepth >= cury) {
409:
410: // ignore cell
411: // Check if empty cell needs to be rendered
412: if (emptyCells > 0) {
413: target.startTag("gc");
414: target.addAttribute("x", curx
415: - emptyCells);
416: target.addAttribute("y", cury);
417: if (emptyCells > 1) {
418: target
419: .addAttribute("w",
420: emptyCells);
421: }
422: target.endTag("gc");
423:
424: emptyCells = 0;
425: }
426: } else {
427:
428: // empty cell is needed
429: emptyCells++;
430:
431: // Remove cellUsed key as it has become obsolete
432: cellUsed.remove(new Integer(curx));
433: }
434: } else {
435:
436: // empty cell is needed
437: emptyCells++;
438: }
439: }
440:
441: } // iterate every column
442:
443: // Last column handled of current row
444:
445: // Check if empty cell needs to be rendered
446: if (emptyCells > 0) {
447: target.startTag("gc");
448: target.addAttribute("x", width - emptyCells);
449: target.addAttribute("y", cury);
450: if (emptyCells > 1) {
451: target.addAttribute("w", emptyCells);
452: }
453: target.endTag("gc");
454:
455: emptyCells = 0;
456: }
457:
458: target.endTag("gr");
459: } // iterate every row
460:
461: // Last row handled
462: }
463:
464: /** Gets the components UIDL tag.
465: *
466: * @return Component UIDL tag as string.
467: * @see org.millstone.base.ui.AbstractComponent#getTag()
468: */
469: public String getTag() {
470: return "gridlayout";
471: }
472:
473: /** This class defines an area on a grid. An Area is defined by the
474: * coordinates of its upper left corner (x1,y1) and lower right corner
475: * (x2,y2)
476: * @author IT Mill Ltd.
477: * @version 3.1.1
478: * @since 3.0
479: */
480: public class Area {
481:
482: /** X-coordinate of the upper left corner of the area */
483: private int x1;
484:
485: /** Y-coordinate of the upper left corner of the area */
486: private int y1;
487:
488: /** X-coordinate of the lower right corner of the area */
489: private int x2;
490:
491: /** Y-coordinate of the lower right corner of the area */
492: private int y2;
493:
494: /** Component painted on the area */
495: private Component component;
496:
497: /** <p>Construct a new area on a grid.
498: *
499: * @param x1 The X-coordinate of the upper left corner of the area
500: * <code>c</code> is supposed to occupy
501: * @param y1 The Y-coordinate of the upper left corner of the area
502: * <code>c</code> is supposed to occupy
503: * @param x2 The X-coordinate of the lower right corner of the area
504: * <code>c</code> is supposed to occupy
505: * @param y2 The Y-coordinate of the lower right corner of the area
506: * <code>c</code> is supposed to occupy
507: * @throws OverlapsException if the new component overlaps with any
508: * of the components already in the grid
509: */
510: public Area(Component component, int x1, int y1, int x2, int y2) {
511: this .x1 = x1;
512: this .y1 = y1;
513: this .x2 = x2;
514: this .y2 = y2;
515: this .component = component;
516: }
517:
518: /** Tests if the given Area overlaps with an another Area.
519: *
520: * @param other Another Area that's to be tested for overlap with
521: * this area
522: * @return <code>true</code> if <code>other</code> overlaps with
523: * this area, <code>false</code> if it doesn't
524: */
525: public boolean overlaps(Area other) {
526: return x1 <= other.getX2() && y1 <= other.getY2()
527: && x2 >= other.getX1() && y2 >= other.getY1();
528:
529: }
530:
531: /** Returns the component connected to the area.
532: * @return Component
533: */
534: public Component getComponent() {
535: return component;
536: }
537:
538: /** Sets the component connected to the area.
539: *
540: * <p>This function only sets the value in the datastructure and does not
541: * send any events or set parents</p>
542: *
543: * @param newComponent The new connected overriding the existing one
544: */
545: protected void setComponent(Component newComponent) {
546: component = newComponent;
547: }
548:
549: /** Returns the top-left corner x-coordinate.
550: * @return int
551: */
552: public int getX1() {
553: return x1;
554: }
555:
556: /**
557: * Returns the bottom-right corner x-coordinate.
558: * @return int
559: */
560: public int getX2() {
561: return x2;
562: }
563:
564: /**
565: * Returns the top-left corner y-coordinate.
566: * @return int
567: */
568: public int getY1() {
569: return y1;
570: }
571:
572: /**
573: * Returns the bottom-right corner y-coordinate.
574: * @return int
575: */
576: public int getY2() {
577: return y2;
578: }
579:
580: }
581:
582: /** An <code>Exception</code> object which is thrown when two Items
583: * occupy the same space on a grid.
584: * @author IT Mill Ltd.
585: * @version 3.1.1
586: * @since 3.0
587: */
588: public class OverlapsException extends java.lang.RuntimeException {
589:
590: /**
591: * Serial generated by eclipse.
592: */
593: private static final long serialVersionUID = 3978144339870101561L;
594:
595: private Area existingArea;
596:
597: /** Constructs an <code>OverlapsException</code>.
598: * @param msg the detail message.
599: */
600: public OverlapsException(Area existingArea) {
601: this .existingArea = existingArea;
602: }
603:
604: /** Get the area */
605: public Area getArea() {
606: return existingArea;
607: }
608: }
609:
610: /** An <code>Exception</code> object which is thrown when an area exceeds the
611: * bounds of the grid.
612: * @author IT Mill Ltd.
613: * @version 3.1.1
614: * @since 3.0
615: */
616: public class OutOfBoundsException extends
617: java.lang.RuntimeException {
618:
619: /**
620: * Serial generated by eclipse.
621: */
622: private static final long serialVersionUID = 3618985589664592694L;
623:
624: private Area areaOutOfBounds;
625:
626: /** Constructs an <code>OoutOfBoundsException</code> with the specified
627: * detail message.
628: *
629: * @param msg the detail message.
630: */
631: public OutOfBoundsException(Area areaOutOfBounds) {
632: this .areaOutOfBounds = areaOutOfBounds;
633: }
634:
635: /** Get the area that is out of bounds */
636: public Area getArea() {
637: return areaOutOfBounds;
638: }
639: }
640:
641: /** Set the width of the grid. The width can not be reduced if there are
642: * any areas that would be outside of the shrunk grid.
643: * @param width New width of the grid.
644: * @throws OutOfBoundsException if the one of the areas would exceed the
645: * bounds of the grid after the modification of the grid size.
646: */
647: public void setWidth(int width) {
648:
649: // The the param
650: if (width < 1)
651: throw new IllegalArgumentException(
652: "The grid width and height must be at least 1");
653:
654: // In case of no change
655: if (this .width == width)
656: return;
657:
658: // Check for overlaps
659: if (this .width > width)
660: for (Iterator i = areas.iterator(); i.hasNext();) {
661: Area area = (Area) i.next();
662: if (area.x2 >= width)
663: throw new OutOfBoundsException(area);
664: }
665:
666: this .width = width;
667:
668: requestRepaint();
669: }
670:
671: /** Get the width of the grids.
672: * @return The width of the grid
673: */
674: public final int getWidth() {
675: return this .width;
676: }
677:
678: /** Set the height of the grid. The width can not be reduced if there are
679: * any areas that would be outside of the shrunk grid.
680: * @param Height of the grid
681: */
682: public void setHeight(int height) {
683:
684: // The the param
685: if (height < 1)
686: throw new IllegalArgumentException(
687: "The grid width and height must be at least 1");
688:
689: // In case of no change
690: if (this .height == height)
691: return;
692:
693: // Check for overlaps
694: if (this .height > height)
695: for (Iterator i = areas.iterator(); i.hasNext();) {
696: Area area = (Area) i.next();
697: if (area.y2 >= height)
698: throw new OutOfBoundsException(area);
699: }
700:
701: this .height = height;
702:
703: requestRepaint();
704: }
705:
706: /** Get the height of the grid.
707: * @return int - how many cells high the grid is
708: */
709: public final int getHeight() {
710: return this .height;
711: }
712:
713: /** Get the current cursor x-position.
714: * The cursor position points the position for the next component
715: * that is added without specifying its coordinates. When the
716: * cursor position is occupied, the next component will be added
717: * to first free position after the cursor.
718: * @return Cursor x-coordinate.
719: */
720: public int getCursorX() {
721: return cursorX;
722: }
723:
724: /** Get the current cursor y-position.
725: * The cursor position points the position for the next component
726: * that is added without specifying its coordinates. When the
727: * cursor position is occupied, the next component will be added
728: * to first free position after the cursor.
729: * @return Cursor y-coordinate.
730: */
731: public int getCursorY() {
732: return cursorY;
733: }
734:
735: /* Documented in superclass */
736: public void replaceComponent(Component oldComponent,
737: Component newComponent) {
738:
739: // Get the locations
740: Area oldLocation = null;
741: Area newLocation = null;
742: for (Iterator i = areas.iterator(); i.hasNext();) {
743: Area location = (Area) i.next();
744: Component component = (Component) location.getComponent();
745: if (component == oldComponent)
746: oldLocation = location;
747: if (component == newComponent)
748: newLocation = location;
749: }
750:
751: if (oldLocation == null)
752: addComponent(newComponent);
753: else if (newLocation == null) {
754: removeComponent(oldComponent);
755: addComponent(newComponent, oldLocation.getX1(), oldLocation
756: .getY1(), oldLocation.getX2(), oldLocation.getY2());
757: } else {
758: oldLocation.setComponent(newComponent);
759: newLocation.setComponent(oldComponent);
760: requestRepaint();
761: }
762: }
763:
764: /*
765: * @see org.millstone.base.ui.ComponentContainer#removeAllComponents()
766: */
767: public void removeAllComponents() {
768: super .removeAllComponents();
769: this .cursorX = 0;
770: this .cursorY = 0;
771: }
772:
773: }
|