View Javadoc
1   /*******************************************************************************
2    * Copyhacked (H) 2012-2025.
3    * This program and the accompanying materials
4    * are made available under no term at all, use it like
5    * you want, but share and discuss it
6    * every time possible with every body.
7    * 
8    * Contributors:
9    *      ron190 at ymail dot com - initial implementation
10   ******************************************************************************/
11  package com.jsql.view.swing.list;
12  
13  import com.jsql.util.I18nUtil;
14  import com.jsql.util.LogLevelUtil;
15  import org.apache.commons.io.FilenameUtils;
16  import org.apache.commons.lang3.StringUtils;
17  import org.apache.logging.log4j.LogManager;
18  import org.apache.logging.log4j.Logger;
19  
20  import javax.swing.*;
21  import java.awt.event.*;
22  import java.io.BufferedReader;
23  import java.io.File;
24  import java.io.FileReader;
25  import java.io.IOException;
26  import java.nio.charset.StandardCharsets;
27  import java.util.ArrayList;
28  import java.util.List;
29  
30  /**
31   * A list supporting drag and drop.
32   */
33  public class DnDList extends JList<ItemList> {
34      
35      private static final Logger LOGGER = LogManager.getRootLogger();
36      
37      /**
38       * Model for the JList.
39       */
40      protected final DefaultListModel<ItemList> listModel;
41      
42      /**
43       * List of default items.
44       */
45      private final transient List<ItemList> defaultList;
46      
47      /**
48       * Create a JList decorated with drag/drop features.
49       * @param newList List to decorate
50       */
51      public DnDList(List<ItemList> newList) {
52          this.defaultList = newList;
53          this.listModel = new DefaultListModel<>();
54  
55          for (ItemList path: newList) {
56              this.listModel.addElement(path);
57          }
58  
59          this.setModel(this.listModel);
60          this.initActionMap();
61          this.initListener();
62          this.setDragEnabled(true);
63          this.setDropMode(DropMode.INSERT);
64          this.setTransferHandler(new ListTransfertHandler());  // Set Drag and Drop
65      }
66  
67      private void initListener() {
68          this.addMouseListener(new MouseAdapterMenuAction(this));
69          this.addFocusListener(new FocusListener() {  // Allows color change when list loses/gains focus
70              @Override
71              public void focusLost(FocusEvent focusEvent) {
72                  DnDList.this.repaint();
73              }
74              @Override
75              public void focusGained(FocusEvent focusEvent) {
76                  DnDList.this.repaint();
77              }
78          });
79          this.addKeyListener(new KeyAdapter() {  // Allows deleting values
80              @Override
81              public void keyPressed(KeyEvent keyEvent) {
82                  if (keyEvent.getKeyCode() == KeyEvent.VK_DELETE) {
83                      DnDList.this.removeSelectedItem();
84                  }
85              }
86          });
87      }
88  
89      private void initActionMap() {
90          var listActionMap = this.getActionMap();  // Transform Cut, selects next value
91          listActionMap.put(TransferHandler.getCutAction().getValue(Action.NAME), new AbstractAction() {
92              @Override
93              public void actionPerformed(ActionEvent e) {
94                  if (DnDList.this.getSelectedValuesList().isEmpty()) {
95                      return;
96                  }
97                  
98                  List<ItemList> selectedValues = DnDList.this.getSelectedValuesList();
99                  List<ItemList> siblings = new ArrayList<>();
100                 for (ItemList value: selectedValues) {
101                     int valueIndex = DnDList.this.listModel.indexOf(value);
102                     if (valueIndex < DnDList.this.listModel.size() - 1) {
103                         siblings.add(DnDList.this.listModel.get(valueIndex + 1));
104                     } else if (valueIndex > 0) {
105                         siblings.add(DnDList.this.listModel.get(valueIndex - 1));
106                     }
107                 }
108 
109                 TransferHandler.getCutAction().actionPerformed(e);
110                 
111                 for (ItemList sibling: siblings) {
112                     DnDList.this.setSelectedValue(sibling, true);
113                 }
114             }
115         });
116 
117         listActionMap.put(
118             TransferHandler.getCopyAction().getValue(Action.NAME),
119             TransferHandler.getCopyAction()
120         );
121         listActionMap.put(
122             TransferHandler.getPasteAction().getValue(Action.NAME),
123             TransferHandler.getPasteAction()
124         );
125     }
126 
127     /**
128      * Delete selected items from the list.
129      */
130     public void removeSelectedItem() {
131         if (this.getSelectedValuesList().isEmpty()) {
132             return;
133         }
134 
135         List<ItemList> selectedValues = this.getSelectedValuesList();
136         for (ItemList itemSelected: selectedValues) {
137             int indexOfItemSelected = this.listModel.indexOf(itemSelected);
138             this.listModel.removeElement(itemSelected);
139             if (indexOfItemSelected == this.listModel.getSize()) {
140                 this.setSelectedIndex(indexOfItemSelected - 1);
141             } else {
142                 this.setSelectedIndex(indexOfItemSelected);
143             }
144         }
145         
146         try {
147             var rectangle = this.getCellBounds(
148                 this.getMinSelectionIndex(),
149                 this.getMaxSelectionIndex()
150             );
151             if (rectangle != null) {
152                 this.scrollRectToVisible(
153                     this.getCellBounds(
154                         this.getMinSelectionIndex(),
155                         this.getMaxSelectionIndex()
156                     )
157                 );
158             }
159         } catch (NullPointerException e) {
160             // Report NullPointerException #1571 : manual scroll elsewhere then run action
161             LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
162         }
163     }
164 
165     /**
166      * Load a file into the list (drag/drop or copy/paste).
167      */
168     public void dropPasteFile(final List<File> filesToImport, int position) {
169         if (filesToImport.isEmpty()) {
170             return;
171         }
172         
173         for (File fileToImport : filesToImport) {
174             // Report NoSuchMethodError #1617
175             if (
176                 !FilenameUtils
177                 .getExtension(fileToImport.getPath())
178                 .matches("txt|csv|ini")
179             ) {
180                 // Fix #42832: ClassCastException on showMessageDialog()
181                 try {
182                     JOptionPane.showMessageDialog(
183                         this.getTopLevelAncestor(),
184                         I18nUtil.valueByKey("LIST_IMPORT_ERROR_LABEL"),
185                         I18nUtil.valueByKey("LIST_IMPORT_ERROR_TITLE"),
186                         JOptionPane.ERROR_MESSAGE
187                     );
188                 } catch (ClassCastException e) {
189                     LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e.getMessage(), e);
190                 }
191                 return;
192             }
193         }
194 
195         var options = new String[] {
196             I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_REPLACE"),
197             I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_ADD"),
198             I18nUtil.valueByKey("LIST_ADD_VALUE_CANCEL")
199         };
200         int answer = JOptionPane.showOptionDialog(
201             this.getTopLevelAncestor(),
202             I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_LABEL"),
203             I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_TITLE"),
204             JOptionPane.YES_NO_CANCEL_OPTION,
205             JOptionPane.QUESTION_MESSAGE,
206             null,
207             options,
208             options[2]
209         );
210         if (answer != JOptionPane.YES_OPTION && answer != JOptionPane.NO_OPTION) {
211             return;
212         }
213 
214         int startPosition = position;
215         if (answer == JOptionPane.YES_OPTION) {
216             this.listModel.clear();
217             startPosition = 0;
218         }
219         int startPositionFinal = startPosition;
220         SwingUtilities.invokeLater(() -> this.addItems(filesToImport, startPositionFinal));
221     }
222 
223     private void addItems(final List<File> filesToImport, int startPosition) {
224         int endPosition = startPosition;
225         for (File file: filesToImport) {
226             endPosition = this.initItems(endPosition, file);
227         }
228         if (!this.listModel.isEmpty()) {
229             this.setSelectionInterval(startPosition, endPosition - 1);
230         }
231         
232         try {
233             this.scrollRectToVisible(
234                 this.getCellBounds(
235                     this.getMinSelectionIndex(),
236                     this.getMaxSelectionIndex()
237                 )
238             );
239         } catch (NullPointerException e) {
240             // Report NullPointerException #1571 : manual scroll elsewhere then run action
241             LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e.getMessage(), e);
242         }
243     }
244 
245     private int initItems(int startPosition, File file) {
246         int endPosition = startPosition;
247         
248         try (
249             var fileReader = new FileReader(file, StandardCharsets.UTF_8);
250             var bufferedReader = new BufferedReader(fileReader)
251         ) {
252             String line;
253             while ((line = bufferedReader.readLine()) != null) {
254                 if (
255                     StringUtils.isNotEmpty(line)
256                     // Fix Report #60
257                     && 0 <= endPosition
258                     && endPosition <= this.listModel.size()
259                 ) {
260                     this.addItem(endPosition++, line);
261                 }
262             }
263         } catch (IOException e) {
264             LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e.getMessage(), e);
265         }
266         
267         return endPosition;
268     }
269     
270     public void restore() {
271         this.listModel.clear();
272         for (ItemList path: this.defaultList) {
273             this.listModel.addElement(path);
274         }
275     }
276     
277     public void addItem(int endPosition, String line) {
278         this.listModel.add(endPosition, new ItemList(line.replace("\\", "/")));
279     }
280 }