package org.rr.jeborker.gui.cell;
import static org.rr.commons.utils.StringUtil.EMPTY;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.AbstractCellEditor;
import javax.swing.Action;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.text.NavigationFilter;
import javax.swing.text.Position;
import javax.swing.tree.TreeCellEditor;
import org.apache.commons.io.FilenameUtils;
import org.rr.commons.collection.LRUCacheMap;
import org.rr.commons.swing.SwingUtils;
import org.rr.commons.swing.components.button.JMediumWeightPopupMenu;
import org.rr.commons.swing.components.resources.ImageResourceBundle;
import org.rr.commons.utils.ListUtils;
import org.rr.commons.utils.StringUtil;
import org.rr.commons.utils.UtilConstants;
public class FileSystemRenameTreeCellEditor extends AbstractCellEditor implements TreeCellEditor {
private static final ImageIcon ARROW_IMAGE = ImageResourceBundle.getResourceAsImageIcon("arrow.gif");
/**
* The Swing component being edited.
*/
protected JPanel editorComponent;
private JButton arrowButton;
private JTextField textField;
private LRUCacheMap<String, List<String>> latestFileNameOffers = new LRUCacheMap<String, List<String>>(3);
/**
* A wrapper for the actions to be displayed when the pop-up is drawn.
*/
private class ActionOptionWrapper extends AbstractAction {
private static final long serialVersionUID = -7161990723874074407L;
private Action internalAction = null;
/**
* Creates a new instance of a wrapper action
*
* @param wrapAction
* The action to be wrapped
*/
public ActionOptionWrapper(Action wrapAction) {
internalAction = wrapAction;
this.setEnabled(wrapAction.isEnabled());
putValue(Action.NAME, wrapAction.getValue(Action.NAME));
putValue(Action.SMALL_ICON, wrapAction.getValue(Action.SMALL_ICON));
}
/**
* Fired when the action is performed
*
* @param actionEvent
* The action event
*/
public void actionPerformed(ActionEvent actionEvent) {
internalAction.actionPerformed(actionEvent);
}
}
/**
* NavigationFilter for the text field that did not allow to navigate behind the
* file extension.
*/
private class RenameFieldNavigationFilter extends NavigationFilter {
public void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias) {
int dotIdx = textField.getText().lastIndexOf('.');
if (dotIdx >= 0 && dot >= dotIdx) {
fb.setDot(dotIdx, bias);
} else {
fb.setDot(dot, bias);
}
}
public void moveDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias) {
int dotIdx = textField.getText().lastIndexOf('.');
if (dotIdx >= 0 && dot >= dotIdx) {
fb.moveDot(dotIdx, bias);
} else {
fb.moveDot(dot, bias);
}
}
}
/**
* Constructs a <code>DefaultCellEditor</code> object that uses a combo box.
*
* @param comboBox
* a <code>JComboBox</code> object
*/
protected FileSystemRenameTreeCellEditor() {
createEditorComponent();
}
private JPanel createEditorComponent() {
editorComponent = new JPanel();
editorComponent.setLayout(new BorderLayout());
textField = new JTextField(EMPTY, 9) {
@Override
public void selectAll() {
String text = getText();
if(text.lastIndexOf('.') != -1) {
select(0, text.lastIndexOf('.'));
} else {
super.selectAll();
}
}
};
textField.setNavigationFilter(new RenameFieldNavigationFilter());
textField.setBorder(null);
textField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_DOWN) {
e.consume();
//open menu on key down
arrowButton.getAction().actionPerformed(null);
} else if(e.getKeyCode() == KeyEvent.VK_ENTER) {
stopCellEditing();
}
}
});
editorComponent.add(textField, BorderLayout.CENTER);
arrowButton = new JButton();
arrowButton.setAction(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent ev) {
final Component focusOwner = SwingUtils.getSurroundingComponent(editorComponent, JTree.class);
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
JPopupMenu popup = new JMediumWeightPopupMenu();
popup.setLightWeightPopupEnabled(false);
List<String> offers = getCommaChangeOffers(textField.getText());
for (final String offer : offers) {
AbstractAction action = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
textField.setText(offer);
stopCellEditing();
if(focusOwner != null) {
focusOwner.requestFocus();
}
}
};
action.putValue(Action.NAME, offer);
popup.add(new ActionOptionWrapper(action));
}
popup.setMinimumSize(new Dimension(getEditorWidth(textField.getText()), offers.size() * 25));
popup.show(editorComponent, 0, editorComponent.getHeight());
}
});
arrowButton.setIcon(ARROW_IMAGE);
editorComponent.add(arrowButton, BorderLayout.EAST);
return editorComponent;
}
/**
* Returns a reference to the editor component.
*
* @return the editor <code>Component</code>
*/
public Component getComponent() {
return editorComponent;
}
/**
* Get the current cell editor value.
*/
public Object getCellEditorValue() {
return textField.getText();
}
/**
* Returns true if <code>anEvent</code> is <b>not</b> a <code>MouseEvent</code>. Otherwise, it returns true if the necessary number of clicks have
* occurred, and returns false otherwise.
*
* @param anEvent
* the event
* @return true if cell is ready for editing, false otherwise
* @see #setClickCountToStart
* @see #shouldSelectCell
*/
public boolean isCellEditable(EventObject anEvent) {
if (anEvent == null || anEvent instanceof KeyEvent) {
return true;
}
return true;
}
/**
* Returns true to indicate that the editing cell may be selected.
*
* @param anEvent
* the event
* @return true
* @see #isCellEditable
*/
public boolean shouldSelectCell(EventObject anEvent) {
if (anEvent instanceof MouseEvent) {
MouseEvent e = (MouseEvent) anEvent;
return e.getID() != MouseEvent.MOUSE_DRAGGED;
}
return true;
}
/**
* Stops editing and returns true to indicate that editing has stopped. This method calls <code>fireEditingStopped</code>.
*
* @return true
*/
public boolean stopCellEditing() {
fireEditingStopped();
return true;
}
/**
* Cancels editing. This method calls <code>fireEditingCanceled</code>.
*/
public void cancelCellEditing() {
fireEditingCanceled();
}
/**
* Implements the <code>TreeCellEditor</code> interface.
*/
public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row) {
final String stringValue = tree.convertValueToText(value, isSelected, expanded, leaf, row, false);
int width = getEditorWidth(stringValue);
int height = editorComponent.getPreferredSize().height;
editorComponent.setPreferredSize(new Dimension(width, height));
textField.setText(stringValue);
if(getCommaChangeOffers(stringValue).isEmpty()) {
arrowButton.setVisible(false);
} else {
arrowButton.setVisible(true);
}
return editorComponent;
}
private int getEditorWidth(String stringValue) {
Dimension textDimension = SwingUtils.getTextDimension(stringValue, textField.getFont());
int width = textDimension.width + arrowButton.getPreferredSize().width;
return width;
}
/**
* Get some change offers for the given file name.
*/
private List<String> getCommaChangeOffers(final String filename) {
List<String> cachedOffer = latestFileNameOffers.get(filename);
if(cachedOffer != null) {
return cachedOffer;
}
List<String> offers = new ArrayList<String>() {
@Override
public boolean add(String e) {
String clean = clean(e);
return super.add(clean);
}
@Override
public boolean remove(Object o) {
String clean = clean((String) o);
return super.remove(clean);
}
/**
* Clean up the given string.
*/
private String clean(String s) {
String result = s;
result = StringUtil.replace(s, " ", EMPTY);
result = result.trim();
return result;
}
};
String name = FilenameUtils.getBaseName(filename);
String ext = FilenameUtils.getExtension(filename);
if(name.indexOf(',') != -1) {
offers.add(name.replaceAll("([\\w\\.]*),\\s{0,1}([\\w\\.]*)", "$2 $1").trim() + "." + ext);
offers.add(name.replaceAll("([\\w\\.]*\\s[\\w\\.]*),\\s{0,1}([\\w\\.]*)", "$2 $1").trim() + "." + ext);
offers.add(name.replaceAll("([\\w\\.]*\\s[\\w\\.]*),\\s{0,1}([\\w\\.]*\\s\\w*)", "$2 $1").trim() + "." + ext);
offers.add(name.replaceAll("([\\w\\.]*),\\s{0,1}([\\w\\.]*)\\s([\\w\\.]{1,})", "$2 $3 $1").trim() + "." + ext);
}
offers = ListUtils.distinct(offers);
List<String> toRemove = new ArrayList<>();
toRemove.add(filename);
int nameWhiteSpaces = StringUtil.occurrence(name, " ", UtilConstants.COMPARE_BINARY);
for(String offer : offers) {
if(StringUtil.occurrence(offer, " ", UtilConstants.COMPARE_BINARY) != nameWhiteSpaces) {
toRemove.add(offer);
}
}
offers.removeAll(toRemove);
List<String> toAdd = new ArrayList<>(offers.size());
for(int i = 0; i < offers.size(); i++) {
String offer = offers.get(i);
if(offer.contains(" - ")) {
List<String> split = ListUtils.split(offer, " - ");
if(split.size() == 2) {
String sName = FilenameUtils.getBaseName(split.get(1));
String sExt = FilenameUtils.getExtension(split.get(1));
toAdd.add(sName + " - " + split.get(0) + "." + sExt);
}
}
}
offers.addAll(toAdd);
latestFileNameOffers.put(filename, offers);
return offers;
}
}