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 }