package org.limewire.ui.swing.properties;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
import net.miginfocom.swing.MigLayout;
import org.jdesktop.application.Resource;
import org.jdesktop.swingx.JXPanel;
import org.limewire.core.api.FilePropertyKey;
import org.limewire.core.api.library.LibraryManager;
import org.limewire.core.api.library.LocalFileItem;
import org.limewire.core.api.library.MagnetLinkFactory;
import org.limewire.core.api.library.PropertiableFile;
import org.limewire.ui.swing.action.AbstractAction;
import org.limewire.ui.swing.action.BitziLookupAction;
import org.limewire.ui.swing.components.HyperlinkButton;
import org.limewire.ui.swing.components.TextFieldClipboardControl;
import org.limewire.ui.swing.painter.factories.BarPainterFactory;
import org.limewire.ui.swing.properties.FileInfoDialog.FileInfoType;
import org.limewire.ui.swing.search.model.VisualSearchResult;
import org.limewire.ui.swing.search.resultpanel.CopyMagnetLinkToClipboardAction;
import org.limewire.ui.swing.util.CategoryIconManager;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.ui.swing.util.IconManager;
import org.limewire.ui.swing.util.ResizeUtils;
import org.limewire.util.CommonUtils;
import org.limewire.util.FileUtils;
import org.limewire.util.OSUtils;
import com.google.inject.Provider;
/**
* Displays general information about a PropertiableFile.
*/
class FileInfoOverviewPanel implements FileInfoPanel {
@Resource private Color foreground;
@Resource private Font smallFont;
@Resource private Font headerFont;
@Resource private Font headerFont2;
private final FileInfoType type;
private PropertiableFile propertiableFile;
private final MagnetLinkFactory magnetLinkFactory;
private final CategoryIconManager categoryIconManager;
private final LibraryManager libraryManager;
private final RenameAction renameAction;
private final JXPanel component;
private JButton renameButton;
private JTextField nameLabel;
private JTextField renameTextField;
public FileInfoOverviewPanel(FileInfoType type, PropertiableFile propertiableFile,
Provider<IconManager> iconManager, MagnetLinkFactory magnetLinkFactory,
CategoryIconManager categoryIconManager,
LibraryManager libraryManager,
BarPainterFactory barPainterFactory) {
this.type = type;
this.propertiableFile = propertiableFile;
this.magnetLinkFactory = magnetLinkFactory;
this.categoryIconManager = categoryIconManager;
this.libraryManager = libraryManager;
this.renameAction = new RenameAction();
GuiUtils.assignResources(this);
component = new JXPanel(new MigLayout("fill, gap 0, insets 10 10 16 14"));
component.setBackgroundPainter(barPainterFactory.createPopUpBarPainter());
init();
}
public JComponent getComponent() {
return component;
}
@Override
public boolean hasChanged() {
return false;
}
@Override
public void save() {
// if file is currently being renamed, attempt to save the changes
// before exiting
if(renameTextField != null && renameTextField.isVisible())
saveFileName();
}
@Override
public void dispose() {
//no listeners registered
}
@Override
public void updatePropertiableFile(PropertiableFile file) {
this.propertiableFile = file;
nameLabel.setText(propertiableFile.getFileName());
}
public void enableRename() {
renameAction.actionPerformed(null);
}
private void init() {
component.setOpaque(false);
addOverviewCategory();
HyperlinkButton copyToClipboard = null;
if(type == FileInfoType.LOCAL_FILE){
if(propertiableFile instanceof LocalFileItem && ((LocalFileItem)propertiableFile).isShareable()) {
copyToClipboard = new HyperlinkButton();
copyToClipboard.setFont(smallFont);
copyToClipboard.setAction(new AbstractAction(I18n.tr("Copy Magnet Link")) {
@Override
public void actionPerformed(ActionEvent e) {
StringSelection sel = new StringSelection(magnetLinkFactory.createMagnetLink((LocalFileItem)propertiableFile));
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(sel, sel);
}
});
}
} else if(type == FileInfoType.REMOTE_FILE) {
if(propertiableFile instanceof VisualSearchResult) {
copyToClipboard = new HyperlinkButton();
copyToClipboard.setFont(smallFont);
copyToClipboard.setAction(new CopyMagnetLinkToClipboardAction((VisualSearchResult)propertiableFile));
}
}
HyperlinkButton moreFileInfo = new HyperlinkButton(new BitziLookupAction(propertiableFile));
moreFileInfo.setFont(smallFont);
if(type == FileInfoType.LOCAL_FILE && propertiableFile instanceof LocalFileItem) {
renameButton = new HyperlinkButton(renameAction);
renameButton.setFont(smallFont);
component.add(renameButton, "cell 1 1, alignx right");
}
component.add(moreFileInfo, "cell 1 2, alignx right");
if(copyToClipboard != null)
component.add(copyToClipboard, "cell 1 3, alignx right");
}
/**
* Loads values for the Overview Panel. If a given field is null or
* doesn't exist, the field is ignored.
*/
private void addOverviewCategory() {
Icon icon = getIcon(propertiableFile);
JPanel iconDock = new JPanel();
iconDock.setOpaque(false);
iconDock.add(new JLabel(icon));
String title = propertiableFile.getPropertyString(FilePropertyKey.TITLE);
component.add(iconDock, "split, aligny top, growy, gapafter 0");
if (title != null) {
nameLabel = createLabelField(title);
}
else {
nameLabel = createLabelField(propertiableFile.getFileName());
}
nameLabel.setFont(headerFont);
nameLabel.setPreferredSize(new Dimension(440, 26));
component.add(nameLabel, "growx, span, wrap");
if (title != null) {
JTextField fileNameField = createLabelField(propertiableFile.getFileName());
fileNameField.setFont(headerFont2);
component.add(fileNameField, "gapbefore 4, growx, wrap");
}
String info = "";
if (!("".equals(FileInfoUtils.getFileSize(propertiableFile)))) {
info = FileInfoUtils.getFileSize(propertiableFile) + " (" + FileInfoUtils.getFileSizeBytes(propertiableFile)+ ")";
}
switch(propertiableFile.getCategory()) {
case AUDIO:
String time = FileInfoUtils.getLength(propertiableFile);
String bitrate = propertiableFile.getPropertyString(FilePropertyKey.BITRATE);
if(time != null) {
if (!("".equals(info))) {
info += ", ";
}
info += time;
}
if(bitrate != null) {
String quality = FileInfoUtils.getQuality(propertiableFile);
if (!("".equals(info))) {
info += ", ";
}
if(quality != null) {
info += bitrate + " kbps (" + quality + ")";
}
else {
info += bitrate + " kbps";
}
}
break;
}
if (!("".equals(info))) {
component.add(createLabelField(info), "gapbefore 4, growx, wrap");
}
}
private JTextField createLabelField(String text) {
JTextField field = new JTextField(text);
TextFieldClipboardControl.install(field);
field.setCaretPosition(0);
field.setEditable(false);
field.setOpaque(false);
field.setFont(smallFont);
field.setForeground(foreground);
field.setBorder(BorderFactory.createEmptyBorder(0,1,0,1));
return field;
}
/**
* Returns the appropriate Icon for the file type.
*/
private Icon getIcon(PropertiableFile propertiableFile) {
switch(type){
case LOCAL_FILE:
return categoryIconManager.getIcon(propertiableFile);
case DOWNLOADING_FILE:
case REMOTE_FILE:
default:
return categoryIconManager.getIcon(propertiableFile);
}
}
/**
* Creates a textfield used exclusively while renaming the file.
*/
private void createRenameTextField() {
renameTextField = new JTextField();
renameTextField.setFont(headerFont);
renameTextField.setForeground(foreground);
renameTextField.setVisible(false);
TextFieldClipboardControl.install(renameTextField);
// close when enter key is typed
renameTextField.addKeyListener(new KeyAdapter(){
@Override
public void keyTyped(KeyEvent e) {
if(e.getKeyChar() == KeyEvent.VK_ENTER) {
enableRenameMode(false);
}
}
});
// close when mouse is clicked somewhere else
component.addMouseListener(new MouseAdapter(){
@Override
public void mouseClicked(MouseEvent e) {
enableRenameMode(false);
}
});
component.add(renameTextField, "hidemode 3, pos " + (nameLabel.getLocation().x-3) + " " + (nameLabel.getLocation().y-3));
component.revalidate();
}
/**
* Updates components and saves states depending on whether
* rename mode is activated or deactivated.
*/
private void enableRenameMode(boolean enable) {
if(enable) {
setRenameText();
setRenameEnabled(true);
renameTextField.requestFocusInWindow();
renameTextField.selectAll();
} else {
saveFileName();
setRenameEnabled(false);
component.repaint();
}
}
/**
* Updates the text of the editable renameTextField. Once the text is updated,
* the editable textfield is made visible, allowing the user to change the
* filename.
*/
private void setRenameText() {
if(propertiableFile instanceof LocalFileItem) {
int maxFileLength = getMaxFileSize(((LocalFileItem)propertiableFile).getFile());
renameTextField.setDocument(new FileNameDocument(maxFileLength));
}
String fileName = FileUtils.getFilenameNoExtension(nameLabel.getText());
renameTextField.setText(fileName);
ResizeUtils.forceWidth(renameTextField, nameLabel.getWidth());
}
/**
* Updates components that change when in edit mode.
*
* @isRenaming is true when the mode will be switched to
* edit mode, false when mode is switched out of edit mode.
*/
private void setRenameEnabled(boolean isRenaming) {
renameTextField.setVisible(isRenaming);
nameLabel.setVisible(!isRenaming);
renameButton.setEnabled(!isRenaming);
}
/**
* Attempts to save the filename changes to disk.
*/
private void saveFileName() {
String newFileName = renameTextField.getText().trim();
LocalFileItem oldFileItem = (LocalFileItem) propertiableFile;
// check the new file name is valid
if(!isValidFileName(newFileName)) {
renameTextField.setText(oldFileItem.getName());
return;
}
// check if the name hasn't changed, just return
if(newFileName.equals(oldFileItem.getName())) {
return;
}
File oldFile = oldFileItem.getFile();
newFileName = newFileName + "." + FileUtils.getFileExtension(oldFile);
File newFile = new File(oldFile.getParentFile(), newFileName);
// try performing the file rename, if something goes wrong, revert textfield.
if(FileUtils.forceRename(oldFile, newFile)) {
updateFileNameInLibrary(oldFile, newFile);
} else {
newFile.delete();
renameTextField.setText(oldFileItem.getName());
}
}
/**
* Notifies the library that the fileName has been changed.
*/
private void updateFileNameInLibrary(File oldFile, File newFile) {
libraryManager.getLibraryManagedList().fileRenamed(oldFile, newFile);
}
/**
* Returns true if the text is a valid file name.
*/
private boolean isValidFileName(String fileName) {
return fileName != null && fileName.length() > 0 && CommonUtils.santizeString(fileName).equals(fileName);
}
/**
* Handles renaming of the file name. This is handled using
* a popup menu to get the support of loosing focus and
* properlly disappearing.
*/
private class RenameAction extends AbstractAction {
public RenameAction() {
super(I18n.tr("Rename File"));
}
@Override
public void actionPerformed(ActionEvent e) {
if(renameTextField == null)
createRenameTextField();
enableRenameMode(true);
}
}
/**
* Returns the maximum length the filename can be.
*/
private int getMaxFileSize(File file) {
String fileName = FileUtils.getFilenameNoExtension(file.getName());
return OSUtils.getMaxPathLength() - file.getAbsolutePath().length() + fileName.length();
}
/**
* Prevents a TextField from having a String beyond a certain length. Also
* prevents non-valid filename characters from being entered.
*/
private class FileNameDocument extends PlainDocument {
private int maxLength = Integer.MAX_VALUE;
public FileNameDocument(int length) {
this.maxLength = length;
}
@Override
public void insertString(int offset, String str, AttributeSet attr) throws BadLocationException {
if(str == null)
return;
String santize = CommonUtils.santizeString(str);
// only add the string if it contains valid characters and is below the max length
if(santize.equals(str) && getLength() + str.length() <= maxLength) {
super.insertString(offset, str, attr);
} else {
Toolkit.getDefaultToolkit().beep();
}
}
}
}