/*
* (c) Copyright 2009 Tim Jenkins
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* ITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
package com.screenrunner.ui;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.AttributedString;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.Vector;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import com.screenrunner.ScreenRunner;
import com.screenrunner.data.I18n;
import com.screenrunner.data.ImageSlide;
import com.screenrunner.data.VectorListModel;
/**
* @author Tim Jenkins
*
* Media picker dialog
* Used for both inserting images and video into the current playlist
* and showing on-the-fly media (in "immediate" mode)
*
*/
public class MediaPicker extends CookSwingDialog {
private static final int ICON_WIDTH = 80;
private static final int ICON_HEIGHT = 60;
private static final int PREVIEW_WIDTH = 200;
private static final int PREVIEW_HEIGHT = 150;
public JDialog rootDialog;
public JTextField currentDirField;
public JList itemsList;
public JPanel previewPanel;
public JLabel previewNameLabel;
public JLabel previewSizeLabel;
public JButton doneButton;
public JButton cancelButton;
public JButton chooseButton;
private boolean isImmediate = false;
private boolean allowImages = false;
private boolean allowVideos = false;
private String initialDir = null;
private PreviewImageLoader curPreviewLoader = null;
private VectorListModel<String> fileList = null;
private Hashtable<String, BufferedImage> previews = null;
private Hashtable<String, BufferedImage> thumbs = null;
private Hashtable<String, Dimension> imgSizes = null;
//private Hashtable<String, Boolean> previewUpdates = null;
public MediaPicker(boolean immediate, boolean allowimages, boolean allowvideos, String initialDir) {
isImmediate = immediate;
allowImages = allowimages;
allowVideos = allowvideos;
fileList = new VectorListModel<String>();
previews = new Hashtable<String, BufferedImage>();
thumbs = new Hashtable<String, BufferedImage>();
imgSizes = new Hashtable<String, Dimension>();
//previewUpdates = new Hashtable<String, Boolean>();
this.initialDir = initialDir;
}
@Override
protected String getDialogXml() {
return "com/screenrunner/ui/xml/MediaPicker.xml";
}
@Override
protected void initDialog() {
previewPanel.setLayout(new BorderLayout());
doneButton.setVisible(isImmediate);
rootDialog.setTitle(isImmediate ? I18n.get("ui/dialogs/mediapicker/title?show") : I18n.get("ui/dialogs/mediapicker/title?add"));
cancelButton.setVisible(!isImmediate);
chooseButton.setText(isImmediate ? I18n.get("ui/dialogs/generic/show") : I18n.get("ui/dialogs/generic/add"));
itemsList.setSelectionMode(isImmediate ? ListSelectionModel.SINGLE_SELECTION : ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
itemsList.setModel(fileList);
itemsList.setCellRenderer(new ListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList list,
Object value, int index, boolean isSelected,
boolean cellHasFocus) {
UIDefaults uid = UIManager.getLookAndFeel().getDefaults();
Color itemBg = (isSelected ? uid.getColor("List.selectionBackground") : uid.getColor("List.background"));
Color itemFg = (isSelected ? uid.getColor("List.selectionForeground") : uid.getColor("List.foreground"));
JLabel lbl = new JLabel();
JPanel lblPnl = new JPanel(new GridBagLayout()) {
private static final long serialVersionUID = 1L;
@Override
protected void paintComponent(Graphics g) {
boolean temp = this.isOpaque();
this.setOpaque(false);
if(temp) {
Graphics2D g2d = (Graphics2D)g;
g2d.setColor(this.getBackground());
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.fillRoundRect(0, 0, this.getWidth(), this.getHeight(), 10, 10);
}
super.paintComponent(g);
this.setOpaque(temp);
}
};
wrapTextIntoLabel(lbl, value.toString(), list.getFixedCellWidth() - 6);
JLabel imglbl = new JLabel();
if(previews.containsKey(value.toString())) {
BufferedImage img = previews.get(value.toString());
if(isSelected) {
img = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB);
Graphics g = img.getGraphics();
g.drawImage(previews.get(value.toString()), 0, 0, img.getWidth(), img.getHeight(), null);
g.setColor(new Color(itemBg.getRed(), itemBg.getGreen(), itemBg.getBlue(), 127));
g.fillRect(0, 0, img.getWidth(), img.getHeight());
}
imglbl.setIcon(new ImageIcon(img));
}
Dimension sz = new Dimension(80, 60);
imglbl.setIconTextGap(1);
imglbl.setPreferredSize(sz);
imglbl.setMaximumSize(sz);
imglbl.setMinimumSize(sz);
imglbl.setHorizontalAlignment(JLabel.CENTER);
imglbl.setVerticalAlignment(JLabel.CENTER);
lbl.setHorizontalAlignment(JLabel.CENTER);
lbl.setHorizontalTextPosition(JLabel.CENTER);
lbl.setVerticalAlignment(JLabel.TOP);
lbl.setVerticalTextPosition(JLabel.TOP);
lbl.setForeground(itemFg);
JPanel pnl = new JPanel(new GridBagLayout());
pnl.setOpaque(false);
lblPnl.setBackground(itemBg);
lblPnl.setOpaque(isSelected);
lblPnl.add(lbl, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(1,3,3,3), 0, 0));
pnl.add(imglbl, new GridBagConstraints(0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE, new Insets(2,2,2,2), 0, 0));
pnl.add(lblPnl, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.NORTH, GridBagConstraints.NONE, new Insets(0,0,0,0), 0, 0));
return pnl;
}
});
currentDirField.setText(initialDir);
fillMediaList();
}
private class PreviewImageLoader implements Runnable {
private boolean cancel = false;
public PreviewImageLoader() {
}
public synchronized void cancelProcess() {
cancel = true;
}
public synchronized boolean cancelRequested() {
return cancel;
}
@Override
public void run() {
for(int i = 0; i < fileList.size() && !cancelRequested(); i++) {
String file = fileList.get(i);
if(file == null) continue;
File f = new File(getFullPath(file));
if(f.exists() && !f.isDirectory()) {
try {
BufferedImage img = ImageIO.read(f);
while(!previewPanel.prepareImage(img, previewPanel)) {
Thread.yield();
if(cancelRequested()) return;
}
Dimension thumbsz = calculateNewSize(img.getWidth(), img.getHeight(), PREVIEW_WIDTH, PREVIEW_HEIGHT);
Dimension pvwsz = calculateNewSize(img.getWidth(), img.getHeight(), ICON_WIDTH, ICON_HEIGHT);
BufferedImage thumbimg = new BufferedImage(thumbsz.width, thumbsz.height, BufferedImage.TYPE_INT_ARGB);
BufferedImage pvwimg = new BufferedImage(pvwsz.width, pvwsz.height, BufferedImage.TYPE_INT_ARGB);
while(!thumbimg.getGraphics().drawImage(img, 0, 0, thumbsz.width, thumbsz.height, null)) {
Thread.yield();
if(cancelRequested()) return;
}
while(!pvwimg.getGraphics().drawImage(img, 0, 0, pvwsz.width, pvwsz.height, null)) {
Thread.yield();
if(cancelRequested()) return;
}
thumbs.put(file, thumbimg);
previews.put(file, pvwimg);
imgSizes.put(file, new Dimension(img.getWidth(), img.getHeight()));
SwingUtilities.invokeLater(new FileListRefresh(i));
if(itemsList.getSelectedIndex() != -1 && (itemsList.getSelectedValue().toString().contentEquals(fileList.get(i))))
updatePreview();
} catch (IOException e) {
e.printStackTrace();
}
}
if(cancelRequested()) return;
}
}
}
private class FileListRefresh implements Runnable {
private int index;
public FileListRefresh(int index) {
this.index = index;
}
@Override
public void run() {
int sel = itemsList.getSelectedIndex();
fileList.add(index, fileList.remove(index));
if(sel != -1)
itemsList.setSelectedIndex(sel);
}
};
private void wrapTextIntoLabel(JLabel lbl, String text, int width) {
AttributedString astr = new AttributedString(text);
astr.addAttribute(TextAttribute.FONT, lbl.getFont());
FontRenderContext frc = lbl.getFontMetrics(lbl.getFont()).getFontRenderContext();
LineBreakMeasurer lbm = new LineBreakMeasurer(astr.getIterator(), frc);
Vector<String> lines = new Vector<String>();
int prevOffs = 0;
int offs = lbm.nextOffset(width, text.length(), false);
do {
lines.add(text.substring(prevOffs, offs));
lbm.setPosition(offs);
prevOffs = offs;
offs = lbm.nextOffset(width, text.length(), false);
} while(offs > prevOffs);
StringBuilder output = new StringBuilder();
output.append("<html><center>");
for(int i = 0; i < lines.size(); i++) {
if(i > 0) output.append("<br/>");
output.append(lines.get(i));
}
output.append("</center></html>");
lbl.setText(output.toString());
}
private Dimension calculateNewSize(int srcw, int srch, int boxw, int boxh) {
if(srcw <= boxw && srch <= boxh) return new Dimension(srcw, srch);
Dimension rv = new Dimension(boxw, boxh);
float srca = (float)srcw / (float)srch;
float boxa = (float)boxw / (float)boxh;
if(srca > boxa) {
rv.height = (int)(((float)boxw / (float)srcw) * srch);
} else if(srca < boxa) {
rv.width = (int)(((float)boxh / (float)srch) * srcw);
}
return rv;
}
private String getFullPath(String file) {
String dir = currentDirField.getText();
dir += dir.endsWith(File.separator) ? "" : File.separator;
dir += file;
return dir;
}
/**
* General dialog action listener. Handles buttons and
* version combobox changes.
*/
public ActionListener actions = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
String cmd = e.getActionCommand();
if(cmd.equals("cancel")) {
setDialogResult(SongPicker.RESULT_CANCEL);
rootDialog.setVisible(false);
}
else if(cmd.equals("ok")) {
if(!isImmediate) {
setDialogResult(SongPicker.RESULT_OK);
rootDialog.setVisible(false);
} else {
showSelectedMedia();
}
}
else if(cmd.equals("browse")) {
browseForMediaFolder();
}
else {
System.out.println("FIXME: actions: Unhandled action: '" + cmd + "'");
}
}
};
/**
* Handles selection changes for the main list on the dialog
*/
public ListSelectionListener listSelection = new ListSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
if(!e.getValueIsAdjusting()) {
if(e.getSource() == itemsList) {
updatePreview();
setButtonsEnabled();
}
else {
System.out.println("MediaPicker: listSelection: unknown source");
}
}
}
};
public MouseListener listMouse = new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) { }
@Override
public void mouseEntered(MouseEvent e) { }
@Override
public void mouseExited(MouseEvent e) { }
@Override
public void mousePressed(MouseEvent e) {
if(e.getButton() == MouseEvent.BUTTON1) {
int idx = itemsList.locationToIndex(e.getPoint());
if(idx == -1) return;
Rectangle rt = itemsList.getCellBounds(idx, idx);
if(rt == null) return;
if(!rt.contains(e.getPoint())) {
itemsList.clearSelection();
e.consume();
}
}
}
@Override
public void mouseReleased(MouseEvent e) { }
};
private void browseForMediaFolder() {
String curdir = currentDirField.getText();
JFileChooser jfc = new JFileChooser(curdir);
jfc.setDialogTitle(I18n.get("ui/dialogs/mediapicker/folderbrowser?title"));
jfc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
jfc.setMultiSelectionEnabled(false);
if(jfc.showDialog(rootDialog, I18n.get("ui/dialogs/generic/select")) != JFileChooser.APPROVE_OPTION) return;
currentDirField.setText(jfc.getSelectedFile().getAbsolutePath());
fillMediaList();
}
private void fillMediaList() {
File dir = new File(currentDirField.getText());
fileList.clear();
previews.clear();
thumbs.clear();
if(!dir.exists() || !dir.isDirectory()) return;
String[] contents = dir.list();
Arrays.sort(contents);
Vector<String> exts = new Vector<String>();
if(allowImages) {
String[] imgExts = ImageIO.getReaderFileSuffixes();
for(int i = 0; i < imgExts.length; i++) {
exts.add(imgExts[i].toLowerCase());
}
}
if(allowVideos) {
// TODO add video extensions here
}
for(int i = 0; i < contents.length; i++) {
File f = new File(dir.getAbsolutePath() + (dir.getAbsolutePath().endsWith(File.separator) ? "" : File.separator) + contents[i]);
if(!f.exists() || f.isDirectory()) continue;
for(int j = 0; j < exts.size(); j++) {
if(contents[i].toLowerCase().endsWith(exts.get(j))) {
fileList.add(fileList.size(), contents[i]);
break;
}
}
}
if(curPreviewLoader != null)
curPreviewLoader.cancelProcess();
curPreviewLoader = new PreviewImageLoader();
new Thread(curPreviewLoader).start();
}
private void updatePreview() {
String img = (String)itemsList.getSelectedValue();
previewPanel.removeAll();
previewNameLabel.setText("");
previewSizeLabel.setText("");
if(img == null) return;
if(!thumbs.containsKey(img)) return;
previewPanel.add(new JLabel(new ImageIcon(thumbs.get(img))), BorderLayout.CENTER);
Dimension sz = imgSizes.get(img);
wrapTextIntoLabel(previewNameLabel, I18n.get("ui/dialogs/mediapicker/namelbl", img), previewPanel.getWidth() - 4);
wrapTextIntoLabel(previewSizeLabel, I18n.get("ui/dialogs/mediapicker/sizelbl", Integer.toString(sz.width) + "x" + Integer.toString(sz.height)), previewPanel.getWidth() - 4);
}
/**
* Called when the "display" button is clicked in immediate mode.
* Shows the currently selected media on the display/preview screens.
*/
private void showSelectedMedia() {
int idx = itemsList.getSelectedIndex();
ImageSlide imgslide = new ImageSlide();
if(idx == -1) {
Display.getDisplay().prepareNextBackground(ScreenRunner.currentStyle, imgslide.getStyleType());
Display.getDisplay().prepareNextSlide(null, 1, ScreenRunner.currentStyle);
} else {
Display.getDisplay().prepareNextBackground(ScreenRunner.currentStyle, imgslide.getStyleType());
imgslide.setUserData(getImageSlidePathString());
Display.getDisplay().prepareNextSlide(imgslide, 1, ScreenRunner.currentStyle);
}
Display.getDisplay().applyChanges();
//if(idx < (fileList.size() - 1)) itemsList.setSelectedIndex(idx + 1);
}
public String[] getSelectedPaths() {
Object[] items = itemsList.getSelectedValues();
String[] files = new String[items.length];
for(int i = 0; i < items.length; i++) {
files[i] = getFullPath(items[i].toString());
}
return files;
}
public String getImageSlidePathString() {
String[] paths = getSelectedPaths();
String rv = "";
for(int i = 0; i < paths.length; i++) {
rv += (i == 0 ? "" : File.pathSeparator) + paths[i];
}
return rv;
}
private void setButtonsEnabled() {
int idx = itemsList.getSelectedIndex();
chooseButton.setEnabled((idx != -1));
}
}