1 /* 2 * Copyright (c) 2011, 2017, 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.AlphaComposite; 42 import java.awt.Graphics2D; 43 import java.awt.image.BufferedImage; 44 import java.awt.image.DataBufferInt; 45 import java.awt.image.SampleModel; 46 import java.awt.image.SinglePixelPackedSampleModel; 47 import java.io.File; 48 import java.io.IOException; 49 import java.io.UnsupportedEncodingException; 50 import java.nio.ByteBuffer; 51 import java.nio.IntBuffer; 52 import java.security.AccessControlContext; 53 import java.security.AccessController; 54 import java.security.PrivilegedAction; 55 import java.util.Arrays; 56 import java.util.HashMap; 57 import java.util.List; 58 import java.util.Map; 59 import javafx.event.EventHandler; 60 import javafx.geometry.Rectangle2D; 61 import javafx.scene.image.Image; 62 import javafx.scene.image.PixelFormat; 63 import javafx.scene.image.PixelReader; 64 import javafx.scene.image.WritablePixelFormat; 65 import javafx.scene.input.ClipboardContent; 66 import javafx.scene.input.DataFormat; 67 import javafx.scene.input.Dragboard; 68 import javafx.scene.input.TransferMode; 69 import javafx.scene.web.PopupFeatures; 70 import javafx.scene.web.PromptData; 71 import javafx.scene.web.WebEngine; 72 import javafx.scene.web.WebEvent; 73 import javafx.scene.web.WebView; 74 import javafx.scene.paint.Color; 75 import javafx.stage.FileChooser; 76 import javafx.stage.FileChooser.ExtensionFilter; 77 import javafx.stage.Window; 78 import javax.imageio.ImageIO; 79 80 public final class UIClientImpl implements UIClient { 81 private final Accessor accessor; 82 private FileChooser chooser; 83 private static final Map<String, FileExtensionInfo> fileExtensionMap = new HashMap<>(); 84 85 private static class FileExtensionInfo { 86 private String description; 87 private List<String> extensions; 88 static void add(String type, String description, String... extensions) { 89 FileExtensionInfo info = new FileExtensionInfo(); 90 info.description = description; 91 info.extensions = Arrays.asList(extensions); 92 fileExtensionMap.put(type, info); 93 } 94 95 private ExtensionFilter getExtensionFilter(String type) { 96 final String extensionType = "*." + type; 97 String desc = this.description + " "; 98 99 if (type.equals("*")) { 100 desc += extensions.stream().collect(java.util.stream.Collectors.joining(", ", "(", ")")); 101 return new ExtensionFilter(desc, this.extensions); 102 } else if (extensions.contains(extensionType)) { 103 desc += "(" + extensionType + ")"; 104 return new ExtensionFilter(desc, extensionType); 105 } 106 return null; 107 } 108 } 109 110 static { 111 FileExtensionInfo.add("video", "Video Files", "*.webm", "*.mp4", "*.ogg"); 112 FileExtensionInfo.add("audio", "Audio Files", "*.mp3", "*.aac", "*.wav"); 113 FileExtensionInfo.add("text", "Text Files", "*.txt", "*.csv", "*.text", "*.ttf", "*.sdf", "*.srt", "*.htm", "*.html"); 114 FileExtensionInfo.add("image", "Image Files", "*.png", "*.jpg", "*.gif", "*.bmp", "*.jpeg"); 115 } 116 117 public UIClientImpl(Accessor accessor) { 118 this.accessor = accessor; 119 } 120 121 private WebEngine getWebEngine() { 122 return accessor.getEngine(); 123 } 124 125 private AccessControlContext getAccessContext() { 126 return accessor.getPage().getAccessControlContext(); 127 } 128 129 @Override public WebPage createPage( 130 boolean menu, boolean status, boolean toolbar, boolean resizable) { 131 final WebEngine w = getWebEngine(); 132 if (w != null && w.getCreatePopupHandler() != null) { 133 final PopupFeatures pf = 134 new PopupFeatures(menu, status, toolbar, resizable); 135 WebEngine popup = AccessController.doPrivileged( 136 (PrivilegedAction<WebEngine>) () -> w.getCreatePopupHandler().call(pf), getAccessContext()); 137 return Accessor.getPageFor(popup); 138 } 139 return null; 140 } 141 142 private void dispatchWebEvent(final EventHandler handler, final WebEvent ev) { 143 AccessController.doPrivileged((PrivilegedAction<Void>) () -> { 144 handler.handle(ev); 145 return null; 146 }, getAccessContext()); 147 } 148 149 private void notifyVisibilityChanged(boolean visible) { 150 WebEngine w = getWebEngine(); 151 if (w != null && w.getOnVisibilityChanged() != null) { 152 dispatchWebEvent( 153 w.getOnVisibilityChanged(), 154 new WebEvent<Boolean>(w, VISIBILITY_CHANGED, visible)); 155 } 156 } 157 158 @Override public void closePage() { 159 notifyVisibilityChanged(false); 160 } 161 162 @Override public void showView() { 163 notifyVisibilityChanged(true); 164 } 165 166 @Override public WCRectangle getViewBounds() { 167 WebView view = accessor.getView(); 168 Window win = null; 169 if (view != null && 170 view.getScene() != null && 171 (win = view.getScene().getWindow()) != null) 172 { 173 return new WCRectangle( 174 (float) win.getX(), (float) win.getY(), 175 (float) win.getWidth(), (float) win.getHeight()); 176 } 177 return null; 178 } 179 180 @Override public void setViewBounds(WCRectangle r) { 181 WebEngine w = getWebEngine(); 182 if (w != null && w.getOnResized() != null) { 183 dispatchWebEvent( 184 w.getOnResized(), 185 new WebEvent<Rectangle2D>(w, RESIZED, 186 new Rectangle2D(r.getX(), r.getY(), r.getWidth(), r.getHeight()))); 187 } 188 } 189 190 @Override public void setStatusbarText(String text) { 191 WebEngine w = getWebEngine(); 192 if (w != null && w.getOnStatusChanged() != null) { 193 dispatchWebEvent( 194 w.getOnStatusChanged(), 195 new WebEvent<String>(w, STATUS_CHANGED, text)); 196 } 197 } 198 199 @Override public void alert(String text) { 200 WebEngine w = getWebEngine(); 201 if (w != null && w.getOnAlert() != null) { 202 dispatchWebEvent( 203 w.getOnAlert(), 204 new WebEvent<String>(w, ALERT, text)); 205 } 206 } 207 208 @Override public boolean confirm(final String text) { 209 final WebEngine w = getWebEngine(); 210 if (w != null && w.getConfirmHandler() != null) { 211 return AccessController.doPrivileged( 212 (PrivilegedAction<Boolean>) () -> w.getConfirmHandler().call(text), getAccessContext()); 213 } 214 return false; 215 } 216 217 @Override public String prompt(String text, String defaultValue) { 218 final WebEngine w = getWebEngine(); 219 if (w != null && w.getPromptHandler() != null) { 220 final PromptData data = new PromptData(text, defaultValue); 221 return AccessController.doPrivileged( 222 (PrivilegedAction<String>) () -> w.getPromptHandler().call(data), getAccessContext()); 223 } 224 return ""; 225 } 226 227 @Override public boolean canRunBeforeUnloadConfirmPanel() { 228 return false; 229 } 230 231 @Override public boolean runBeforeUnloadConfirmPanel(String message) { 232 return false; 233 } 234 235 @Override public String[] chooseFile(String initialFileName, boolean multiple, String mimeFilters) { 236 // get the toplevel window 237 Window win = null; 238 WebView view = accessor.getView(); 239 if (view != null && view.getScene() != null) { 240 win = view.getScene().getWindow(); 241 } 242 243 if (chooser == null) { 244 chooser = new FileChooser(); 245 } 246 247 // Remove old filters, add specific filters and finally add generic filter 248 chooser.getExtensionFilters().clear(); 249 if (mimeFilters != null && !mimeFilters.isEmpty()) { 250 addMimeFilters(chooser, mimeFilters); 251 } 252 chooser.getExtensionFilters().addAll(new ExtensionFilter("All Files", "*.*")); 253 254 // set initial directory 255 if (initialFileName != null) { 256 File dir = new File(initialFileName); 257 while (dir != null && !dir.isDirectory()) { 258 dir = dir.getParentFile(); 259 } 260 chooser.setInitialDirectory(dir); 261 } 262 263 if (multiple) { 264 List<File> files = chooser.showOpenMultipleDialog(win); 265 if (files != null) { 266 int n = files.size(); 267 String[] result = new String[n]; 268 for (int i = 0; i < n; i++) { 269 result[i] = files.get(i).getAbsolutePath(); 270 } 271 return result; 272 } 273 return null; 274 } else { 275 File f = chooser.showOpenDialog(win); 276 return f != null 277 ? new String[] { f.getAbsolutePath() } 278 : null; 279 } 280 } 281 282 private void addSpecificFilters(FileChooser chooser, String mimeString) { 283 if (mimeString.contains("/")) { 284 final String splittedMime[] = mimeString.split("/"); 285 final String mainType = splittedMime[0]; 286 final String subType = splittedMime[1]; 287 final FileExtensionInfo extensionValue = fileExtensionMap.get(mainType); 288 289 if (extensionValue != null) { 290 ExtensionFilter extFilter = extensionValue.getExtensionFilter(subType); 291 if(extFilter != null) { 292 chooser.getExtensionFilters().addAll(extFilter); 293 } 294 } 295 } 296 } 297 298 private void addMimeFilters(FileChooser chooser, String mimeFilters) { 299 if (mimeFilters.contains(",")) { 300 // Filter consists of multiple MIME types 301 String types[] = mimeFilters.split(","); 302 for (String mimeType : types) { 303 addSpecificFilters(chooser, mimeType); 304 } 305 } else { 306 // Filter consists of single MIME type 307 addSpecificFilters(chooser, mimeFilters); 308 } 309 } 310 311 @Override public void print() { 312 } 313 314 private ClipboardContent content; 315 private static DataFormat getDataFormat(String mimeType) { 316 synchronized (DataFormat.class) { 317 DataFormat ret = DataFormat.lookupMimeType(mimeType); 318 if (ret == null) { 319 ret = new DataFormat(mimeType); 320 } 321 return ret; 322 } 323 } 324 325 //copy from com.sun.glass.ui.Clipboard 326 private final static DataFormat DF_DRAG_IMAGE = getDataFormat(DRAG_IMAGE); 327 private final static DataFormat DF_DRAG_IMAGE_OFFSET = getDataFormat(DRAG_IMAGE_OFFSET); 328 329 @Override public void startDrag(WCImage image, 330 int imageOffsetX, int imageOffsetY, 331 int eventPosX, int eventPosY, 332 String[] mimeTypes, Object[] values, boolean isImageSource 333 ){ 334 content = new ClipboardContent(); 335 for (int i = 0; i < mimeTypes.length; ++i) if (values[i] != null) { 336 try { 337 content.put(getDataFormat(mimeTypes[i]), 338 IE_URL_SHORTCUT_FILENAME.equals(mimeTypes[i]) 339 ? (Object)ByteBuffer.wrap(((String)values[i]).getBytes("UTF-16LE")) 340 : (Object)values[i]); 341 } catch (UnsupportedEncodingException ex) { 342 //never happens 343 } 344 } 345 if (image != null) { 346 ByteBuffer dragImageOffset = ByteBuffer.allocate(8); 347 dragImageOffset.rewind(); 348 dragImageOffset.putInt(imageOffsetX); 349 dragImageOffset.putInt(imageOffsetY); 350 content.put(DF_DRAG_IMAGE_OFFSET, dragImageOffset); 351 352 int w = image.getWidth(); 353 int h = image.getHeight(); 354 ByteBuffer pixels = image.getPixelBuffer(); 355 356 ByteBuffer dragImage = ByteBuffer.allocate(8 + w*h*4); 357 dragImage.putInt(w); 358 dragImage.putInt(h); 359 dragImage.put(pixels); 360 content.put(DF_DRAG_IMAGE, dragImage); 361 362 //The image is prepared synchronously, that is sad. 363 //Image need to be created by target request only. 364 //QuantumClipboard.putContent have to be rewritten in Glass manner 365 //with postponed data requests (DelayedCallback data object). 366 if (isImageSource) { 367 Object platformImage = image.getWidth() > 0 && image.getHeight() > 0 ? 368 image.getPlatformImage() : null; 369 String fileExtension = image.getFileExtension(); 370 if (platformImage != null) { 371 try { 372 File temp = File.createTempFile("jfx", "." + fileExtension); 373 temp.deleteOnExit(); 374 ImageIO.write( 375 toBufferedImage(Toolkit.getImageAccessor().fromPlatformImage( 376 Toolkit.getToolkit().loadPlatformImage( 377 platformImage 378 ) 379 )), 380 fileExtension, 381 temp); 382 content.put(DataFormat.FILES, Arrays.asList(temp)); 383 } catch (IOException | SecurityException e) { 384 //That is ok. It was just an attempt. 385 //e.printStackTrace(); 386 } 387 } 388 } 389 } 390 } 391 392 @Override public void confirmStartDrag() { 393 WebView view = accessor.getView(); 394 if (view != null && content != null) { 395 //TODO: implement native support for Drag Source actions. 396 Dragboard db = view.startDragAndDrop(TransferMode.ANY); 397 db.setContent(content); 398 } 399 content = null; 400 } 401 402 @Override public boolean isDragConfirmed() { 403 return accessor.getView() != null && content != null; 404 } 405 406 private static int 407 getBestBufferedImageType(PixelFormat<?> fxFormat, BufferedImage bimg, 408 boolean isOpaque) 409 { 410 if (bimg != null) { 411 int bimgType = bimg.getType(); 412 if (bimgType == BufferedImage.TYPE_INT_ARGB || 413 bimgType == BufferedImage.TYPE_INT_ARGB_PRE || 414 (isOpaque && 415 (bimgType == BufferedImage.TYPE_INT_BGR || 416 bimgType == BufferedImage.TYPE_INT_RGB))) 417 { 418 // We will allow the caller to give us a BufferedImage 419 // that has an alpha channel, but we might not otherwise 420 // construct one ourselves. 421 // We will also allow them to choose their own premultiply 422 // type which may not match the image. 423 // If left to our own devices we might choose a more specific 424 // format as indicated by the choices below. 425 return bimgType; 426 } 427 } 428 switch (fxFormat.getType()) { 429 default: 430 case BYTE_BGRA_PRE: 431 case INT_ARGB_PRE: 432 return BufferedImage.TYPE_INT_ARGB_PRE; 433 case BYTE_BGRA: 434 case INT_ARGB: 435 return BufferedImage.TYPE_INT_ARGB; 436 case BYTE_RGB: 437 return BufferedImage.TYPE_INT_RGB; 438 case BYTE_INDEXED: 439 return (fxFormat.isPremultiplied() 440 ? BufferedImage.TYPE_INT_ARGB_PRE 441 : BufferedImage.TYPE_INT_ARGB); 442 } 443 } 444 445 private static WritablePixelFormat<IntBuffer> 446 getAssociatedPixelFormat(BufferedImage bimg) 447 { 448 switch (bimg.getType()) { 449 // We lie here for xRGB, but we vetted that the src data was opaque 450 // so we can ignore the alpha. We use ArgbPre instead of Argb 451 // just to get a loop that does not have divides in it if the 452 // PixelReader happens to not know the data is opaque. 453 case BufferedImage.TYPE_INT_RGB: 454 case BufferedImage.TYPE_INT_ARGB_PRE: 455 return PixelFormat.getIntArgbPreInstance(); 456 case BufferedImage.TYPE_INT_ARGB: 457 return PixelFormat.getIntArgbInstance(); 458 default: 459 // Should not happen... 460 throw new InternalError("Failed to validate BufferedImage type"); 461 } 462 } 463 464 private static boolean checkFXImageOpaque(PixelReader pr, int iw, int ih) { 465 for (int x = 0; x < iw; x++) { 466 for (int y = 0; y < ih; y++) { 467 Color color = pr.getColor(x,y); 468 if (color.getOpacity() != 1.0) { 469 return false; 470 } 471 } 472 } 473 return true; 474 } 475 476 private static BufferedImage fromFXImage(Image img, BufferedImage bimg) { 477 PixelReader pr = img.getPixelReader(); 478 if (pr == null) { 479 return null; 480 } 481 int iw = (int) img.getWidth(); 482 int ih = (int) img.getHeight(); 483 PixelFormat<?> fxFormat = pr.getPixelFormat(); 484 boolean srcPixelsAreOpaque = false; 485 switch (fxFormat.getType()) { 486 case INT_ARGB_PRE: 487 case INT_ARGB: 488 case BYTE_BGRA_PRE: 489 case BYTE_BGRA: 490 // Check fx image opacity only if 491 // supplied BufferedImage is without alpha channel 492 if (bimg != null && 493 (bimg.getType() == BufferedImage.TYPE_INT_BGR || 494 bimg.getType() == BufferedImage.TYPE_INT_RGB)) { 495 srcPixelsAreOpaque = checkFXImageOpaque(pr, iw, ih); 496 } 497 break; 498 case BYTE_RGB: 499 srcPixelsAreOpaque = true; 500 break; 501 } 502 int prefBimgType = getBestBufferedImageType(pr.getPixelFormat(), bimg, srcPixelsAreOpaque); 503 if (bimg != null) { 504 int bw = bimg.getWidth(); 505 int bh = bimg.getHeight(); 506 if (bw < iw || bh < ih || bimg.getType() != prefBimgType) { 507 bimg = null; 508 } else if (iw < bw || ih < bh) { 509 Graphics2D g2d = bimg.createGraphics(); 510 g2d.setComposite(AlphaComposite.Clear); 511 g2d.fillRect(0, 0, bw, bh); 512 g2d.dispose(); 513 } 514 } 515 if (bimg == null) { 516 bimg = new BufferedImage(iw, ih, prefBimgType); 517 } 518 DataBufferInt db = (DataBufferInt)bimg.getRaster().getDataBuffer(); 519 int data[] = db.getData(); 520 int offset = bimg.getRaster().getDataBuffer().getOffset(); 521 int scan = 0; 522 SampleModel sm = bimg.getRaster().getSampleModel(); 523 if (sm instanceof SinglePixelPackedSampleModel) { 524 scan = ((SinglePixelPackedSampleModel)sm).getScanlineStride(); 525 } 526 527 WritablePixelFormat<IntBuffer> pf = getAssociatedPixelFormat(bimg); 528 pr.getPixels(0, 0, iw, ih, pf, data, offset, scan); 529 return bimg; 530 } 531 532 // Method to implement the following via reflection: 533 // SwingFXUtils.fromFXImage(img, null) 534 public static BufferedImage toBufferedImage(Image img) { 535 try { 536 return fromFXImage(img, null); 537 } catch (Exception ex) { 538 ex.printStackTrace(System.err); 539 } 540 541 // return null upon any exception 542 return null; 543 } 544 545 }