DnDList.java

/*******************************************************************************
 * Copyhacked (H) 2012-2020.
 * This program and the accompanying materials
 * are made available under no term at all, use it like
 * you want, but share and discuss about it
 * every time possible with every body.
 * 
 * Contributors:
 *      ron190 at ymail dot com - initial implementation
 ******************************************************************************/
package com.jsql.view.swing.list;

import com.jsql.util.I18nUtil;
import com.jsql.util.LogLevelUtil;
import com.jsql.view.swing.util.UiUtil;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.*;
import java.awt.event.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * A list supporting drag and drop.
 */
public class DnDList extends JList<ItemList> {
    
    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();
    
    /**
     * Model for the JList.
     */
    protected final DefaultListModel<ItemList> listModel;
    
    /**
     * List of default items.
     */
    private final transient List<ItemList> defaultList;
    
    /**
     * Create a JList decorated with drag/drop features.
     * @param newList List to decorate
     */
    public DnDList(List<ItemList> newList) {
        
        this.defaultList = newList;

        this.listModel = new DefaultListModel<>();

        for (ItemList path: newList) {
            this.listModel.addElement(path);
        }

        this.setModel(this.listModel);

        ListCellRenderer<ItemList> renderer = new RendererComplexCell();
        this.setCellRenderer(renderer);
        
        this.initializeActionMap();
        this.initializeListener();
        
        this.setDragEnabled(true);
        this.setDropMode(DropMode.INSERT);

        // Set Drag and Drop
        this.setTransferHandler(new ListTransfertHandler());
    }

