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.lang3.StringUtils;
16  import org.apache.logging.log4j.LogManager;
17  import org.apache.logging.log4j.Logger;
18  
19  import javax.swing.*;
20  import java.awt.event.*;
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.FileReader;
24  import java.io.IOException;
25  import java.nio.charset.StandardCharsets;
26  import java.nio.file.Files;
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) throws IOException {
169         if (filesToImport.isEmpty()) {
170             return;
171         }
172         
173         for (File fileToImport : filesToImport) {
174             // Report NoSuchMethodError #1617
175             String type = Files.probeContentType(fileToImport.toPath());
176             if (type != null && !type.startsWith("text")) {
177                 // Fix #42832: ClassCastException on showMessageDialog()
178                 try {
179                     JOptionPane.showMessageDialog(
180                         this.getTopLevelAncestor(),
181                         I18nUtil.valueByKey("LIST_IMPORT_ERROR_LABEL"),
182                         I18nUtil.valueByKey("LIST_IMPORT_ERROR_TITLE"),
183                         JOptionPane.ERROR_MESSAGE
184                     );
185                 } catch (ClassCastException e) {
186                     LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
187                 }
188                 return;
189             }
190         }
191 
192         int answer = JOptionPane.YES_OPTION;
193         if (!this.listModel.isEmpty()) {
194             var options = new String[]{
195                 I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_REPLACE"),
196                 I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_ADD"),
197                 I18nUtil.valueByKey("LIST_ADD_VALUE_CANCEL")
198             };
199             answer = JOptionPane.showOptionDialog(
200                 this.getTopLevelAncestor(),
201                 I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_LABEL"),
202                 I18nUtil.valueByKey("LIST_IMPORT_CONFIRM_TITLE"),
203                 JOptionPane.YES_NO_CANCEL_OPTION,
204                 JOptionPane.QUESTION_MESSAGE,
205                 null,
206                 options,
207                 options[2]
208             );
209             if (answer != JOptionPane.YES_OPTION && answer != JOptionPane.NO_OPTION) {
210                 return;
211             }
212         }
213 
214         int startPosition = Math.max(position, 0);  // can be -1 when empty JList
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, 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 && endPosition <= this.listModel.size()
258                 ) {
259                     this.addItem(endPosition++, line);
260                 }
261             }
262         } catch (IOException e) {
263             LOGGER.log(LogLevelUtil.CONSOLE_JAVA, e, e);
264         }
265         
266         return endPosition;
267     }
268     
269     public void restore() {
270         this.listModel.clear();
271         for (ItemList path: this.defaultList) {
272             this.listModel.addElement(path);
273         }
274     }
275     
276     public void addItem(int endPosition, String line) {
277         this.listModel.add(endPosition, new ItemList(line.replace("\\", "/")));
278     }
279 }