View Javadoc

1   // START LICENSE
2   // JSet - a Java JSet card board game implementation
3   // Copyright (C) 2004 Jerome Lacoste
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 2 of the License, or (at
8   // your option) any later version.
9   //
10  // This program is distributed in the hope that it will be useful, but
11  // WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // 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, write to the Free Software
17  // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
18  // END LICENSE
19  
20  package org.cb.jset.client.ui;
21  
22  import org.cb.jset.CardProperties;
23  import org.cb.jset.JSetCard;
24  import org.cb.jset.client.model.SetGameClientCardListener;
25  import org.cb.jset.client.model.CardEvent;
26  import org.apache.log4j.Logger;
27  
28  import javax.swing.JPanel;
29  import javax.swing.JComponent;
30  import javax.swing.BorderFactory;
31  import javax.swing.border.Border;
32  import java.awt.Rectangle;
33  import java.awt.Color;
34  import java.awt.Paint;
35  import java.awt.Graphics;
36  import java.awt.Graphics2D;
37  import java.awt.Shape;
38  import java.awt.TexturePaint;
39  import java.awt.BasicStroke;
40  import java.awt.Stroke;
41  import java.awt.geom.RectangularShape;
42  import java.awt.geom.Area;
43  import java.awt.geom.Ellipse2D;
44  import java.awt.geom.GeneralPath;
45  import java.awt.event.MouseAdapter;
46  import java.awt.event.MouseEvent;
47  import java.awt.image.BufferedImage;
48  import java.util.HashMap;
49  import java.util.Map;
50  import java.io.Serializable;
51  
52  /***
53   * The visual component representing a card on the board.
54   *
55   * @author jerome@coffeebreaks.org - last modified by $LastChangedBy: jerome $
56   * @version $Id: JSetCardComponent.java 130 2004-04-15 05:18:07Z jerome $
57   */
58  public class JSetCardComponent extends JPanel
59  {
60    private final transient Logger _logger;
61    private ShapeComponent[] _shapes;
62  
63    private boolean _isSelected = false;
64    // private Border _selectedBorder = BorderFactory.createLoweredBevelBorder();
65    private Border _selectedBorder = BorderFactory.createLineBorder(Color.black, 2);
66    private Border _unselectedBorder = BorderFactory.createRaisedBevelBorder();
67    private EventHandler _eventHandler = new EventHandler();
68    private JSetCard _card;
69  
70    /***
71     * A class representing the third type of shape.
72     * @todo make it a subclass of RectangularShape ?
73     */
74    class OddShape
75    {
76      // int xa, ya, x1, y1, x2, y2, x3, y3;
77      private final GeneralPath _oddShape;
78  //    private boolean _firstTime;
79  //    private final Rectangle _area;
80      /***
81       * Constructs an instance of <code>OddShape</code>.
82       */
83      public OddShape()
84      {
85        _oddShape = new GeneralPath();
86  //      _firstTime = true;
87  //      _area = new Rectangle();
88      }
89  
90      /***
91       * Return a general path that walks throught the contour of the odd shape, given the specified bounds.
92       * @param bounds
93       * @return the general path of the odd shape
94       */
95      GeneralPath constructOddShape(final Rectangle bounds)
96      {
97        _logger.debug("building odd shape " + bounds);
98        //
99        //   x0,y0 --- x1,y1
100       //     \         \
101       //      \         \
102       //      |         |
103       //     /         /
104       //    /         /
105       //   |         |
106       //    \         \
107       //   x2,y2 ---- x3,y3
108 
109       final int margin = 8;
110 
111       final int x0 = (int) bounds.getX() + margin;
112       final int y0 = (int) bounds.getY();
113       _oddShape.moveTo(x0, y0);
114       final int x1 = x0 + (int) bounds.getWidth() - 2 * margin;
115       final int y1 = y0;
116       _oddShape.lineTo(x1, y1);
117 
118       final int x3 = x1;
119       final int y3 = y1 + (int) bounds.getHeight();
120       final int bez1x1 = x1 + 15;
121       final int bez1y1 = y1 + 10;
122       final int bez1x2 = x1 - 15;
123       final int bez1y2 = y3 - 10;
124       _oddShape.curveTo(bez1x1, bez1y1, bez1x2, bez1y2, x3, y3);
125 
126       final int x2 = x0;
127       final int y2 = y3;
128       _oddShape.lineTo(x2, y2);
129 
130       // temp
131       final int bez2x1 = x2 - 15;
132       final int bez2y1 = y2 - 10;
133       final int bez2x2 = x2 + 15;
134       final int bez2y2 = y0 + 10;
135       _oddShape.curveTo(bez2x1, bez2y1, bez2x2, bez2y2, x0, y0);
136 //      _oddShape.lineTo(x0, y0);
137 
138       return _oddShape;
139     }
140   }
141 
142   /***
143    * A graphical component representing the shapes on the card.
144    */
145   final class ShapeComponent extends JComponent
146   {
147     private final Color _color;
148     private final CardProperties _cardProperties;
149     private final transient Logger _logger;
150     private Map _shapes;
151 
152     /***
153      * Constructs an instance of <code>ShapeComponent</code> given the specified card properties, and index
154      * on the card.
155      * @param cardProperties the properties used to determine the shape
156      * @param index the index used to determine the position.
157      */
158     private ShapeComponent(final CardProperties cardProperties, final int index)
159     {
160       _cardProperties = cardProperties;
161       _color = getColor(_cardProperties.getColor());
162       _logger = Logger.getLogger("Shape " + index + " " + cardProperties);
163       setBackground(Color.white);
164     }
165 
166     private Color getColor(final byte pColor)
167     {
168       switch(pColor)
169       {
170         case CardProperties.COLOR_RED:
171           return Color.RED;
172         case CardProperties.COLOR_GREEN:
173           return new Color(0, 102, 0);
174         case CardProperties.COLOR_BLUE:
175           return Color.BLUE;
176         default:
177           throw new IllegalStateException("Invalid color " + pColor);
178       }
179     }
180 
181     /***
182      * @inheritDoc
183      */
184     protected void paintComponent(final Graphics g)
185     {
186       _logger.debug("painting shape component: " +  getBounds());
187       super.paintComponent(g);    //To change body of overridden methods use File | Settings | File Templates.
188 
189       final Graphics2D g2 = (Graphics2D) g;
190 
191       _logger.debug("this bounds: " +  getBounds());
192       _logger.debug("container bounds: " +  getParent().getBounds());
193 
194       final Paint oldPaint = g2.getPaint();
195 
196       switch (_cardProperties.getFill())
197       {
198         case CardProperties.FILL_EMPTY :
199         case CardProperties.FILL_FULL :
200           g2.setPaint(_color);
201           break;
202         case CardProperties.FILL_DOTTED:
203           final BufferedImage bi = new BufferedImage(5, 5, BufferedImage.TYPE_INT_RGB);
204           final Graphics2D big = bi.createGraphics();
205           big.setColor(_color);
206           big.fillRect(0, 0, 5, 5);
207           big.setColor(getParent().getBackground());
208           big.fillOval(0, 0, 5, 5);
209           final Rectangle r = new Rectangle(0,0,5,5);
210           g2.setPaint(new TexturePaint(bi, r));
211           break;
212       }
213 
214       // set the stroke
215       switch (_cardProperties.getFill())
216       {
217         case CardProperties.FILL_EMPTY:
218           final Stroke biggerStroke = new BasicStroke(4.0f);
219           g2.setStroke(biggerStroke);
220           break;
221         case CardProperties.FILL_FULL :
222           break;
223         case CardProperties.FILL_DOTTED:
224           break;
225       }
226 
227       final Shape shape = getShape(_cardProperties.getShape());
228       _logger.debug("%%% shape: " + shape + " bounds " + shape.getBounds());
229 
230       // Determines whether to fill, stroke, or fill and stroke.
231       switch (_cardProperties.getFill())
232       {
233         case CardProperties.FILL_FULL :
234           g2.fill(shape);
235           break;
236         case CardProperties.FILL_DOTTED:
237           final Graphics2D tempg2 = g2;
238           g2.fill(shape);
239           g2.setColor(Color.darkGray);
240           g2.draw(shape);
241           g2.setPaint(tempg2.getPaint());
242           break;
243         case CardProperties.FILL_EMPTY:
244           g2.draw(shape);
245           break;
246       }
247 
248       _logger.debug("painting circle: " +  getSize());
249 
250       g2.setPaint(oldPaint);
251     }
252 
253     private Map initShapes()
254     {
255       final Map shapes = new HashMap(3);
256 
257       final Rectangle rectangle = new Rectangle();
258       final Rectangle internRectangle = new Rectangle();
259       shapes.put(new Integer(CardProperties.SHAPE_RECTANGLE), createShape(rectangle, internRectangle));
260 
261       final Ellipse2D.Double oval = new Ellipse2D.Double();
262       final Ellipse2D.Double internOval = new Ellipse2D.Double();
263       shapes.put(new Integer(CardProperties.SHAPE_OVALE), createShape(oval, internOval));
264 
265       final Rectangle lShapeBounds =
266           new Rectangle(0, 0, (int) getBounds().getWidth(), (int) getBounds().getHeight());
267       final Shape odd = new OddShape().constructOddShape(lShapeBounds);
268       shapes.put(new Integer(CardProperties.SHAPE_ODD), odd);
269 
270       return shapes;
271     }
272 
273     /***
274      * Create a shape by {@link Area#subtract(java.awt.geom.Area) substracting} the specified
275      * internal shape to the specified external one.
276      * @param externalShape
277      * @param internShape
278      * @return
279      */
280     private Area createShape(final RectangularShape externalShape, final RectangularShape internShape)
281     {
282       externalShape.setFrame(0, 0, getBounds().getWidth(), getBounds().getHeight());
283       // FIXME rename
284       final int xxxX = (int) (getBounds().getWidth() / 4);
285       final int xxxY = (int) (getBounds().getHeight() / 4);
286       internShape.setFrame(xxxX, xxxY, getBounds().getWidth() - 2 * xxxX, getBounds().getHeight() - 2 * xxxY);
287       final Area area2 = new Area(externalShape);
288       final Area internOv = new Area(internShape);
289       area2.subtract(internOv);
290       return area2;
291     }
292 
293     private Shape getShape(final byte shapeID)
294     {
295       // lazily construct the shapes so that they are built at a time when surrouding size information is known.
296       if (_shapes == null)
297       {
298         _shapes = this.initShapes();
299       }
300       return (Shape) _shapes.get(new Integer(shapeID));
301     }
302   }
303 
304   /***
305    * Constructs an instance of <code>JSetCardComponent</code> given the specified card.
306    * @param card
307    */
308   public JSetCardComponent(final JSetCard card)
309   {
310     _logger = Logger.getLogger("Shape " + card);
311     _card = card;
312     _shapes = initShapes(card.getProperties());
313     for (int i = 0; i < _shapes.length; i++)
314     {
315       final ShapeComponent mShape = _shapes[i];
316       this.add(mShape);
317     }
318     init();
319   }
320 
321   private void init()
322   {
323     this.setBackground(Color.white);
324     this.addMouseListener(_eventHandler);
325     this.setBorder(_unselectedBorder);
326     final int width = 80;
327     final int height = (int) (1.5 * width);
328     setSize(width, height);
329     setPreferredSize(getSize());
330     _logger.debug("initialized: " +  getSize());
331   }
332 
333   private ShapeComponent[] initShapes(final CardProperties pProperties)
334   {
335     final ShapeComponent[] lResult = new ShapeComponent[pProperties.getNumber()];
336     for (int i = 0; i < lResult.length; i++)
337     {
338       lResult[i] = new ShapeComponent(pProperties, i);
339     }
340     return lResult;
341   }
342 
343 
344   private void resetShapeBounds()
345   {
346     final int width = (int) this.getBounds().getWidth();
347     final int height = (int) this.getBounds().getHeight();
348     // margins when displaying 3 shapes
349     final int sideMargin = 5;
350     final int middleMargin = 5;
351     final int shapeWidth = (width - 2 * sideMargin - middleMargin) / 2;
352     final int shapeHeight = (height - 2 * sideMargin - middleMargin) / 2;
353     _logger.debug("** setting new shapes bounds: " + this.getBounds());
354     _logger.debug("** shapeWidth: " + shapeWidth + " shapeHeight: " + shapeHeight);
355 
356     switch (_shapes.length)
357     {
358       case 1:
359         _shapes[0].setBounds((width - shapeWidth) / 2, (height - shapeHeight) / 2, shapeWidth, shapeHeight);
360         break;
361       case 2:
362         // upper _area
363         _shapes[0].setBounds((width - shapeWidth) / 2, sideMargin, shapeWidth, shapeHeight);
364         // lower _area
365         _shapes[1].setBounds((width - shapeWidth) / 2, sideMargin + shapeHeight + middleMargin, shapeWidth, shapeHeight);
366         break;
367       case 3:
368         // upper _area
369         _shapes[0].setBounds(sideMargin, sideMargin, shapeWidth, shapeHeight);
370         _shapes[1].setBounds(sideMargin + shapeWidth + middleMargin, sideMargin, shapeWidth, shapeHeight);
371         // lower _area
372         _shapes[2].setBounds((width - shapeWidth) / 2, sideMargin + shapeHeight + middleMargin, shapeWidth, shapeHeight);
373         break;
374       default:
375         throw new IllegalStateException("Invalid number of shapes " + _shapes.length);
376     }
377     for (int i = 0; i < _shapes.length; i++)
378     {
379       final ShapeComponent mShape = _shapes[i];
380       _logger.debug("shape[" + i + "]s bounds: " + mShape.getBounds());
381     }
382   }
383 
384   // debugging code
385   /*
386   public void paint(final Graphics g)
387   {
388     super.paint(g);    //To change body of overridden methods use File | Settings | File Templates.
389     _logger.debug("painting card: " +  getBounds());
390   }
391 
392   // debugging code
393   public void setSize(int width, int height)
394   {
395     _logger.debug("setting size, " + width + " " + height);
396     super.setSize(width, height);    //To change body of overridden methods use File | Settings | File Templates.
397   }
398 
399   public void setBounds(final int x, final int y, final int width, final int height)
400   {
401     _logger.debug("setting bounds, " + x + " " + y + " " + width + " " + height);
402     super.setBounds(x, y, width, height);
403     _logger.debug("this bounds: " +  getBounds());
404     if (getParent() != null)
405     {
406       _logger.debug("container bounds: " +  getParent().getBounds());
407     }
408   }
409   */
410 
411   /***
412    * Update the border depending on the internal selection status.
413    * @see #isSelected
414    */
415   public void updateBorder()
416   {
417     if (_isSelected)
418     {
419       setBorder(_selectedBorder);
420     }
421     else
422     {
423       setBorder(_unselectedBorder);
424     }
425     invalidate();
426   }
427 
428   /***
429    * Modifies the internal selection status
430    */
431   private void changeSelection()
432   {
433     setSelection(! _isSelected);
434   }
435 
436   /***
437    * Set the new internal selection status for the card.
438    * If the selection has changed, fire an new {@link CardEvent} with the appropriate event type.
439    * @param newSelection
440    * @see #isSelected()
441    */
442   public void setSelection(final boolean newSelection)
443   {
444     final boolean oldSelection = _isSelected;
445     if (oldSelection != newSelection)
446     {
447       _isSelected = newSelection;
448       final int selected = _isSelected ? CardEvent.SELECTED : CardEvent.DESELECTED;
449       fireCardSelectionEvent(new CardEvent(getCard(), selected));
450     }
451   }
452 
453   /***
454    * @return <code>true</code> if the card is selected, <code>false</code> otherwise.
455    */
456   public boolean isSelected()
457   {
458     return _isSelected;
459   }
460 
461   /***
462    * @return the card
463    */
464   public JSetCard getCard()
465   {
466     return _card;
467   }
468 
469   /***
470    * Paint this card component given the specified graphics.
471    * @param g
472    */
473   protected void paintComponent(final Graphics g)
474   {
475     _logger.debug("painting card component: " +  getBounds());
476     super.paintComponent(g);    //To change body of overridden methods use File | Settings | File Templates.
477     resetShapeBounds();
478     updateBorder();
479   }
480 
481   /***
482    * @param i
483    * @return the ith shape from that card.
484    */
485   ShapeComponent getShape(int i)
486   {
487     return _shapes[i];
488   }
489 
490   /***
491    * A mouse listener that {@link JSetCardComponent#changeSelection changes the card's selection} upon a mouse click.
492    */
493   private class EventHandler extends MouseAdapter implements Serializable
494   {
495     /***
496      * @inheritDoc
497      */
498     public void mouseClicked(final MouseEvent e)
499     {
500       // Checks whether or not the cursor is inside of the rectangle when the
501       // user releases the mouse button.
502 			if(JSetCardComponent.this.contains(e.getX(), e.getY()))
503       {
504         changeSelection();
505       }
506     }
507   }
508 
509   /***
510    * Adds the specified cardlistener to the component listener list.
511    * @param cardListener
512    */
513   void addCardEventListener(final SetGameClientCardListener cardListener)
514   {
515     super.listenerList.add(SetGameClientCardListener.class, cardListener);
516   }
517 
518   /***
519    * Removes the specified cardlistener to the component listener list.
520    * @param cardListener
521    */
522   void removeCardEventListener(final SetGameClientCardListener cardListener)
523   {
524     super.listenerList.remove(SetGameClientCardListener.class, cardListener);
525   }
526 
527   /*** fire the specified card selection event
528    * @param e the event to fire
529    * @see SetGameClientCardListener#notifyCardEvent(org.cb.jset.client.model.CardEvent)
530    */
531   private void fireCardSelectionEvent(final CardEvent e)
532   {
533     // Guaranteed to return a non-null array
534     final Object[] listeners = super.listenerList.getListenerList();
535     _logger.debug("" + listeners.length);
536     // Process the listeners last to first, notifying
537     // those that are interested in this event
538     for (int i = listeners.length - 2; i >= 0; i -= 2)
539     {
540       if (listeners[i] == SetGameClientCardListener.class)
541       {
542         ((SetGameClientCardListener) listeners[i + 1]).notifyCardEvent(e);
543       }
544     }
545   }
546 }