    private void initializeListener() {
        
        this.addMouseListener(new MouseAdapterMenuAction(this));

        // Allows color change when list loses/gains focus
        this.addFocusListener(new FocusListener() {
                
            @Override
            public void focusLost(FocusEvent arg0) {
                DnDList.this.repaint();
            }
            
            @Override
            public void focusGained(FocusEvent arg0) {
                DnDList.this.repaint();
            }
        });
        
        // Allows deleting values
        this.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent arg0) {
                if (arg0.getKeyCode() == KeyEvent.VK_DELETE) {
                    DnDList.this.removeSelectedItem();
                }
            }
        });
    }

    private void initializeActionMap() {
        
        // Transform Cut, selects next value
        var listActionMap = this.getActionMap();
        
        listActionMap.put(TransferHandler.getCutAction().getValue(Action.NAME), new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                
                if (DnDList.this.getSelectedValuesList().isEmpty()) {
                    return;
                }
                
                List<ItemList> selectedValues = DnDList.this.getSelectedValuesList();
                List<ItemList> siblings = new ArrayList<>();
                
                for (ItemList value: selectedValues) {
                    
                    int valueIndex = DnDList.this.listModel.indexOf(value);

                    if (valueIndex < DnDList.this.listModel.size() - 1) {
                        siblings.add(DnDList.this.listModel.get(valueIndex + 1));
                    } else if (valueIndex > 0) {
                        siblings.add(DnDList.this.listModel.get(valueIndex - 1));
                    }
                }

                TransferHandler.getCutAction().actionPerformed(e);
                
                for (ItemList sibling: siblings) {
                    DnDList.this.setSelectedValue(sibling, true);
                }
            }
        });

        listActionMap.put(
            TransferHandler.getCopyAction().getValue(Action.NAME),
            TransferHandler.getCopyAction()
        );
        
        listActionMap.put(
            TransferHandler.getPasteAction().getValue(Action.NAME),
            TransferHandler.getPasteAction()
        );
    }

    /**
     * Delete selected items from the list.
     */
    public void removeSelectedItem() {
        
        if (this.getSelectedValuesList().isEmpty()) {
            return;
        }

        List<ItemList> selectedValues = this.getSelectedValuesList();
        
        for (ItemList itemSelected: selectedValues) {
            
            int indexOfItemSelected = this.listModel.indexOf(itemSelected);
            this.listModel.removeElement(itemSelected);
            
            if (indexOfItemSelected == this.listModel.getSize()) {
                this.setSelectedIndex(indexOfItemSelected - 1);
            } else {
                this.setSelectedIndex(indexOfItemSelected);
            }
        }
        
        try {
            var rectangle = this.getCellBounds(
                this.getMinSelectionIndex(),
                this.getMaxSelectionIndex()
            );
            
            if (rectangle != null) {
                this.scrollRectToVisible(
                    this.getCellBounds(
                        this.getMinSelectionIndex(),
                        this.getMaxSelectionIndex()
                    )
                );
            }
        } catch (NullPointerException e) {
            // Report NullPointerException #1571 : manual scroll elsewhere then run action
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
        }
    }

    /**
     * Load a file into the list (drag/drop or copy/paste).
     * @param filesToImport
     * @param position
     */
    public void dropPasteFile(final List<File> filesToImport, int position) {
        
        if (filesToImport.isEmpty()) {
            return;
        }
        
        for (File fileToImport : filesToImport) {
            
            // Report NoSuchMethodError #1617
            if (
                !FilenameUtils
                .getExtension(fileToImport.getPath())
                .matches("txt|csv|ini")
            ) {
                
                // Fix #42832: ClassCastException on showMessageDialog()
                try {
                    JOptionPane.showMessageDialog(
                        this.getTopLevelAncestor(),
                        I18nUtil.valueByKey("LIST_IMPORT_ERROR_LABEL"),
                        I18nUtil.valueByKey("LIST_IMPORT_ERROR_TITLE"),
                        JOptionPane.ERROR_MESSAGE,
                        UiUtil.ICON_ERROR
                    );
                } catch (ClassCastException e) {
                    LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e.getMessage(), e);
                }
                
                return;
            }
        }

        var options = new String[] {
            I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_REPLACE"),
            I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_ADD"),
            I18nUtil.valueByKey("LIST_ADD_VALUE_CANCEL")
        };
        
        int answer = JOptionPane.showOptionDialog(
            this.getTopLevelAncestor(),
            I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_LABEL"),
            I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_TITLE"),
            JOptionPane.YES_NO_CANCEL_OPTION,
            JOptionPane.QUESTION_MESSAGE,
            null,
            options,
            options[2]
        );
        
        int startPosition = position;

        if (answer != JOptionPane.YES_OPTION && answer != JOptionPane.NO_OPTION) {
            return;
        }
        
        if (answer == JOptionPane.YES_OPTION) {
            
            this.listModel.clear();
            startPosition = 0;
        }
        
        int startPositionFinal = startPosition;
        
        SwingUtilities.invokeLater(() -> this.addItems(filesToImport, startPositionFinal));
    }

    private void addItems(final List<File> filesToImport, int startPosition) {
        
        int endPosition = startPosition;
        
        for (File file: filesToImport) {
            endPosition = this.initializeItems(endPosition, file);
        }

        if (!this.listModel.isEmpty()) {
            DnDList.this.setSelectionInterval(startPosition, endPosition - 1);
        }
        
        try {
            DnDList.this.scrollRectToVisible(
                DnDList.this.getCellBounds(
                    DnDList.this.getMinSelectionIndex(),
                    DnDList.this.getMaxSelectionIndex()
                )
            );
        } catch (NullPointerException e) {
            // Report NullPointerException #1571 : manual scroll elsewhere then run action
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e.getMessage(), e);
        }
    }

    private int initializeItems(int startPosition, File file) {
        
        int endPosition = startPosition;
        
        try (
            var fileReader = new FileReader(file, StandardCharsets.UTF_8);
            var bufferedReader = new BufferedReader(fileReader)
        ) {
            
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                if (
                    StringUtils.isNotEmpty(line)
                    // Fix Report #60
                    && 0 <= endPosition
                    && endPosition <= this.listModel.size()
                ) {
                    this.addItem(endPosition++, line);
                }
            }
        } catch (IOException e) {
            LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e.getMessage(), e);
        }
        
        return endPosition;
    }
    
    public void restore() {
        
        this.listModel.clear();
        
        for (ItemList path: this.defaultList) {
            this.listModel.addElement(path);
        }
    }
    
    public void addItem(int endPosition, String line) {
        this.listModel.add(endPosition, new ItemList(line.replace("\\", "/")));
    }
}