Java tutorial
/* Copyright (c) 2006 Harri Kaimio This file is part of Photovault. Photovault is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. Photovault is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Photovault; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.photovault.swingui; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Container; import java.awt.Dimension; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Stroke; import java.awt.Toolkit; import java.awt.event.ComponentEvent; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.photovault.command.CommandException; import org.photovault.dbhelper.ODMGXAWrapper; import org.photovault.imginfo.FuzzyDate; import org.photovault.imginfo.PhotoCollection; import org.photovault.imginfo.PhotoCollectionChangeEvent; import org.photovault.imginfo.PhotoCollectionChangeListener; import org.photovault.imginfo.PhotoInfo; import org.photovault.imginfo.PhotoInfoChangeEvent; import org.photovault.imginfo.PhotoInfoChangeListener; import org.photovault.imginfo.Thumbnail; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentListener; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseEvent; import java.awt.font.FontRenderContext; import java.awt.font.TextLayout; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ImageIcon; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JMenu; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JViewport; import javax.swing.KeyStroke; import javax.swing.Scrollable; import javax.swing.SwingConstants; import javax.swing.TransferHandler; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.photovault.folder.PhotoFolder; import org.photovault.imginfo.CreateCopyImageCommand; import org.photovault.imginfo.CreatePreviewImagesTask; import org.photovault.imginfo.Volume; import org.photovault.imginfo.VolumeDAO; import org.photovault.swingui.taskscheduler.TaskPriority; import org.photovault.taskscheduler.TaskProducer; import org.photovault.taskscheduler.BackgroundTask; /** This class implements the default thumbnail view for photo collections. Some of the planned features include: <ul> <li> Either vertically or horizontally scrollable view with multiple columns </li> <li> Multiple selection for thumbnails </li> <li> Automatic fetching and creation of thumbnails on background if these do not exist </li> </ul> <h1> Selection & drag-n-drop logic</h1> <ul> <li> If mouse is pressed between images, the drag is interpreted as a drag selection </li> <li> If mouse is pressed on top of a thumbnail the drag is interpreted as a drag-n-drop operation </li> </ul> @author Harri Kaimio */ public class PhotoCollectionThumbView extends JPanel implements MouseMotionListener, MouseListener, ActionListener, PhotoCollectionChangeListener, PhotoInfoChangeListener, Scrollable, TaskProducer { static Log log = LogFactory.getLog(PhotoCollectionThumbView.class.getName()); PhotoViewController ctrl; /** Default constructor */ public PhotoCollectionThumbView() { this(null, null); } /** Creates a new <code>PhotoCollectionThumbView</code> instance. @param collection Initial collection to show. If this collection is nonempty, select the first photo in it when creating the control. */ public PhotoCollectionThumbView(PhotoViewController ctrl, List<PhotoInfo> initialPhotos) { super(); this.ctrl = ctrl; createUI(); if (photos.size() > 0) { selection.add(photos.get(0)); } setPhotos(initialPhotos); } public void setPhotos(List<PhotoInfo> photos) { for (PhotoInfo photo : this.photos) { photo.removeChangeListener(this); } this.photos.clear(); if (photos != null) { for (Object o : photos) { PhotoInfo photo = (PhotoInfo) o; photo.addChangeListener(this); this.photos.add(photo); } } revalidate(); repaint(); } public List<PhotoInfo> getPhotos() { return Collections.unmodifiableList(photos); } /** Removes all change listeners this photo has added, repopulates the photos array from current photoCollection and adds change listeners to all photos in it. */ private void refreshPhotoChangeListeners() { // remove change listeners from all existing photos Iterator iter = photos.iterator(); while (iter.hasNext()) { PhotoInfo photo = (PhotoInfo) iter.next(); photo.removeChangeListener(this); } photos.clear(); // Add the change listeners to all photos so that we are aware of modifications for (int n = 0; n < photos.size(); n++) { PhotoInfo photo = photos.get(n); photo.addChangeListener(this); } } List<PhotoInfo> photos = new ArrayList<PhotoInfo>(); /** * Get a currently selected photos * @return Collection of currently selected photos or <code>null</code> if none is selected */ public Collection getSelection() { return new HashSet(selection); } /** Returns the number of photos that are selected in this view */ public int getSelectedCount() { return selection.size(); } /** Removes given photo from selection if it currently is selected. If photo is removed, a selection change event is sent to all listeners. @param p The photo that is removed from selection */ public void removeFromSelection(PhotoInfo p) { if (selection.contains(p)) { selection.remove(p); fireSelectionChangeEvent(); } } Set<PhotoInfo> selection = new HashSet<PhotoInfo>(); /** Adds a listener to listen for selection changes */ public void addSelectionChangeListener(SelectionChangeListener l) { selectionChangeListeners.add(l); } /** removes a listener for selection changes */ public void removeSelectionChangeListener(SelectionChangeListener l) { selectionChangeListeners.remove(l); } protected void fireSelectionChangeEvent() { Iterator iter = selectionChangeListeners.iterator(); while (iter.hasNext()) { SelectionChangeListener l = (SelectionChangeListener) iter.next(); l.selectionChanged(new SelectionChangeEvent(this)); } } List<SelectionChangeListener> selectionChangeListeners = new ArrayList<SelectionChangeListener>(); int columnWidth = 150; public int getColumnWidth() { return columnWidth; } int rowHeight = 150; public int getRowHeight() { return rowHeight; } int thumbWidth = 100; int thumbHeight = 100; int columnCount = 1; int rowCount = -1; int columnsToPaint = 1; /** * Start used for quality indicator */ ImageIcon starIcon = null; /** * Icon used to indicate rejected image */ ImageIcon rejectedIcon = null; /** Icon to indicate that this image is stored in raw format. */ ImageIcon rawIcon = null; JPopupMenu popup = null; /** This helper class from Java Tutorial handles displaying of popup menu on correct mouse events */ class PopupListener extends MouseAdapter { @Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); } @Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if (e.isPopupTrigger()) { popup.show(e.getComponent(), e.getX(), e.getY()); } } } TransferHandler photoTransferHandler = null; /** Icon to display in front of a thumbnail while Photovault is creating a new replacement for it. */ ImageIcon creatingThumbIcon = null; void createUI() { photoTransferHandler = new PhotoCollectionTransferHandler(this); setTransferHandler(photoTransferHandler); setAutoscrolls(true); addMouseListener(this); addMouseMotionListener(this); // Create the popup menu popup = new JPopupMenu(); ImageIcon propsIcon = getIcon("view_properties.png"); editSelectionPropsAction = new EditSelectionPropsAction(this, "Properties...", propsIcon, "Edit properties of the selected photos", KeyEvent.VK_P); JMenuItem propsItem = new JMenuItem(editSelectionPropsAction); ImageIcon colorsIcon = getIcon("colors.png"); editSelectionColorsAction = new EditSelectionColorsAction(this, null, "Adjust colors...", colorsIcon, "Adjust colors of the selected photos", KeyEvent.VK_A); JMenuItem colorsItem = new JMenuItem(editSelectionColorsAction); ImageIcon showIcon = getIcon("show_new_window.png"); showSelectedPhotoAction = new ShowSelectedPhotoAction(this, "Show image", showIcon, "Show the selected phot(s)", KeyEvent.VK_S); JMenuItem showItem = new JMenuItem(showSelectedPhotoAction); showHistoryAction = new ShowPhotoHistoryAction(this, "Show history", null, "Show history of selected photo", KeyEvent.VK_H, null); resolveConflictsAction = new ResolvePhotoConflictsAction(this, "Resolve conflicts", null, "Resolve synchronization conflicts", KeyEvent.VK_R, null); JMenuItem rotateCW = new JMenuItem(ctrl.getActionAdapter("rotate_cw")); JMenuItem rotateCCW = new JMenuItem(ctrl.getActionAdapter("rotate_ccw")); JMenuItem rotate180deg = new JMenuItem(ctrl.getActionAdapter("rotate_180")); JMenuItem addToFolder = new JMenuItem("Add to folder..."); addToFolder.addActionListener(this); addToFolder.setActionCommand(PHOTO_ADD_TO_FOLDER_CMD); addToFolder.setIcon(getIcon("empty_icon.png")); ImageIcon exportIcon = getIcon("filesave.png"); exportSelectedAction = new ExportSelectedAction(this, "Export selected...", exportIcon, "Export the selected photos to from archive database to image files", KeyEvent.VK_E); JMenuItem exportSelected = new JMenuItem(exportSelectedAction); ImageIcon deleteSelectedIcon = getIcon("delete_image.png"); deleteSelectedAction = new DeletePhotoAction(this, "Delete", deleteSelectedIcon, "Delete selected photos including all of their instances", KeyEvent.VK_D); JMenuItem deleteSelected = new JMenuItem(deleteSelectedAction); starIcon = getIcon("star_normal_border.png"); rejectedIcon = getIcon("quality_unusable.png"); rawIcon = getIcon("raw_icon.png"); JMenuItem showHistory = new JMenuItem(showHistoryAction); JMenuItem resolveConflicts = new JMenuItem(resolveConflictsAction); AddTagAction addTagAction = new AddTagAction(ctrl, "Add tag...", null, "Add tag to image", KeyEvent.VK_T); JMenuItem addTag = new JMenuItem(addTagAction); popup.add(showItem); popup.add(propsItem); popup.add(colorsItem); popup.add(rotateCW); popup.add(rotateCCW); popup.add(rotate180deg); popup.add(addToFolder); popup.add(exportSelected); popup.add(deleteSelected); popup.add(showHistory); popup.add(resolveConflicts); popup.add(addTag); MouseListener popupListener = new PopupListener(); addMouseListener(popupListener); ImageIcon selectNextIcon = getIcon("next.png"); selectNextAction = new ChangeSelectionAction(this, ChangeSelectionAction.MOVE_FWD, "Next photo", selectNextIcon, "Move to next photo", KeyEvent.VK_N, KeyStroke.getKeyStroke(KeyEvent.VK_N, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); ImageIcon selectPrevIcon = getIcon("previous.png"); selectPrevAction = new ChangeSelectionAction(this, ChangeSelectionAction.MOVE_BACK, "Previous photo", selectPrevIcon, "Move to previous photo", KeyEvent.VK_P, KeyStroke.getKeyStroke(KeyEvent.VK_P, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask())); creatingThumbIcon = getIcon("creating_thumb.png"); } /** Loads an icon using class loader of this class @param resouceName Name of the icon reosurce to load @return The icon or <code>null</code> if no image was found using the given resource name. */ private ImageIcon getIcon(String resourceName) { ImageIcon icon = null; java.net.URL iconURL = JAIPhotoViewer.class.getClassLoader().getResource(resourceName); if (iconURL != null) { icon = new ImageIcon(iconURL); } return icon; } /** * Sets the shape of the thumbnail grid so that it has specified number of columns. * When this is set then row count is adjusted so that all thumbnails fit. */ public void setColumnCount(int c) { columnCount = c; // If number of columns is fixed number of rows must be dynamic. rowCount = -1; columnsToPaint = c; revalidate(); repaint(); } /** * Return number of columns or -1 if this is adjusted dynamically based on row cound or component size */ public int getColumnCount() { return columnCount; } public void setRowCount(int c) { rowCount = c; columnCount = -1; revalidate(); repaint(); } public int getRowCount() { return rowCount; } public void setThumbWidth(int width) { thumbWidth = width; thumbHeight = width; columnWidth = thumbWidth + 50; rowHeight = thumbHeight + 50; revalidate(); repaint(); } /** * Set the heith of one row of photos. Maximum size of thumbnails is * determined based on this. * @param newHeight Height of row in pixels */ public void setRowHeight(int newHeight) { rowHeight = newHeight; columnWidth = newHeight; if (newHeight > 150) { thumbWidth = thumbHeight = newHeight - 50; } else if (newHeight > 100) { thumbWidth = thumbHeight = 100; } else { thumbWidth = thumbHeight = Math.max(1, newHeight - 8); } revalidate(); repaint(); } // Popup menu actions private static final String PHOTO_ADD_TO_FOLDER_CMD = "addToFolder"; private AbstractAction exportSelectedAction; private AbstractAction editSelectionPropsAction; private AbstractAction editSelectionColorsAction; private AbstractAction showSelectedPhotoAction; private AbstractAction rotateCWAction; private AbstractAction rotateCCWAction; private AbstractAction rotate180degAction; private AbstractAction selectNextAction; private AbstractAction selectPrevAction; private AbstractAction deleteSelectedAction; private AbstractAction showHistoryAction; private AbstractAction resolveConflictsAction; public AbstractAction getExportSelectedAction() { return exportSelectedAction; } public AbstractAction getEditSelectionPropsAction() { return editSelectionPropsAction; } public AbstractAction getEditSelectionColorsAction() { return editSelectionColorsAction; } public AbstractAction getShowSelectedPhotoAction() { return showSelectedPhotoAction; } public AbstractAction getRotateCWActionAction() { return rotateCWAction; } public AbstractAction getRotateCCWActionAction() { return rotateCCWAction; } public AbstractAction getRotate180degActionAction() { return rotate180degAction; } public AbstractAction getSelectNextAction() { return selectNextAction; } public AbstractAction getSelectPreviousAction() { return selectPrevAction; } public AbstractAction getDeleteSelectedAction() { return deleteSelectedAction; } @Override public void paint(Graphics g) { super.paint(g); Graphics2D g2 = (Graphics2D) g; Rectangle clipRect = g.getClipBounds(); Dimension compSize = getSize(); // columnCount = (int)(compSize.getWidth()/columnWidth); int photoCount = 0; if (photos != null) { photoCount = photos.size(); } // Determine the grid size based on couln & row count columnsToPaint = columnCount; // if columnCount is not specified determine it based on row count if (columnCount < 0) { if (rowCount > 0) { columnsToPaint = photoCount / rowCount; if (columnsToPaint * rowCount < photoCount) { columnsToPaint++; } } else { columnsToPaint = (int) (compSize.getWidth() / columnWidth); } } int col = 0; int row = 0; Rectangle thumbRect = new Rectangle(); for (PhotoInfo photo : photos) { thumbRect.setBounds(col * columnWidth, row * rowHeight, columnWidth, rowHeight); if (thumbRect.intersects(clipRect)) { if (photo != null) { paintThumbnail(g2, photo, col * columnWidth, row * rowHeight, selection.contains(photo)); } } col++; if (col >= columnsToPaint) { row++; col = 0; } } // Paint the selection rectangle if needed if (dragSelectionRect != null) { Stroke prevStroke = g2.getStroke(); Color prevColor = g2.getColor(); g2.setStroke(new BasicStroke(1.0f)); g2.setColor(Color.BLACK); g2.draw(dragSelectionRect); g2.setColor(prevColor); g2.setStroke(prevStroke); lastDragSelectionRect = dragSelectionRect; } } boolean showDate = true; boolean showPlace = true; boolean showQuality = true; private void paintThumbnail(Graphics2D g2, PhotoInfo photo, int startx, int starty, boolean isSelected) { log.debug("paintThumbnail entry " + photo.getUuid()); long startTime = System.currentTimeMillis(); long thumbReadyTime = 0; long thumbDrawnTime = 0; long endTime = 0; // Current position in which attributes can be drawn int ypos = starty + rowHeight / 2; boolean useOldThumbnail = false; Thumbnail thumbnail = null; log.debug("finding thumb"); boolean hasThumbnail = photo.hasThumbnail(); log.debug("asked if has thumb"); if (hasThumbnail) { log.debug("Photo " + photo.getUuid() + " has thumbnail"); thumbnail = photo.getThumbnail(); log.debug("got thumbnail"); } else { /* Check if the thumbnail has been just invalidated. If so, use the old one until we get the new thumbnail created. */ thumbnail = photo.getOldThumbnail(); if (thumbnail != null) { useOldThumbnail = true; } else { // No success, use default thumnail. thumbnail = Thumbnail.getDefaultThumbnail(); } // Inform background task scheduler that we have some work to do ctrl.getBackgroundTaskScheduler().registerTaskProducer(this, TaskPriority.CREATE_VISIBLE_THUMBNAIL); } thumbReadyTime = System.currentTimeMillis(); log.debug("starting to draw"); // Find the position for the thumbnail BufferedImage img = thumbnail.getImage(); if (img == null) { thumbnail = Thumbnail.getDefaultThumbnail(); img = thumbnail.getImage(); } float scaleX = ((float) thumbWidth) / ((float) img.getWidth()); float scaleY = ((float) thumbHeight) / ((float) img.getHeight()); float scale = Math.min(scaleX, scaleY); int w = (int) (img.getWidth() * scale); int h = (int) (img.getHeight() * scale); int x = startx + (columnWidth - w) / (int) 2; int y = starty + (rowHeight - h) / (int) 2; log.debug("drawing thumbnail"); // Draw shadow int offset = isSelected ? 2 : 0; int shadowX[] = { x + 3 - offset, x + w + 1 + offset, x + w + 1 + offset }; int shadowY[] = { y + h + 1 + offset, y + h + 1 + offset, y + 3 - offset }; GeneralPath polyline = new GeneralPath(GeneralPath.WIND_EVEN_ODD, shadowX.length); polyline.moveTo(shadowX[0], shadowY[0]); for (int index = 1; index < shadowX.length; index++) { polyline.lineTo(shadowX[index], shadowY[index]); } ; BasicStroke shadowStroke = new BasicStroke(4.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER); Stroke oldStroke = g2.getStroke(); g2.setStroke(shadowStroke); g2.setColor(Color.DARK_GRAY); g2.draw(polyline); g2.setStroke(oldStroke); // Paint thumbnail g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g2.drawImage(img, new AffineTransform(scale, 0f, 0f, scale, x, y), null); if (useOldThumbnail) { creatingThumbIcon.paintIcon(this, g2, startx + (columnWidth - creatingThumbIcon.getIconWidth()) / (int) 2, starty + (rowHeight - creatingThumbIcon.getIconHeight()) / (int) 2); } log.debug("Drawn, drawing decorations"); if (isSelected) { Stroke prevStroke = g2.getStroke(); Color prevColor = g2.getColor(); g2.setStroke(new BasicStroke(3.0f)); g2.setColor(Color.BLUE); g2.drawRect(x, y, w, h); g2.setColor(prevColor); g2.setStroke(prevStroke); } thumbDrawnTime = System.currentTimeMillis(); boolean drawAttrs = (thumbWidth >= 100); if (drawAttrs) { // Increase ypos so that attributes are drawn under the image ypos += ((int) h) / 2 + 3; // Draw the attributes // Draw the qualoity icon to the upper left corner of the thumbnail int quality = photo.getQuality(); if (showQuality && quality != 0) { int qx = startx + (columnWidth - quality * starIcon.getIconWidth()) / (int) 2; for (int n = 0; n < quality; n++) { starIcon.paintIcon(this, g2, qx, ypos); qx += starIcon.getIconWidth(); } ypos += starIcon.getIconHeight(); } ypos += 6; if (photo.getRawSettings() != null) { // Draw the "RAW" icon int rx = startx + (columnWidth + w - rawIcon.getIconWidth()) / (int) 2 - 5; int ry = starty + (columnWidth - h - rawIcon.getIconHeight()) / (int) 2 + 5; rawIcon.paintIcon(this, g2, rx, ry); } if (photo.getHistory().getHeads().size() > 1) { // Draw the "unresolved conflicts" icon int rx = startx + (columnWidth + w - 10) / (int) 2 - 20; int ry = starty + (columnWidth - h - 10) / (int) 2; g2.setColor(Color.RED); g2.fillRect(rx, ry, 10, 10); } Color prevBkg = g2.getBackground(); if (isSelected) { g2.setBackground(Color.BLUE); } else { g2.setBackground(this.getBackground()); } Font attrFont = new Font("Arial", Font.PLAIN, 10); FontRenderContext frc = g2.getFontRenderContext(); if (showDate && photo.getShootTime() != null) { FuzzyDate fd = new FuzzyDate(photo.getShootTime(), photo.getTimeAccuracy()); String dateStr = fd.format(); TextLayout txt = new TextLayout(dateStr, attrFont, frc); // Calculate the position for the text Rectangle2D bounds = txt.getBounds(); int xpos = startx + ((int) (columnWidth - bounds.getWidth())) / 2 - (int) bounds.getMinX(); g2.clearRect(xpos - 2, ypos - 2, (int) bounds.getWidth() + 4, (int) bounds.getHeight() + 4); txt.draw(g2, xpos, (int) (ypos + bounds.getHeight())); ypos += bounds.getHeight() + 4; } String shootPlace = photo.getShootingPlace(); if (showPlace && shootPlace != null && shootPlace.length() > 0) { TextLayout txt = new TextLayout(photo.getShootingPlace(), attrFont, frc); // Calculate the position for the text Rectangle2D bounds = txt.getBounds(); int xpos = startx + ((int) (columnWidth - bounds.getWidth())) / 2 - (int) bounds.getMinX(); g2.clearRect(xpos - 2, ypos - 2, (int) bounds.getWidth() + 4, (int) bounds.getHeight() + 4); txt.draw(g2, xpos, (int) (ypos + bounds.getHeight())); ypos += bounds.getHeight() + 4; } g2.setBackground(prevBkg); } endTime = System.currentTimeMillis(); log.debug("paintThumbnail: exit " + photo.getUuid()); log.debug("Thumb fetch " + (thumbReadyTime - startTime) + " ms"); log.debug("Thumb draw " + (thumbDrawnTime - thumbReadyTime) + " ms"); log.debug("Deacoration draw " + (endTime - thumbDrawnTime) + " ms"); log.debug("Total " + (endTime - startTime) + " ms"); } @Override public Dimension getPreferredSize() { int prefWidth = 0; int prefHeight = 0; if (columnCount > 0) { prefWidth = columnWidth * columnCount; prefHeight = rowHeight; if (photos != null) { prefHeight += rowHeight * (int) (photos.size() / columnCount); } } else if (rowCount > 0) { prefHeight = rowHeight * rowCount; prefWidth = columnWidth; if (photos != null) { prefWidth += columnWidth * (int) (photos.size() / rowCount); } } else { prefWidth = 500; prefHeight = 500; Dimension compSize = getSize(); if (compSize.getWidth() > 0) { int columns = (int) (compSize.getWidth() / columnWidth); prefWidth = columnWidth * columns; prefHeight = rowHeight; if (photos != null) { prefHeight += rowHeight * (int) (photos.size() / columns); } } } // prefHeight += 10; return new Dimension(prefWidth, prefHeight); } final static int MIN_CELL_WIDTH = 20; final static int MAX_CELL_WIDTH = 250; @Override public Dimension getMinimumSize() { int minWidth = 0; int minHeight = 0; if (columnCount > 0) { minWidth = MIN_CELL_WIDTH * columnCount; minHeight = MIN_CELL_WIDTH; if (photos != null) { minHeight += MIN_CELL_WIDTH * (int) (photos.size() / columnCount); } } else if (rowCount > 0) { minHeight = MIN_CELL_WIDTH * rowCount; minWidth = MIN_CELL_WIDTH; if (photos != null) { minWidth += MIN_CELL_WIDTH * (int) (photos.size() / rowCount); } } else { return getPreferredSize(); } return new Dimension(minWidth, minHeight); } @Override public Dimension getMaximumSize() { int minWidth = 0; int minHeight = 0; if (columnCount > 0) { minWidth = MAX_CELL_WIDTH * columnCount; minHeight = MAX_CELL_WIDTH; if (photos != null) { minHeight += MAX_CELL_WIDTH * (int) (photos.size() / columnCount); } } else if (rowCount > 0) { minHeight = MAX_CELL_WIDTH * rowCount; minWidth = MAX_CELL_WIDTH; if (photos != null) { minWidth += MAX_CELL_WIDTH * (int) (photos.size() / rowCount); } } else { return getPreferredSize(); } return new Dimension(minWidth, minHeight); } // implementation of java.awt.event.ActionListener interface /** * ActionListener implementation, is called when a popup menu item is selected * @param e The event object */ public void actionPerformed(ActionEvent e) { String cmd = e.getActionCommand(); if (cmd == PHOTO_ADD_TO_FOLDER_CMD) { queryForNewFolder(); } } public void photoCollectionChanged(PhotoCollectionChangeEvent e) { refreshPhotoChangeListeners(); revalidate(); repaint(); } /** This method is called when a PhotoInfo that is visible in the view is changed. it redraws the thumbnail & texts ascociated with it */ public void photoInfoChanged(PhotoInfoChangeEvent ev) { PhotoInfo photo = (PhotoInfo) ev.getSource(); repaintPhoto(photo); // Find the location of the photo } /** Issues a repaint request for a certain photo. @param n index of the photo in photos */ protected void repaintPhoto(int n) { if (n >= 0) { int row = (int) (n / columnsToPaint); int col = n - (row * columnsToPaint); repaint(0, col * columnWidth, row * rowHeight, columnWidth, rowHeight); } } /** Issues a repaint request for a certain photo. @param photo Photo to be repainted */ protected void repaintPhoto(PhotoInfo photo) { int n = photos.indexOf(photo); repaintPhoto(n); } /** Checks which photo is under the specified coordinates @return The photo that covers the position, null if the coordinates point to free space between photos. */ private PhotoInfo getPhotoAtLocation(int x, int y) { if (photos == null) { return null; } PhotoInfo photo = null; int row = y / rowHeight; int col = x / columnWidth; int photoNum = row * columnsToPaint + col; log.debug("Located photo # " + photoNum); if (photoNum < photos.size()) { Rectangle imgRect = getPhotoBounds(photoNum); if (imgRect.contains(new Point(x, y))) { photo = photos.get(photoNum); } } return photo; } /** * Get bounding rectangle for the area in which the thumbnail for a given photo * is displayed * @param photoNum order number of the photo * @return Rectangle bounding the photo or null if photoNum is larger than * # of photos. */ protected Rectangle getPhotoBounds(int photoNum) { if (photoNum < photos.size()) { PhotoInfo photoCandidate = photos.get(photoNum); log.debug("Checking bounds"); // Get thumbnail dimensions or use defaults if no thumbnail available int width = thumbWidth; int height = thumbHeight; Thumbnail thumb = null; if (photoCandidate.hasThumbnail()) { thumb = photoCandidate.getThumbnail(); } if (thumb != null) { BufferedImage img = thumb.getImage(); double scaleW = ((double) thumbWidth) / img.getWidth(); double scaleH = ((double) thumbHeight) / img.getHeight(); double scale = Math.min(scaleW, scaleH); width = (int) (img.getWidth() * scale); height = (int) (img.getHeight() * scale); } int row = photoNum / columnsToPaint; int col = photoNum - row * columnsToPaint; int imgX = col * columnWidth + (columnWidth - width) / 2; int imgY = row * rowHeight + (rowHeight - height) / 2; Rectangle imgRect = new Rectangle(imgX, imgY, width, height); return imgRect; } return null; } /** Get bounding rectangle for the table cell in which a given photo is displayed <p> This method is faster than @see getPhotoBounds since it does not need to query PhotoInfo and its thumbnail (which may in worst case require loading the thumbnail from disk) </p> * @param photoNum order number of the photo * @return Rectangle bounding the table cell or null if photoNum is larger than * # of photos. */ protected Rectangle getPhotoCellBounds(int photoNum) { Rectangle boundsRect = null; if (photoNum >= 0 && photoNum < photos.size()) { int row = (int) photoNum / columnsToPaint; int col = photoNum - row * columnsToPaint; int imgX = col * columnWidth; int imgY = row * rowHeight; boundsRect = new Rectangle(imgX, imgY, columnWidth, rowHeight); } return boundsRect; } /** The mouse press event that started current drag */ MouseEvent firstMouseEvent = null; // Implementation of java.awt.event.MouseListener /** * On mouse click select the photo clicked. * * @param mouseEvent a <code>MouseEvent</code> value */ public void mouseClicked(MouseEvent mouseEvent) { log.debug("mouseClicked (" + mouseEvent.getX() + ", " + mouseEvent.getY()); if (dragJustEnded) { // Selection was already handled by drag handler so do nothing dragJustEnded = false; return; } PhotoInfo clickedPhoto = getPhotoAtLocation(mouseEvent.getX(), mouseEvent.getY()); if (clickedPhoto != null) { if (mouseEvent.isControlDown()) { photoClickedCtrlDown(clickedPhoto); } else { photoClickedNoModifiers(clickedPhoto); } // If this was a doublke click open the selected photo(s) if (mouseEvent.getClickCount() == 2) { showSelectedPhotoAction.actionPerformed(new ActionEvent(this, 0, null)); } } else { // The click was between photos. Clear the selection if (!mouseEvent.isControlDown()) { Object[] oldSelection = selection.toArray(); selection.clear(); fireSelectionChangeEvent(); for (int n = 0; n < oldSelection.length; n++) { PhotoInfo photo = (PhotoInfo) oldSelection[n]; repaintPhoto(photo); } } } repaintPhoto(clickedPhoto); } private void photoClickedCtrlDown(PhotoInfo clickedPhoto) { if (selection.contains(clickedPhoto)) { selection.remove(clickedPhoto); } else { selection.add(clickedPhoto); } fireSelectionChangeEvent(); } private void photoClickedNoModifiers(PhotoInfo clickedPhoto) { // Clear selection & issue repaint requests for all selected photos Object[] oldSelection = selection.toArray(); selection.clear(); for (int n = 0; n < oldSelection.length; n++) { PhotoInfo photo = (PhotoInfo) oldSelection[n]; repaintPhoto(photo); } selection.add(clickedPhoto); fireSelectionChangeEvent(); } /** * Describe <code>mouseEntered</code> method here. * * @param mouseEvent a <code>MouseEvent</code> value */ public void mouseEntered(MouseEvent mouseEvent) { } /** * Describe <code>mouseExited</code> method here. * * @param mouseEvent a <code>MouseEvent</code> value */ public void mouseExited(MouseEvent mouseEvent) { } /** * Describe <code>mousePressed</code> method here. * * @param mouseEvent a <code>MouseEvent</code> value */ public void mousePressed(MouseEvent mouseEvent) { // save the mouse press event so that we can later decide whether this gesture // is intended as a drag firstMouseEvent = mouseEvent; PhotoInfo photo = getPhotoAtLocation(mouseEvent.getX(), mouseEvent.getY()); if (photo == null) { dragType = DRAG_TYPE_SELECT; // If ctrl is not down clear the selection if (!mouseEvent.isControlDown()) { Object[] oldSelection = selection.toArray(); selection.clear(); fireSelectionChangeEvent(); for (int n = 0; n < oldSelection.length; n++) { PhotoInfo p = (PhotoInfo) oldSelection[n]; repaintPhoto(p); } } dragSelectionRect = new Rectangle(mouseEvent.getX(), mouseEvent.getY(), 0, 0); } else { dragType = DRAG_TYPE_DND; } } int dragType = 0; static final int DRAG_TYPE_SELECT = 1; static final int DRAG_TYPE_DND = 2; /** Area covered by current selection drag */ Rectangle dragSelectionRect = null; Rectangle lastDragSelectionRect = null; boolean dragJustEnded = false; /** * * * @param mouseEvent a <code>MouseEvent</code> value */ public void mouseReleased(MouseEvent mouseEvent) { firstMouseEvent = null; if (dragType == DRAG_TYPE_SELECT && photos != null) { // Find out thumbails inside the selection rectangle // First lets restrict search to those rows that intersect with selection int topRow = (int) dragSelectionRect.getMinY() / rowHeight; int bottomRow = ((int) dragSelectionRect.getMaxY() / rowHeight) + 1; int startPhoto = topRow * columnsToPaint; int endPhoto = bottomRow * columnsToPaint; if (endPhoto > photos.size()) { endPhoto = photos.size(); } // Find out which photos are selected for (int n = startPhoto; n < endPhoto; n++) { /* Performance optimization: Since getPhotoBounds() needs access to photo thumbnail which may not yet be loaded we will do first a rough check of if the table cell is in the selection area. */ Rectangle cellRect = getPhotoCellBounds(n); if (dragSelectionRect.intersects(cellRect)) { Rectangle photoRect = getPhotoBounds(n); if (dragSelectionRect.intersects(photoRect)) { selection.add(photos.get(n)); repaintPhoto(photos.get(n)); } } } fireSelectionChangeEvent(); // Redrw the selection area so that the selection rectangle is not shown anymore Rectangle repaintRect = dragSelectionRect; if (lastDragSelectionRect != null) { repaintRect = dragSelectionRect.union(lastDragSelectionRect); } repaint((int) repaintRect.getX() - 1, (int) repaintRect.getY() - 1, (int) repaintRect.getWidth() + 2, (int) repaintRect.getHeight() + 2); dragSelectionRect = null; lastDragSelectionRect = null; // Notify the mouse click handler that it has to do nothing dragJustEnded = true; } } // Implementation of java.awt.event.MouseMotionListener /** * Describe <code>mouseDragged</code> method here. * * @param e The event object */ public void mouseDragged(MouseEvent e) { switch (dragType) { case DRAG_TYPE_SELECT: handleSelectionDragEvent(e); break; case DRAG_TYPE_DND: handleDnDDragEvent(e); break; default: log.error("Invalid drag type"); } // Make sure tht current drag location is visible Rectangle r = new Rectangle(e.getX(), e.getY(), 1, 1); scrollRectToVisible(r); } protected void handleSelectionDragEvent(MouseEvent e) { dragSelectionRect = new Rectangle(firstMouseEvent.getX(), firstMouseEvent.getY(), 0, 0); dragSelectionRect.add(e.getX(), e.getY()); // Determine which area needs to be redrawn. If there is a selection marker rectangle already drawn // the redraw are must be union of the previous and current areas (current area can be also smaller! Rectangle repaintRect = dragSelectionRect; if (lastDragSelectionRect != null) { repaintRect = dragSelectionRect.union(lastDragSelectionRect); } repaint((int) repaintRect.getX() - 1, (int) repaintRect.getY() - 1, (int) repaintRect.getWidth() + 2, (int) repaintRect.getHeight() + 2); } protected void handleDnDDragEvent(MouseEvent e) { //Don't bother to drag if no photo is selected if (selection.isEmpty()) { return; } if (firstMouseEvent != null) { log.debug("considering drag"); e.consume(); //If they are holding down the control key, COPY rather than MOVE int ctrlMask = InputEvent.CTRL_DOWN_MASK; int action = e.isControlDown() ? TransferHandler.COPY : TransferHandler.MOVE; int dx = Math.abs(e.getX() - firstMouseEvent.getX()); int dy = Math.abs(e.getY() - firstMouseEvent.getY()); //Arbitrarily define a 5-pixel shift as the //official beginning of a drag. if (dx > 5 || dy > 5) { log.debug("Start a drag"); //This is a drag, not a click. JComponent c = (JComponent) e.getSource(); //Tell the transfer handler to initiate the drag. TransferHandler handler = c.getTransferHandler(); handler.exportAsDrag(c, firstMouseEvent, action); firstMouseEvent = null; } } } public void selectNextPhoto() { if (this.getSelectedCount() == 1) { PhotoInfo selectedPhoto = (PhotoInfo) (selection.toArray())[0]; int idx = photos.indexOf(selectedPhoto); idx++; if (idx >= photos.size()) { idx = photos.size() - 1; } // Scroll so that the selected photo is visible Rectangle selectionBounds = getPhotoCellBounds(idx); scrollRectToVisible(selectionBounds); selection.clear(); selection.add(photos.get(idx)); fireSelectionChangeEvent(); repaint(); } } public void selectPreviousPhoto() { if (this.getSelectedCount() == 1) { PhotoInfo selectedPhoto = (PhotoInfo) (selection.toArray())[0]; int idx = photos.indexOf(selectedPhoto); idx--; if (idx < 0) { idx = 0; } // Scroll so that the selected photo is visible Rectangle selectionBounds = getPhotoCellBounds(idx); scrollRectToVisible(selectionBounds); selection.clear(); selection.add(photos.get(idx)); fireSelectionChangeEvent(); repaint(); } } public void selectFirstPhoto() { if (photos.size() > 0) { // Scroll so that the selected photo is visible Rectangle selectionBounds = getPhotoCellBounds(0); scrollRectToVisible(selectionBounds); selection.clear(); selection.add(photos.get(0)); fireSelectionChangeEvent(); repaint(); } } /** * Describe <code>mouseMoved</code> method here. * * @param mouseEvent a <code>MouseEvent</code> value */ public void mouseMoved(MouseEvent mouseEvent) { } PhotoInfoDlg propertyDlg = null; JFrame frame = null; /** Queries the user for a new folder into which the photo will be added. */ public void queryForNewFolder() { // Try to find the frame in which this component is in Frame frame = null; Container c = getTopLevelAncestor(); if (c instanceof Frame) { frame = (Frame) c; } PhotoFolderSelectionDlg dlg = new PhotoFolderSelectionDlg(frame, true); if (dlg.showDialog()) { PhotoFolder folder = dlg.getSelectedFolder(); // A folder was selected, so add the selected photo to this folder Collection selectedPhotos = getSelection(); Iterator iter = selectedPhotos.iterator(); while (iter.hasNext()) { PhotoInfo photo = (PhotoInfo) iter.next(); if (photo != null) { folder.addPhoto(photo); } } } } // IMplementation of Scrollable interface public Dimension getPreferredScrollableViewportSize() { return getPreferredSize(); } /** Get the amount that needs to be scrolled when pressing scroll bar button. @return number of pixels to scroll until next image thumbnail */ public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { //Get the current position. int currentPosition = 0; int incrementUnit = 0; if (orientation == SwingConstants.HORIZONTAL) { currentPosition = visibleRect.x; incrementUnit = columnWidth; } else { currentPosition = visibleRect.y; incrementUnit = rowHeight; } //Return the number of pixels between currentPosition //and the nearest tick mark in the indicated direction. if (direction < 0) { int newPosition = currentPosition - (currentPosition / incrementUnit) * incrementUnit; return (newPosition == 0) ? incrementUnit : newPosition; } else { return ((currentPosition / incrementUnit) + 1) * incrementUnit - currentPosition; } } /** Get the number of pixels to xcroll if user clicks on scroll bar outside handle. */ public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { if (orientation == SwingConstants.HORIZONTAL) return visibleRect.width - columnWidth; else return visibleRect.height - rowHeight; } /** Returns whether containing Scroll pane should force width of the component to match its width. @return Returns <code>true</code> if the component is in "no preview" mode. */ public boolean getScrollableTracksViewportWidth() { boolean shouldTrack = false; if (rowCount < 0 && columnCount < 0) { shouldTrack = true; } return shouldTrack; } public boolean getScrollableTracksViewportHeight() { return false; } /** This method is called by background task scheduler when it is ready to execute a task for this window. @return Task to create a thumbnail if one is needed. <code>null</code> otherwise. */ public BackgroundTask requestTask() { PhotoInfo nextPhoto = null; Container parent = getParent(); Rectangle viewRect = null; if (parent instanceof JViewport) { viewRect = ((JViewport) parent).getViewRect(); } // Walk through all photos until we find a photo that is visible // and does not have a thumbnail. If all visible photos have a thumbnail // but some non-visible ones do not, create a thumbnail for one of those. log.debug("Finding photo without thumbnail"); for (int n = 0; n < photos.size(); n++) { PhotoInfo photoCandidate = photos.get(n); log.debug("Photo " + photoCandidate.getUuid()); if (!photoCandidate.hasThumbnail()) { log.debug("No thumbnail"); Rectangle photoRect = getPhotoBounds(n); if (photoRect.intersects(viewRect)) { // This photo is visible so it is a perfect candidate // for thumbnail creation. Do not look further nextPhoto = photoCandidate; break; } else if (nextPhoto == null) { // Not visible but no photo without thumbnail has been // found previously. Store as a candidate and keep looking. nextPhoto = photoCandidate; } } } if (nextPhoto != null) { // We found a photo without thumbnail VolumeDAO volDAO = ctrl.getDAOFactory().getVolumeDAO(); Volume vol = volDAO.getDefaultVolume(); return new CreatePreviewImagesTask(nextPhoto); } // All photos have thumbnail :-) return null; } /** Called after executing a thumbnail creation task. CUrrently a no-op. @param task The task executed */ public void taskExecuted(BackgroundTask task) { } }