PicturePanel.java
  1  // HTMLParser Library $Name: v1_6_20060319 $ - A java-based parser for HTML
  2  // http://sourceforge.org/projects/htmlparser
  3  // Copyright (C) 2003 Derrick Oswald
  4  //
  5  // Revision Control Information
  6  //
  7  // $Source: /cvsroot/htmlparser/htmlparser/src/org/htmlparser/lexerapplications/thumbelina/PicturePanel.java,v $
  8  // $Author: derrickoswald $
  9  // $Date: 2005/04/12 11:27:41 $
 10  // $Revision: 1.2 $
 11  //
 12  // This library is free software; you can redistribute it and/or
 13  // modify it under the terms of the GNU Lesser General Public
 14  // License as published by the Free Software Foundation; either
 15  // version 2.1 of the License, or (at your option) any later version.
 16  //
 17  // This library is distributed in the hope that it will be useful,
 18  // but WITHOUT ANY WARRANTY; without even the implied warranty of
 19  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 20  // Lesser General Public License for more details.
 21  //
 22  // You should have received a copy of the GNU Lesser General Public
 23  // License along with this library; if not, write to the Free Software
 24  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 25  //
 26  
 27  package org.htmlparser.lexerapplications.thumbelina;
 28  
 29  import java.awt.Component;
 30  import java.awt.Dimension;
 31  import java.awt.Graphics;
 32  import java.awt.Image;
 33  import java.awt.Insets;
 34  import java.awt.Point;
 35  import java.awt.Rectangle;
 36  import java.awt.event.ComponentEvent;
 37  import java.awt.event.ComponentListener;
 38  import java.awt.event.HierarchyEvent;
 39  import java.awt.event.HierarchyListener;
 40  import java.awt.event.MouseEvent;
 41  import java.awt.event.MouseListener;
 42  import java.util.Enumeration;
 43  import java.util.HashSet;
 44  import javax.swing.JPanel;
 45  import javax.swing.JViewport;
 46  import javax.swing.Scrollable;
 47  import javax.swing.border.BevelBorder;
 48  
 49  /**
 50   * Hold and display a group of pictures.
 51   * @author  derrick
 52   */
 53  public class PicturePanel
 54      extends
 55          JPanel
 56      implements
 57          MouseListener,
 58          Scrollable,
 59          ComponentListener,
 60          HierarchyListener
 61  {
 62      /**
 63       * Scrolling unit increment (both directions).
 64       */
 65      protected static final int UNIT_INCREMENT = 10;
 66  
 67      /**
 68       * Scrolling block increment (both directions).
 69       */
 70      protected static final int BLOCK_INCREMENT = 100;
 71  
 72      /**
 73       * The thumbelina object in use.
 74       */
 75      protected Thumbelina mThumbelina;
 76  
 77      /**
 78       * The display mosaic.
 79       */
 80      protected TileSet mMosaic;
 81  
 82      /**
 83       * The preferred size of this component.
 84       * <code>null</code> initially, caches the results of
 85       * <code>calculatePreferredSize ()</code>.
 86       */
 87      protected Dimension mPreferredSize;
 88  
 89      /**
 90       * Creates a new instance of PicturePanel
 91       * @param thumbelina The <code>Thumeblina</code> this panel is associated
 92       * with.
 93       */
 94      public PicturePanel (final Thumbelina thumbelina)
 95      {
 96          mThumbelina = thumbelina;
 97          mMosaic = new TileSet ();
 98          mPreferredSize = null;
 99          setBorder (new BevelBorder (BevelBorder.LOWERED));
100          addMouseListener (this);
101          addHierarchyListener (this);
102      }
103  
104      /**
105       * Clears the panel, discarding any existing images.
106       */
107      public void reset ()
108      {
109          mMosaic = new TileSet ();
110          repaint ();
111      }
112  
113      /**
114       * Move the given picture to the top of the Z order.
115       * Adds it, even it if it doesn't exist.
116       * Also puts the URL in the url text of the status bar.
117       * @param picture The picture being brought forward.
118       */
119      public void bringToTop (final Picture picture)
120      {
121          picture.reset ();
122          mMosaic.bringToTop (picture);
123          repaint (picture.x, picture.y, picture.width, picture.height);
124          mThumbelina.mUrlText.setText (picture.getURL ().toExternalForm ());
125      }
126  
127      /**
128       * Find a picture with the given URL in the panel.
129       * This should really only be used to discover if the picture is still
130       * visible. There could be more than one picture with the given URL
131       * because it may be partially obscured by another picture, in which
132       * case the pieces are each given their own picture object, but all
133       * point at the same <code>URL</code> and <code>Image</code>.
134       * @param url The url to locate.
135       * @return The first picture encountered in the panel,
136       * or null if the picture was not found.
137       */
138      public Picture find (final String url)
139      {
140          Enumeration enumeration;
141          Picture picture;
142          Picture ret;
143  
144          ret = null;
145          enumeration = mMosaic.getPictures ();
146          while ((null == ret) && enumeration.hasMoreElements ())
147          {
148              picture = (Picture)enumeration.nextElement ();
149              if (url.equals (picture.getURL ().toExternalForm ()))
150                  ret = picture;
151          }
152  
153          return (ret);
154      }
155  
156      /**
157       * Draw an image on screen.
158       * @param picture The picture to draw.
159       * @param add If <code>true</code>, the picture is added to the history.
160       */
161      protected void draw (final Picture picture, final boolean add)
162      {
163          Component parent;
164          boolean dolayout;
165          Dimension before;
166          Dimension after;
167  
168          parent = getParent ();
169          dolayout = false;
170          synchronized (mMosaic)
171          {
172              if (parent instanceof JViewport)
173              {
174                  before = getPreferredSize ();
175                  mMosaic.add (picture);
176                  after = calculatePreferredSize ();
177                  if (after.width > before.width)
178                      dolayout = true;
179                  else
180                      after.width = before.width;
181                  if (after.height > before.height)
182                      dolayout = true;
183                  else
184                      after.height = before.height;
185                  if (dolayout)
186                      mPreferredSize = after;
187              }
188              else
189                  mMosaic.add (picture);
190          }
191          if (dolayout)
192              revalidate ();
193          repaint (picture.x, picture.y, picture.width, picture.height);
194          if (add)
195              mThumbelina.addHistory (picture.getURL ().toExternalForm ());
196      }
197  
198      /**
199       * Updates this component.
200       * @param graphics The graphics context in which to update the component.
201       */
202      public void update (final Graphics graphics)
203      {
204          paint (graphics);
205      }
206  
207      /**
208       * Adjust the graphics clip region to account for insets.
209       * @param graphics The graphics object to set the clip region for.
210       */
211      public void adjustClipForInsets (final Graphics graphics)
212      {
213          Dimension dim;
214          Insets insets;
215          Rectangle clip;
216  
217          dim = getSize ();
218          insets = getInsets ();
219          clip = graphics.getClipBounds ();
220          if (clip.x < insets.left)
221              clip.x = insets.left;
222          if (clip.y < insets.top)
223              clip.y = insets.top;
224          if (clip.x + clip.width > dim.width - insets.right)
225              clip.width = dim.width - insets.right - clip.x;
226          if (clip.y + clip.height > dim.height - insets.bottom)
227              clip.height = dim.height - insets.bottom - clip.y;
228          graphics.setClip (clip.x, clip.y, clip.width, clip.height);
229      }
230  
231      /**
232       * Paints this component.
233       * Runs through the list of tiles and for every one that intersects
234       * the clip region performs a <code>drawImage()</code>.
235       * @param graphics The graphics context used to paint with.
236       */
237      public void paint (final Graphics graphics)
238      {
239          Rectangle clip;
240          Enumeration enumeration;
241          HashSet set; // just so we don't draw things twice
242          Picture picture;
243          Image image;
244          Point origin;
245          int width;
246          int height;
247  
248          adjustClipForInsets (graphics);
249          clip = graphics.getClipBounds ();
250          synchronized (mMosaic)
251          {
252              if (0 == mMosaic.getSize ())
253                  super.paint (graphics);
254              else
255              {
256                  super.paint (graphics);
257                  enumeration = mMosaic.getPictures ();
258                  set = new HashSet ();
259                  while (enumeration.hasMoreElements ())
260                  {
261                      picture = (Picture)enumeration.nextElement ();
262                      if ((null == clip) || (clip.intersects (picture)))
263                      {
264                          image = picture.getImage ();
265                          if (!set.contains (image))
266                          {
267                              origin = picture.getOrigin ();
268                              width = image.getWidth (this);
269                              height = image.getHeight (this);
270                              graphics.drawImage (picture.getImage (),
271                                  origin.x, origin.y,
272                                  origin.x + width, origin.y + height,
273                                  0, 0, width, height,
274                                  this);
275                              set.add (image);
276                          }
277                      }
278                  }
279              }
280          }
281      }
282  
283  
284      /**
285       * Get the preferred size of the component.
286       * @return The dimension of this component.
287       */
288      public Dimension getPreferredSize ()
289      {
290          if (null == mPreferredSize)
291              setPreferredSize (calculatePreferredSize ());
292          else
293              if ((0 == mPreferredSize.width) || (0 == mPreferredSize.height))
294                  setPreferredSize (calculatePreferredSize ());
295          return (mPreferredSize);
296      }
297  
298      /**
299       * Sets the preferred size of this component.
300       * @param dimension The new value to use for
301       * <code>getPreferredSize()</code> until recalculated.
302       */
303      public void setPreferredSize (final Dimension dimension)
304      {
305          mPreferredSize = dimension;
306      }
307  
308      /**
309       * Compute the preferred size of the component.
310       * Computes the minimum bounding rectangle covering all the pictures in
311       * the panel. It then does some funky stuff to handle
312       * embedding in the view port of a scroll pane, basically asking
313       * up the ancestor heirarchy what size is available, and filling it.
314       * @return The optimal dimension for this component.
315       */
316      protected Dimension calculatePreferredSize ()
317      {
318          Enumeration enumeration;
319          int x;
320          int y;
321          Picture picture;
322          Component parent;
323          Insets insets;
324          Dimension ret;
325  
326          enumeration = mMosaic.getPictures ();
327          x = 0;
328          y = 0;
329          picture = null;
330          while (enumeration.hasMoreElements ())
331          {
332              picture = (Picture)enumeration.nextElement ();
333              if (picture.x + picture.width > x)
334                  x = picture.x + picture.width;
335              if (picture.y + picture.height > y)
336                  y = picture.y + picture.height;
337          }
338          parent = getParent ();
339          if (parent instanceof JViewport)
340          {
341              ret = parent.getSize ();
342              insets = ((JViewport)parent).getInsets ();
343              ret.width -= insets.left + insets.right;
344              ret.height -= insets.top + insets.bottom;
345              if ((0 != ret.width) || (0 != ret.height))
346                  ret.width -= 2; // ... I dunno why, it just needs it
347              if (ret.width < x)
348                  ret.width = x;
349              if (ret.height < y)
350                  ret.height = y;
351          }
352          else
353          {
354              insets = getInsets ();
355              x += insets.left + insets.right;
356              y += insets.top + insets.bottom;
357              ret = new Dimension (x, y);
358          }
359  
360          return (ret);
361      }
362  
363      //
364      // MouseListener Interface
365      //
366  
367      /**
368       * Invoked when the mouse button has been clicked
369       * (pressed and released) on a component.
370       * <i>Not used.</i>
371       * @param event The object providing details of the mouse event.
372       */
373      public void mouseClicked (final MouseEvent event)
374      {
375      }
376  
377      /**
378       *Invoked when a mouse button has been released on a component.
379       * <i>Not used.</i>
380       * @param event The object providing details of the mouse event.
381       */
382      public void mouseReleased (final MouseEvent event)
383      {
384      }
385  
386      /**
387       * Invoked when the mouse enters a component.
388       * <i>Not used.</i>
389       * @param event The object providing details of the mouse event.
390       */
391      public void mouseEntered (final MouseEvent event)
392      {
393      }
394  
395      /**
396       * Invoked when the mouse exits a component.
397       * <i>Not used.</i>
398       * @param event The object providing details of the mouse event.
399       */
400      public void mouseExited (final MouseEvent event)
401      {
402      }
403  
404      /**
405       * Handle left click on a picture by bringing it to the top.
406       * @param event The object providing details of the mouse event.
407       */
408      public void mousePressed (final MouseEvent event)
409      {
410          Picture picture;
411  
412          if (!event.isMetaDown ())
413          {
414              picture = mMosaic.pictureAt (event.getX (), event.getY ());
415              if (null != picture)
416                  bringToTop (picture);
417          }
418      }
419  
420      //
421      // Scrollable interface
422      //
423  
424      /**
425       * Returns the preferred size of the viewport for a view component.
426       * For example the preferredSize of a JList component is the size
427       * required to accommodate all of the cells in its list however the
428       * value of preferredScrollableViewportSize is the size required for
429       * JList.getVisibleRowCount() rows.   A component without any properties
430       * that would effect the viewport size should just return
431       * getPreferredSize() here.
432       *
433       * @return The preferredSize of a JViewport whose view is this Scrollable.
434       * @see JViewport#getPreferredSize
435       */
436      public Dimension getPreferredScrollableViewportSize ()
437      {
438          return (getPreferredSize ());
439      }
440  
441  
442      /**
443       * Components that display logical rows or columns should compute
444       * the scroll increment that will completely expose one new row
445       * or column, depending on the value of orientation.  Ideally,
446       * components should handle a partially exposed row or column by
447       * returning the distance required to completely expose the item.
448       * <p>
449       * Scrolling containers, like JScrollPane, will use this method
450       * each time the user requests a unit scroll.
451       *
452       * @param visibleRect The view area visible within the viewport
453       * @param orientation Either SwingConstants.VERTICAL or
454       * SwingConstants.HORIZONTAL.
455       * @param direction Less than zero to scroll up/left,
456       * greater than zero for down/right.
457       * @return The "unit" increment for scrolling in the specified direction.
458       *         This value should always be positive.
459       */
460      public int getScrollableUnitIncrement (
461          final Rectangle visibleRect,
462          final int orientation,
463          final int direction)
464      {
465          return (UNIT_INCREMENT);
466      }
467  
468  
469      /**
470       * Components that display logical rows or columns should compute
471       * the scroll increment that will completely expose one block
472       * of rows or columns, depending on the value of orientation.
473       * <p>
474       * Scrolling containers, like JScrollPane, will use this method
475       * each time the user requests a block scroll.
476       *
477       * @param visibleRect The view area visible within the viewport
478       * @param orientation Either SwingConstants.VERTICAL or
479       * SwingConstants.HORIZONTAL.
480       * @param direction Less than zero to scroll up/left,
481       * greater than zero for down/right.
482       * @return The "block" increment for scrolling in the specified direction.
483       *         This value should always be positive.
484       */
485      public int getScrollableBlockIncrement (
486          final Rectangle visibleRect,
487          final int orientation,
488          final int direction)
489      {
490          return (BLOCK_INCREMENT);
491      }
492  
493  
494      /**
495       * Return true if a viewport should always force the width of this
496       * <code>Scrollable</code> to match the width of the viewport.
497       * For example a normal
498       * text view that supported line wrapping would return true here, since it
499       * would be undesirable for wrapped lines to disappear beyond the right
500       * edge of the viewport.  Note that returning true for a Scrollable
501       * whose ancestor is a JScrollPane effectively disables horizontal
502       * scrolling.
503       * <p>
504       * Scrolling containers, like JViewport, will use this method each
505       * time they are validated.
506       *
507       * @return <code>true</code> if a viewport should force the Scrollables
508       * width to match its own.
509       */
510      public boolean getScrollableTracksViewportWidth ()
511      {
512          return (false);
513      }
514  
515      /**
516       * Return true if a viewport should always force the height of this
517       * Scrollable to match the height of the viewport.  For example a
518       * columnar text view that flowed text in left to right columns
519       * could effectively disable vertical scrolling by returning
520       * true here.
521       * <p>
522       * Scrolling containers, like JViewport, will use this method each
523       * time they are validated.
524       *
525       * @return <code>true</code> if a viewport should force the Scrollables
526       * height to match its own.
527       */
528      public boolean getScrollableTracksViewportHeight ()
529      {
530          return (false);
531      }
532  
533      //
534      // ComponentListener interface
535      //
536  
537      /**
538       * Invoked when the container's size changes.
539       * Un-caches the preferred size.
540       * @param event The resize event.
541       */
542      public void componentResized (final ComponentEvent event)
543      {
544          setPreferredSize (null);
545      }
546  
547      /**
548       * Invoked when the component's position changes.
549       * <i>Not used.</I>
550       * @param event The component event.
551       */
552      public void componentMoved (final ComponentEvent event)
553      {
554      }
555  
556      /**
557       * Invoked when the component has been made visible.
558       * <i>Not used.</I>
559       * @param event The component event.
560       */
561      public void componentShown (final ComponentEvent event)
562      {
563      }
564  
565      /**
566       * Invoked when the component has been made invisible.
567       * <i>Not used.</I>
568       * @param event The component event.
569       */
570      public void componentHidden (final ComponentEvent event)
571      {
572      }
573  
574      //
575      // HierarchyListener interface
576      //
577  
578      /**
579       * Handles this components ancestor being added to a container.
580       * Registers this component as a listener for size changes on the
581       * ancestor so that we may un-cache the prefereed size and force
582       * a recalculation.
583       * @param event The heirarchy event.
584       */
585      public void hierarchyChanged (final HierarchyEvent event)
586      {
587          if (0 != (event.getChangeFlags () & HierarchyEvent.PARENT_CHANGED))
588          {
589              Component dad = event.getChanged ();
590              Component parent = getParent ();
591              if ((null != parent) && (parent.getParent () == dad))
592                  dad.addComponentListener (this);
593          }
594      }
595  }
596  
597  /*
598   * Revision Control Modification History
599   *
600   * $Log: PicturePanel.java,v $
601   * Revision 1.2  2005/04/12 11:27:41  derrickoswald
602   * Documentation revamp part two.
603   *
604   * Revision 1.1  2003/09/21 18:20:56  derrickoswald
605   * Thumbelina
606   * Created a lexer GUI application to extract images behind thumbnails.
607   * Added a task in the ant build script - thumbelina - to create the jar file.
608   * You need JDK 1.4.x to build it.  It can be run on JDK 1.3.x in crippled mode.
609   * Usage: java -Xmx256M thumbelina.jar [URL]
610   *
611   *
612   */