001package jmri.util.swing;
002
003import java.awt.Desktop;
004import java.awt.event.ActionEvent;
005import java.awt.image.BufferedImage;
006import java.io.BufferedInputStream;
007import java.io.File;
008import java.io.FileOutputStream;
009import java.io.IOException;
010import java.net.HttpURLConnection;
011import java.util.Arrays;
012import java.util.UUID;
013
014import javax.imageio.ImageIO;
015import javax.swing.*;
016
017import jmri.util.FileUtil;
018import jmri.util.iharder.dnd.URIDrop;
019
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023public class EditableResizableImagePanel extends ResizableImagePanel implements URIDrop.ListenerExt {
024
025    private transient MyMouseAdapter myMouseAdapter = null;
026    private String dropFolder;
027
028    /**
029     * Default constructor.
030     *
031     */
032    public EditableResizableImagePanel() {
033        super();
034        setDnd(true);
035    }
036
037    /**
038     * Constructor with initial image file path as parameter. Component will be
039     * (preferred) sized to image sized
040     *
041     * @param imagePath Path to image to display
042     */
043    public EditableResizableImagePanel(String imagePath) {
044        super(imagePath);
045        setDnd(true);
046    }
047
048    /**
049     * Constructor for DnDImagePanel with forced initial size
050     *
051     * @param imagePath Path to image to display
052     * @param w         Panel width
053     * @param h         Panel height
054     */
055    public EditableResizableImagePanel(String imagePath, int w, int h) {
056        super(imagePath, w, h);
057        setDnd(true);
058    }
059    
060    /**
061     * Cleanup the DnD from this component
062     *
063     */
064    public void removeDnd() {
065        URIDrop.remove(this);
066    }
067
068    /**
069     * Enable or disable drag'n drop, dropped files will be copied in latest
070     * used image path top folder when dnd enabled, also enable contextual menu
071     * with remove entry.
072     *
073     * @param dnd true to enable, false to disable
074     */
075    public final void setDnd(boolean dnd) {
076        if (dnd) {
077            new URIDrop(this, this);
078            if (myMouseAdapter == null) {
079                myMouseAdapter = new MyMouseAdapter(this);
080            }
081            addMouseListener(JmriMouseListener.adapt(myMouseAdapter));
082        } else {
083            URIDrop.remove(this);
084            if (myMouseAdapter != null) {
085                removeMouseListener(JmriMouseListener.adapt(myMouseAdapter));
086            }
087        }
088    }
089
090    /**
091     * Add a "open system file browser to path" menu item to the contextual menu
092     *
093     * @param menuEntry the menu entry string
094     * @param path the path to browse to
095     * @return the added menu item
096     */
097     public JMenuItem addMenuItemBrowseFolder(String menuEntry, String path) {
098        if (myMouseAdapter == null) {
099            return null;
100        }
101        JMenuItem  mi = new JMenuItem(menuEntry);
102        mi.addActionListener((ActionEvent e) -> {
103            try {
104                Desktop.getDesktop().open(new File(path));
105            } catch (IOException ex) {
106                log.error("Browse to action {}", ex.getMessage());
107            }
108        });
109        myMouseAdapter.addMenuItem(mi);
110        return mi;
111    }
112
113    /**
114     * Remove a given menu item from the contextual menu
115     *
116     * @param mi the JMenuItem to remove
117     */
118     public void removeMenuItemBrowseFolder(JMenuItem mi) {
119        if (myMouseAdapter == null) {
120            return ;
121        }
122        myMouseAdapter.removeMenuItem(mi);
123    }
124
125    //
126    // For contextual menu
127    private static class MyMouseAdapter implements JmriMouseListener {
128
129        private JPopupMenu popUpMenu;
130        private final JMenuItem removeMenuItem;
131        private final JMenuItem fileChooserItem;
132        private final EditableResizableImagePanel myPanel;
133        private JFileChooser fileChooser;
134
135        public MyMouseAdapter(EditableResizableImagePanel resizableImagePanel) {
136            myPanel = resizableImagePanel;
137            popUpMenu = new JPopupMenu();
138            fileChooserItem = new JMenuItem(Bundle.getMessage("OpenFile"));
139            fileChooserItem.addActionListener((ActionEvent e) -> {
140                showFileChooser();
141            });
142            removeMenuItem = new JMenuItem(Bundle.getMessage("Remove"));
143            removeMenuItem.addActionListener((ActionEvent e) -> {
144                myPanel.setImagePath(null);
145            });
146            popUpMenu.add(fileChooserItem);
147            popUpMenu.add(removeMenuItem);
148        }
149
150        public void addMenuItem(JMenuItem item) {
151            if (item != null) {
152                popUpMenu.add(item);
153            }
154        }
155
156        public void removeMenuItem(JMenuItem item) {
157            if (item != null) {
158                popUpMenu.remove(item);
159            }
160        }
161
162        @Override
163        public void mouseClicked(JmriMouseEvent e) {
164            maybeShowPopup(e);
165        }
166
167        @Override
168        public void mousePressed(JmriMouseEvent e) {
169            maybeShowPopup(e);
170        }
171
172        @Override
173        public void mouseReleased(JmriMouseEvent e) {
174            maybeShowPopup(e);
175        }
176
177        @Override
178        public void mouseEntered(JmriMouseEvent e) {
179        }
180
181        @Override
182        public void mouseExited(JmriMouseEvent e) {
183        }
184
185        private void maybeShowPopup(JmriMouseEvent e) {
186            if (e.isPopupTrigger()) {
187                popUpMenu.show(e.getComponent(), e.getX(), e.getY());                
188            }
189            if (e.getClickCount() == 2 && e.getButton() == JmriMouseEvent.BUTTON1) {
190                showFileChooser();
191            }                    
192        }
193
194        private void showFileChooser() {            
195            if (fileChooser == null) {
196                fileChooser = new jmri.util.swing.JmriJFileChooser(); // NOI18N                
197            }
198            // Show dialog
199            fileChooser.rescanCurrentDirectory();
200            int retValue = fileChooser.showOpenDialog(myPanel);
201
202            // Process selection
203            if (retValue == JFileChooser.APPROVE_OPTION) {
204                File file = fileChooser.getSelectedFile();
205                java.net.URI[] fileNames = { file.toURI() };
206                myPanel.URIsDropped(fileNames);
207            }
208        }
209    }
210
211    public void setDropFolder(String s) {
212        dropFolder = s;
213    }
214
215    public String getDropFolder() {
216        return dropFolder;
217    }
218
219    /**
220     * Callback for the dnd listener
221     */
222    @Override
223    public void URIsDropped(java.net.URI[] uris) {
224        if (uris == null) {
225            log.error("URIsDropped: no URI");
226            return;
227        }
228        if (uris.length == 0) {
229            log.error("URIsDropped: no URI");
230            return;
231        }
232        if (uris[0].getPath() == null) {
233            log.error("URIsDropped: not a valid URI path: {}",uris[0]);
234            return;
235        }
236        File src = new File(uris[0].getPath());
237        File dest = new File(uris[0].getPath());
238        if (dropFolder != null) {
239            dest = new File(dropFolder + File.separatorChar + src.getName());
240            if (src.getParent().compareTo(dest.getParent()) != 0) {
241                // else case would be droping from dropFolder, so no copy
242                BufferedInputStream in = null;
243                BufferedInputStream out = null;
244                FileOutputStream fileOutputStream = null;
245                try {
246                    // prepare source reader
247                    boolean srcIsFile;
248                    FileUtil.createDirectory(dest.getParentFile().getPath());
249                    if (uris[0].getScheme() != null && (uris[0].getScheme().equals("content") || uris[0].getScheme().equals("file"))) {
250                        in = new BufferedInputStream(uris[0].toURL().openStream());
251                        srcIsFile = true;
252                    } else { // let's avoid some 403 by passing a user agent
253                        HttpURLConnection httpcon = (HttpURLConnection) uris[0].toURL().openConnection();
254                        httpcon.addRequestProperty("User-Agent", "Mozilla/4.0");
255                        in = new BufferedInputStream(httpcon.getInputStream());
256                        srcIsFile = false;
257                    }
258                    // guess destination name and check if does not already exist
259                    int i = 0;
260                    while (dest.exists()) {
261                        // is it already there?
262                        boolean alreadyThere = false;
263                        if ( (srcIsFile) && (dest.length() == src.length()) ) {
264                            out = new BufferedInputStream( dest.toURI().toURL().openStream() );
265                            byte dataBufferIn[] = new byte[4096];
266                            byte dataBufferOut[] = new byte[4096];
267                            int bytesReadIn;
268                            int bytesReadOut;
269                            alreadyThere = true;
270                            // file comparison loop
271                            while ((bytesReadIn = in.read(dataBufferIn, 0, 4096)) != -1) {
272                                bytesReadOut = out.read(dataBufferOut, 0, 4096);
273                                if ( (bytesReadIn != bytesReadOut) || ( ! Arrays.equals(dataBufferIn, dataBufferOut) ) ) {
274                                    alreadyThere = false;
275                                    break;
276                                }
277                            }
278                            out.close();
279                        }
280                        if (alreadyThere) {
281                            break;
282                        }
283                        // else try next one
284                        i++;
285                        dest = new File(dropFolder + File.separatorChar + i+"-"+src.getName());
286                    }
287                    // finally, if needed, create file and copy data
288                    if ( ! dest.exists()) {
289                        fileOutputStream = new FileOutputStream(dest);
290                        byte dataBuffer[] = new byte[4096];
291                        int bytesRead;
292                        // file copy loop
293                        while ((bytesRead = in.read(dataBuffer, 0, 4096)) != -1) {
294                            fileOutputStream.write(dataBuffer, 0, bytesRead);
295                        }
296                    }
297                } catch (IOException e) {
298                    log.error("URIsDropped: error while copying new file, using original file");
299                    log.error("URIsDropped: Error : {}", e.getMessage());
300                    log.error("URIsDropped: URI : {}", uris[0]);
301                    dest = src;
302                } finally {
303                    try {
304                        if (fileOutputStream != null) {
305                            fileOutputStream.close();
306                        }
307                    } catch (IOException ex) {
308                        log.error("URIsDropped: error while closing copy destination file : {}", ex.getMessage());
309                    }
310                    try {
311                        if (in != null) {
312                            in.close();
313                        }
314                    } catch (IOException ex) {
315                        log.error("URIsDropped: error while closing copy source file : {}", ex.getMessage());
316                    }
317                    try {
318                        if (out != null) {
319                            out.close();
320                        }
321                    } catch (IOException ex) {
322                        log.error("URIsDropped: error while closing duplicate check file : {}", ex.getMessage());
323                    }
324                }
325            }
326        }
327        setImagePath(dest.getPath());
328    }
329    
330    /**
331     * Callback for the dnd listener
332     */
333    @Override
334    public void imageDropped(BufferedImage image) {
335        if (image == null) {
336            log.error("imageDropped: no image");
337            return;
338        }
339        if (dropFolder == null) {
340            log.error("imageDropped: no drop destination");
341            return;
342        }
343        File dest = new File(dropFolder + File.separatorChar + UUID.randomUUID()+".png");
344        try {
345            ImageIO.write(image, "png", dest);
346        } catch (IOException ex) {
347            log.error("imageDropped: error while writing destination file : {}", ex.getMessage());
348        }
349        setImagePath(dest.getPath());
350        
351    }
352
353    private final static Logger log = LoggerFactory.getLogger(EditableResizableImagePanel.class);
354}