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     * Return the mouse adapter used for the contextual menu, can be used to add/remove menu items
127     * but also by mouse handlers over JTable (see FunctionTableMouseListener) to forward mouse events to the contextual menu
128     *
129     * @return the mouse adapter used for the contextual menu, can be used to add/remove menu items
130     * to the contextual menu
131     */
132    public JmriMouseListener getMyMouseAdapter() {
133        return myMouseAdapter;
134    }
135
136    //
137    // For contextual menu
138    private static class MyMouseAdapter implements JmriMouseListener {
139
140        private JPopupMenu popUpMenu;
141        private final JMenuItem removeMenuItem;
142        private final JMenuItem fileChooserItem;
143        private final EditableResizableImagePanel myPanel;
144        private JFileChooser fileChooser;
145
146        public MyMouseAdapter(EditableResizableImagePanel resizableImagePanel) {
147            myPanel = resizableImagePanel;
148            popUpMenu = new JPopupMenu();
149            fileChooserItem = new JMenuItem(Bundle.getMessage("OpenFile"));
150            fileChooserItem.addActionListener((ActionEvent e) -> {
151                showFileChooser();
152            });
153            removeMenuItem = new JMenuItem(Bundle.getMessage("Remove"));
154            removeMenuItem.addActionListener((ActionEvent e) -> {
155                myPanel.setImagePath(null);
156            });
157            popUpMenu.add(fileChooserItem);
158            popUpMenu.add(removeMenuItem);
159        }
160
161        public void addMenuItem(JMenuItem item) {
162            if (item != null) {
163                popUpMenu.add(item);
164            }
165        }
166
167        public void removeMenuItem(JMenuItem item) {
168            if (item != null) {
169                popUpMenu.remove(item);
170            }
171        }
172
173        @Override
174        public void mouseClicked(JmriMouseEvent e) {
175            maybeShowPopup(e);
176        }
177
178        @Override
179        public void mousePressed(JmriMouseEvent e) {
180            maybeShowPopup(e);
181        }
182
183        @Override
184        public void mouseReleased(JmriMouseEvent e) {
185            maybeShowPopup(e);
186        }
187
188        @Override
189        public void mouseEntered(JmriMouseEvent e) {
190        }
191
192        @Override
193        public void mouseExited(JmriMouseEvent e) {
194        }
195
196        private void maybeShowPopup(JmriMouseEvent e) {
197            if (e.isPopupTrigger()) {
198                popUpMenu.show(e.getComponent(), e.getX(), e.getY());                
199            }
200            if (e.getClickCount() == 2 && e.getButton() == JmriMouseEvent.BUTTON1) {
201                showFileChooser();
202            }                    
203        }
204
205        private void showFileChooser() {            
206            if (fileChooser == null) {
207                fileChooser = new jmri.util.swing.JmriJFileChooser(); // NOI18N                
208            }
209            // Show dialog
210            fileChooser.rescanCurrentDirectory();
211            int retValue = fileChooser.showOpenDialog(myPanel);
212
213            // Process selection
214            if (retValue == JFileChooser.APPROVE_OPTION) {
215                File file = fileChooser.getSelectedFile();
216                java.net.URI[] fileNames = { file.toURI() };
217                myPanel.URIsDropped(fileNames);
218            }
219        }
220    }
221
222    public void setDropFolder(String s) {
223        dropFolder = s;
224    }
225
226    public String getDropFolder() {
227        return dropFolder;
228    }
229
230    /**
231     * Callback for the dnd listener
232     */
233    @Override
234    public void URIsDropped(java.net.URI[] uris) {
235        if (uris == null) {
236            log.error("URIsDropped: no URI");
237            return;
238        }
239        if (uris.length == 0) {
240            log.error("URIsDropped: no URI");
241            return;
242        }
243        if (uris[0].getPath() == null) {
244            log.error("URIsDropped: not a valid URI path: {}",uris[0]);
245            return;
246        }
247        File src = new File(uris[0].getPath());
248        File dest = new File(uris[0].getPath());
249        if (dropFolder != null) {
250            dest = new File(dropFolder + File.separatorChar + src.getName());
251            if (src.getParent().compareTo(dest.getParent()) != 0) {
252                // else case would be droping from dropFolder, so no copy
253                BufferedInputStream in = null;
254                BufferedInputStream out = null;
255                FileOutputStream fileOutputStream = null;
256                try {
257                    // prepare source reader
258                    boolean srcIsFile;
259                    FileUtil.createDirectory(dest.getParentFile().getPath());
260                    if (uris[0].getScheme() != null && (uris[0].getScheme().equals("content") || uris[0].getScheme().equals("file"))) {
261                        in = new BufferedInputStream(uris[0].toURL().openStream());
262                        srcIsFile = true;
263                    } else { // let's avoid some 403 by passing a user agent
264                        HttpURLConnection httpcon = (HttpURLConnection) uris[0].toURL().openConnection();
265                        httpcon.addRequestProperty("User-Agent", "Mozilla/4.0");
266                        in = new BufferedInputStream(httpcon.getInputStream());
267                        srcIsFile = false;
268                    }
269                    // guess destination name and check if does not already exist
270                    int i = 0;
271                    while (dest.exists()) {
272                        // is it already there?
273                        boolean alreadyThere = false;
274                        if ( (srcIsFile) && (dest.length() == src.length()) ) {
275                            out = new BufferedInputStream( dest.toURI().toURL().openStream() );
276                            byte dataBufferIn[] = new byte[4096];
277                            byte dataBufferOut[] = new byte[4096];
278                            int bytesReadIn;
279                            int bytesReadOut;
280                            alreadyThere = true;
281                            // file comparison loop
282                            while ((bytesReadIn = in.read(dataBufferIn, 0, 4096)) != -1) {
283                                bytesReadOut = out.read(dataBufferOut, 0, 4096);
284                                if ( (bytesReadIn != bytesReadOut) || ( ! Arrays.equals(dataBufferIn, dataBufferOut) ) ) {
285                                    alreadyThere = false;
286                                    break;
287                                }
288                            }
289                            out.close();
290                        }
291                        if (alreadyThere) {
292                            break;
293                        }
294                        // else try next one
295                        i++;
296                        dest = new File(dropFolder + File.separatorChar + i+"-"+src.getName());
297                    }
298                    // finally, if needed, create file and copy data
299                    if ( ! dest.exists()) {
300                        fileOutputStream = new FileOutputStream(dest);
301                        byte dataBuffer[] = new byte[4096];
302                        int bytesRead;
303                        // file copy loop
304                        while ((bytesRead = in.read(dataBuffer, 0, 4096)) != -1) {
305                            fileOutputStream.write(dataBuffer, 0, bytesRead);
306                        }
307                    }
308                } catch (IOException e) {
309                    log.error("URIsDropped: error while copying new file, using original file");
310                    log.error("URIsDropped: Error : {}", e.getMessage());
311                    log.error("URIsDropped: URI : {}", uris[0]);
312                    dest = src;
313                } finally {
314                    try {
315                        if (fileOutputStream != null) {
316                            fileOutputStream.close();
317                        }
318                    } catch (IOException ex) {
319                        log.error("URIsDropped: error while closing copy destination file : {}", ex.getMessage());
320                    }
321                    try {
322                        if (in != null) {
323                            in.close();
324                        }
325                    } catch (IOException ex) {
326                        log.error("URIsDropped: error while closing copy source file : {}", ex.getMessage());
327                    }
328                    try {
329                        if (out != null) {
330                            out.close();
331                        }
332                    } catch (IOException ex) {
333                        log.error("URIsDropped: error while closing duplicate check file : {}", ex.getMessage());
334                    }
335                }
336            }
337        }
338        setImagePath(dest.getPath());
339    }
340    
341    /**
342     * Callback for the dnd listener
343     */
344    @Override
345    public void imageDropped(BufferedImage image) {
346        if (image == null) {
347            log.error("imageDropped: no image");
348            return;
349        }
350        if (dropFolder == null) {
351            log.error("imageDropped: no drop destination");
352            return;
353        }
354        File dest = new File(dropFolder + File.separatorChar + UUID.randomUUID()+".png");
355        try {
356            ImageIO.write(image, "png", dest);
357        } catch (IOException ex) {
358            log.error("imageDropped: error while writing destination file : {}", ex.getMessage());
359        }
360        setImagePath(dest.getPath());
361        
362    }
363
364    private static final Logger log = LoggerFactory.getLogger(EditableResizableImagePanel.class);
365}