1 /* 2 * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.javafx.webkit; 27 28 import static com.sun.glass.ui.Clipboard.DRAG_IMAGE; 29 import static com.sun.glass.ui.Clipboard.DRAG_IMAGE_OFFSET; 30 import static com.sun.glass.ui.Clipboard.IE_URL_SHORTCUT_FILENAME; 31 import static javafx.scene.web.WebEvent.ALERT; 32 import static javafx.scene.web.WebEvent.RESIZED; 33 import static javafx.scene.web.WebEvent.STATUS_CHANGED; 34 import static javafx.scene.web.WebEvent.VISIBILITY_CHANGED; 35 36 import com.sun.javafx.tk.Toolkit; 37 import com.sun.webkit.UIClient; 38 import com.sun.webkit.WebPage; 39 import com.sun.webkit.graphics.WCImage; 40 import com.sun.webkit.graphics.WCRectangle; 41 import java.awt.image.BufferedImage; 42 import java.io.File; 43 import java.io.IOException; 44 import java.io.UnsupportedEncodingException; 45 import java.lang.reflect.Method; 46 import java.nio.ByteBuffer; 47 import java.security.AccessControlContext; 48 import java.security.AccessController; 49 import java.security.PrivilegedAction; 50 import java.util.Arrays; 51 import java.util.HashMap; 52 import java.util.List; 53 import java.util.Map; 54 import javafx.event.EventHandler; 55 import javafx.geometry.Rectangle2D; 56 import javafx.scene.image.Image; 57 import javafx.scene.input.ClipboardContent; 58 import javafx.scene.input.DataFormat; 59 import javafx.scene.input.Dragboard; 60 import javafx.scene.input.TransferMode; 61 import javafx.scene.web.PopupFeatures; 62 import javafx.scene.web.PromptData; 63 import javafx.scene.web.WebEngine; 64 import javafx.scene.web.WebEvent; 65 import javafx.scene.web.WebView; 66 import javafx.stage.FileChooser; 67 import javafx.stage.FileChooser.ExtensionFilter; 68 import javafx.stage.Window; 69 import javax.imageio.ImageIO; 70 71 public final class UIClientImpl implements UIClient { 72 private final Accessor accessor; 73 private FileChooser chooser; 74 private static final Map<String, FileExtensionInfo> fileExtensionMap = new HashMap<>(); 75 76 private static class FileExtensionInfo { 77 private String description; 78 private List<String> extensions; 79 static void add(String type, String description, String... extensions) { 80 FileExtensionInfo info = new FileExtensionInfo(); 81 info.description = description; 82 info.extensions = Arrays.asList(extensions); 83 fileExtensionMap.put(type, info); 84 } 85 86 private ExtensionFilter getExtensionFilter(String type) { 87 final String extensionType = "*." + type; 88 String desc = this.description + " "; 89 90 if (type.equals("*")) { 91 desc += extensions.stream().collect(java.util.stream.Collectors.joining(", ", "(", ")")); 92 return new ExtensionFilter(desc, this.extensions); 93 } else if (extensions.contains(extensionType)) { 94 desc += "(" + extensionType + ")"; 95 return new ExtensionFilter(desc, extensionType); 96 } 97 return null; 98 } 99 } 100 101 static { 102 FileExtensionInfo.add("video", "Video Files", "*.webm", "*.mp4", "*.ogg"); 103 FileExtensionInfo.add("audio", "Audio Files", "*.mp3", "*.aac", "*.wav"); 104 FileExtensionInfo.add("text", "Text Files", "*.txt", "*.csv", "*.text", "*.ttf", "*.sdf", "*.srt", "*.htm", "*.html"); 105 FileExtensionInfo.add("image", "Image Files", "*.png", "*.jpg", "*.gif", "*.bmp", "*.jpeg"); 106 } 107 108 public UIClientImpl(Accessor accessor) { 109 this.accessor = accessor; 110 } 111 112 private WebEngine getWebEngine() { 113 return accessor.getEngine(); 114 } 115 116 private AccessControlContext getAccessContext() { 117 return accessor.getPage().getAccessControlContext(); 118 } 119 120 @Override public WebPage createPage( 121 boolean menu, boolean status, boolean toolbar, boolean resizable) { 122 final WebEngine w = getWebEngine(); 123 if (w != null && w.getCreatePopupHandler() != null) { 124 final PopupFeatures pf = 125 new PopupFeatures(menu, status, toolbar, resizable); 126 WebEngine popup = AccessController.doPrivileged( 127 (PrivilegedAction<WebEngine>) () -> w.getCreatePopupHandler().call(pf), getAccessContext()); 128 return Accessor.getPageFor(popup); 129 } 130 return null; 131 } 132 133 private void dispatchWebEvent(final EventHandler handler, final WebEvent ev) { 134 AccessController.doPrivileged((PrivilegedAction<Void>) () -> { 135 handler.handle(ev); 136 return null; 137 }, getAccessContext()); 138 } 139 140 private void notifyVisibilityChanged(boolean visible) { 141 WebEngine w = getWebEngine(); 142 if (w != null && w.getOnVisibilityChanged() != null) { 143 dispatchWebEvent( 144 w.getOnVisibilityChanged(), 145 new WebEvent<Boolean>(w, VISIBILITY_CHANGED, visible)); 146 } 147 } 148 149 @Override public void closePage() { 150 notifyVisibilityChanged(false); 151 } 152 153 @Override public void showView() { 154 notifyVisibilityChanged(true); 155 } 156 157 @Override public WCRectangle getViewBounds() { 158 WebView view = accessor.getView(); 159 Window win = null; 160 if (view != null && 161 view.getScene() != null && 162 (win = view.getScene().getWindow()) != null) 163 { 164 return new WCRectangle( 165 (float) win.getX(), (float) win.getY(), 166 (float) win.getWidth(), (float) win.getHeight()); 167 } 168 return null; 169 } 170 171 @Override public void setViewBounds(WCRectangle r) { 172 WebEngine w = getWebEngine(); 173 if (w != null && w.getOnResized() != null) { 174 dispatchWebEvent( 175 w.getOnResized(), 176 new WebEvent<Rectangle2D>(w, RESIZED, 177 new Rectangle2D(r.getX(), r.getY(), r.getWidth(), r.getHeight()))); 178 } 179 } 180 181 @Override public void setStatusbarText(String text) { 182 WebEngine w = getWebEngine(); 183 if (w != null && w.getOnStatusChanged() != null) { 184 dispatchWebEvent( 185 w.getOnStatusChanged(), 186 new WebEvent<String>(w, STATUS_CHANGED, text)); 187 } 188 } 189 190 @Override public void alert(String text) { 191 WebEngine w = getWebEngine(); 192 if (w != null && w.getOnAlert() != null) { 193 dispatchWebEvent( 194 w.getOnAlert(), 195 new WebEvent<String>(w, ALERT, text)); 196 } 197 } 198 199 @Override public boolean confirm(final String text) { 200 final WebEngine w = getWebEngine(); 201 if (w != null && w.getConfirmHandler() != null) { 202 return AccessController.doPrivileged( 203 (PrivilegedAction<Boolean>) () -> w.getConfirmHandler().call(text), getAccessContext()); 204 } 205 return false; 206 } 207 208 @Override public String prompt(String text, String defaultValue) { 209 final WebEngine w = getWebEngine(); 210 if (w != null && w.getPromptHandler() != null) { 211 final PromptData data = new PromptData(text, defaultValue); 212 return AccessController.doPrivileged( 213 (PrivilegedAction<String>) () -> w.getPromptHandler().call(data), getAccessContext()); 214 } 215 return ""; 216 } 217 218 @Override public String[] chooseFile(String initialFileName, boolean multiple, String mimeFilters) { 219 // get the toplevel window 220 Window win = null; 221 WebView view = accessor.getView(); 222 if (view != null && view.getScene() != null) { 223 win = view.getScene().getWindow(); 224 } 225 226 if (chooser == null) { 227 chooser = new FileChooser(); 228 } 229 230 // Remove old filters, add specific filters and finally add generic filter 231 chooser.getExtensionFilters().clear(); 232 if (mimeFilters != null && !mimeFilters.isEmpty()) { 233 addMimeFilters(chooser, mimeFilters); 234 } 235 chooser.getExtensionFilters().addAll(new ExtensionFilter("All Files", "*.*")); 236 237 // set initial directory 238 if (initialFileName != null) { 239 File dir = new File(initialFileName); 240 while (dir != null && !dir.isDirectory()) { 241 dir = dir.getParentFile(); 242 } 243 chooser.setInitialDirectory(dir); 244 } 245 246 if (multiple) { 247 List<File> files = chooser.showOpenMultipleDialog(win); 248 if (files != null) { 249 int n = files.size(); 250 String[] result = new String[n]; 251 for (int i = 0; i < n; i++) { 252 result[i] = files.get(i).getAbsolutePath(); 253 } 254 return result; 255 } 256 return null; 257 } else { 258 File f = chooser.showOpenDialog(win); 259 return f != null 260 ? new String[] { f.getAbsolutePath() } 261 : null; 262 } 263 } 264 265 private void addSpecificFilters(FileChooser chooser, String mimeString) { 266 if (mimeString.contains("/")) { 267 final String splittedMime[] = mimeString.split("/"); 268 final String mainType = splittedMime[0]; 269 final String subType = splittedMime[1]; 270 final FileExtensionInfo extensionValue = fileExtensionMap.get(mainType); 271 272 if (extensionValue != null) { 273 ExtensionFilter extFilter = extensionValue.getExtensionFilter(subType); 274 if(extFilter != null) { 275 chooser.getExtensionFilters().addAll(extFilter); 276 } 277 } 278 } 279 } 280 281 private void addMimeFilters(FileChooser chooser, String mimeFilters) { 282 if (mimeFilters.contains(",")) { 283 // Filter consists of multiple MIME types 284 String types[] = mimeFilters.split(","); 285 for (String mimeType : types) { 286 addSpecificFilters(chooser, mimeType); 287 } 288 } else { 289 // Filter consists of single MIME type 290 addSpecificFilters(chooser, mimeFilters); 291 } 292 } 293 294 @Override public void print() { 295 } 296 297 private ClipboardContent content; 298 private static DataFormat getDataFormat(String mimeType) { 299 synchronized (DataFormat.class) { 300 DataFormat ret = DataFormat.lookupMimeType(mimeType); 301 if (ret == null) { 302 ret = new DataFormat(mimeType); 303 } 304 return ret; 305 } 306 } 307 308 //copy from com.sun.glass.ui.Clipboard 309 private final static DataFormat DF_DRAG_IMAGE = getDataFormat(DRAG_IMAGE); 310 private final static DataFormat DF_DRAG_IMAGE_OFFSET = getDataFormat(DRAG_IMAGE_OFFSET); 311 312 @Override public void startDrag(WCImage image, 313 int imageOffsetX, int imageOffsetY, 314 int eventPosX, int eventPosY, 315 String[] mimeTypes, Object[] values, boolean isImageSource 316 ){ 317 content = new ClipboardContent(); 318 for (int i = 0; i < mimeTypes.length; ++i) if (values[i] != null) { 319 try { 320 content.put(getDataFormat(mimeTypes[i]), 321 IE_URL_SHORTCUT_FILENAME.equals(mimeTypes[i]) 322 ? (Object)ByteBuffer.wrap(((String)values[i]).getBytes("UTF-16LE")) 323 : (Object)values[i]); 324 } catch (UnsupportedEncodingException ex) { 325 //never happens 326 } 327 } 328 if (image != null) { 329 ByteBuffer dragImageOffset = ByteBuffer.allocate(8); 330 dragImageOffset.rewind(); 331 dragImageOffset.putInt(imageOffsetX); 332 dragImageOffset.putInt(imageOffsetY); 333 content.put(DF_DRAG_IMAGE_OFFSET, dragImageOffset); 334 335 int w = image.getWidth(); 336 int h = image.getHeight(); 337 ByteBuffer pixels = image.getPixelBuffer(); 338 339 ByteBuffer dragImage = ByteBuffer.allocate(8 + w*h*4); 340 dragImage.putInt(w); 341 dragImage.putInt(h); 342 dragImage.put(pixels); 343 content.put(DF_DRAG_IMAGE, dragImage); 344 345 //The image is prepared synchronously, that is sad. 346 //Image need to be created by target request only. 347 //QuantumClipboard.putContent have to be rewritten in Glass manner 348 //with postponed data requests (DelayedCallback data object). 349 if (isImageSource) { 350 Object platformImage = image.getWidth() > 0 && image.getHeight() > 0 ? 351 image.getPlatformImage() : null; 352 if (platformImage != null) { 353 try { 354 File temp = File.createTempFile("jfx", ".png"); 355 temp.deleteOnExit(); 356 ImageIO.write( 357 toBufferedImage(Toolkit.getImageAccessor().fromPlatformImage( 358 Toolkit.getToolkit().loadPlatformImage( 359 platformImage 360 ) 361 )), 362 "png", 363 temp); 364 content.put(DataFormat.FILES, Arrays.asList(temp)); 365 } catch (IOException | SecurityException e) { 366 //That is ok. It was just an attempt. 367 //e.printStackTrace(); 368 } 369 } 370 } 371 } 372 } 373 374 @Override public void confirmStartDrag() { 375 WebView view = accessor.getView(); 376 if (view != null && content != null) { 377 //TODO: implement native support for Drag Source actions. 378 Dragboard db = view.startDragAndDrop(TransferMode.ANY); 379 db.setContent(content); 380 } 381 content = null; 382 } 383 384 @Override public boolean isDragConfirmed() { 385 return accessor.getView() != null && content != null; 386 } 387 388 // Method to implement the following via reflection: 389 // SwingFXUtils.fromFXImage(img, null) 390 public static BufferedImage toBufferedImage(Image img) { 391 try { 392 Class swingFXUtilsCls = Class.forName("javafx.embed.swing.SwingFXUtils"); 393 Method m_fromFXImage = swingFXUtilsCls.getMethod("fromFXImage", 394 Image.class, BufferedImage.class); 395 Object bimg = m_fromFXImage.invoke(null, img, null); 396 return (BufferedImage)bimg; 397 } catch (Exception ex) { 398 ex.printStackTrace(System.err); 399 } 400 401 // return null upon any exception 402 return null; 403 } 404 405 }