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}