1 /*
   2  * Copyright (c) 2011, 2018, 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.theme;
  27 
  28 import javafx.beans.InvalidationListener;
  29 import javafx.beans.Observable;
  30 import javafx.collections.FXCollections;
  31 import javafx.geometry.Orientation;
  32 import javafx.scene.control.Button;
  33 import javafx.scene.control.CheckBox;
  34 import javafx.scene.control.ChoiceBox;
  35 import javafx.scene.control.Control;
  36 import javafx.scene.control.ProgressBar;
  37 import javafx.scene.control.RadioButton;
  38 import javafx.scene.control.SkinBase;
  39 import javafx.scene.control.Slider;
  40 import javafx.scene.control.TextField;
  41 import javafx.scene.layout.BorderPane;
  42 import javafx.scene.layout.Region;
  43 import java.lang.ref.WeakReference;
  44 import java.nio.ByteBuffer;
  45 import java.nio.ByteOrder;
  46 import java.util.ArrayList;
  47 import java.util.HashMap;
  48 import java.util.List;
  49 import java.util.Map;
  50 import java.util.LinkedHashMap;
  51 import com.sun.javafx.logging.PlatformLogger;
  52 import com.sun.javafx.logging.PlatformLogger.Level;
  53 import com.sun.javafx.webkit.Accessor;
  54 import com.sun.webkit.LoadListenerClient;
  55 import com.sun.webkit.graphics.Ref;
  56 import com.sun.webkit.graphics.RenderTheme;
  57 import com.sun.webkit.graphics.WCGraphicsContext;
  58 import com.sun.webkit.graphics.WCSize;
  59 import javafx.application.Application;
  60 
  61 public final class RenderThemeImpl extends RenderTheme {
  62     private final static PlatformLogger log = PlatformLogger.getLogger(RenderThemeImpl.class.getName());
  63 
  64     enum WidgetType {
  65         TEXTFIELD      (0),
  66         BUTTON         (1),
  67         CHECKBOX       (2),
  68         RADIOBUTTON    (3),
  69         MENULIST       (4),
  70         MENULISTBUTTON (5),
  71         SLIDER         (6),
  72         PROGRESSBAR    (7),
  73         METER          (8),
  74         SCROLLBAR      (9);
  75 
  76         private static final HashMap<Integer, WidgetType> map = new HashMap<Integer, WidgetType>();
  77         private final int value;
  78 
  79         private WidgetType(int value) { this.value = value; }
  80 
  81         static { for (WidgetType v: values()) map.put(v.value, v); }
  82 
  83         private static WidgetType convert(int index) { return map.get(index); }
  84     };
  85 
  86     private Accessor accessor;
  87     private boolean isDefault; // indicates if the instance is used in non-page context
  88 
  89     private Pool<FormControl> pool;
  90 
  91     /**
  92      * A pool of controls.
  93      * Based on a hash map where a control is the value and its ID is the key.
  94      * The pool size is limited. When a new control is added to the pool and
  95      * the limit is reached, a control which is used most rarely is removed.
  96      */
  97     static final class Pool<T extends Widget> {
  98         private static final int INITIAL_CAPACITY = 100;
  99 
 100         private int capacity = INITIAL_CAPACITY;
 101 
 102         // A map of control IDs used to track the rate of accociated controls
 103         // based on their "popularity".
 104         // Maps an ID to an updateContentCycleID corresponding to the cycle
 105         // at which the control was added to the pool.
 106         private final LinkedHashMap<Long, Integer> ids = new LinkedHashMap<>();
 107 
 108         // A map b/w the IDs and associated controls.
 109         // The {@code ids} map is kept in sync with the set of keys.
 110         private final Map<Long, WeakReference<T>> pool = new HashMap<>();
 111 
 112         private final Notifier<T> notifier;
 113         private final String type; // used for logging
 114 
 115         /**
 116          * An interface used to notify the implementor of removal
 117          * of a control from the pool.
 118          */
 119         interface Notifier<T> {
 120             public void notifyRemoved(T control);
 121         }
 122 
 123         Pool(Notifier<T> notifier, Class<T> type) {
 124             this.notifier = notifier;
 125             this.type = type.getSimpleName();
 126         }
 127 
 128         T get(long id) {
 129             if (log.isLoggable(Level.FINE)) {
 130                 log.fine("type: {0}, size: {1}, id: 0x{2}",
 131                         new Object[] {type, pool.size(), Long.toHexString(id)});
 132             }
 133             assert ids.size() == pool.size();
 134 
 135             WeakReference<T> controlRef = pool.get(id);
 136             if (controlRef == null) {
 137                 return null;
 138             }
 139 
 140             T control = controlRef.get();
 141             if (control == null) {
 142                 return null;
 143             }
 144 
 145             // "Bubble" the id.
 146             Integer value = ids.remove(Long.valueOf(id));
 147             ids.put(id, value);
 148 
 149             return control;
 150         }
 151 
 152         void put(long id, T control, int updateContentCycleID) {
 153             if (log.isLoggable(Level.FINEST)) {
 154                 log.finest("size: {0}, id: 0x{1}, control: {2}",
 155                         new Object[] {pool.size(), Long.toHexString(id), control.getType()});
 156             }
 157             if (ids.size() >= capacity) {
 158                 // Pull a control from the bottom of the map, least used.
 159                 Long _id = ids.keySet().iterator().next();
 160                 Integer cycleID = ids.get(_id);
 161                 // Remove that "unpopular" control in case it wasn't added
 162                 // during the current update cycle.
 163                 if (cycleID != updateContentCycleID) {
 164                     ids.remove(_id);
 165                     T _control = pool.remove(_id).get();
 166                     if (_control != null) {
 167                         notifier.notifyRemoved(_control);
 168                     }
 169                 // Otherwise, double the pool capacity.
 170                 } else {
 171                     capacity = Math.min(capacity, (int)Math.ceil(Integer.MAX_VALUE/2)) * 2;
 172                 }
 173             }
 174             ids.put(id, updateContentCycleID);
 175             pool.put(id, new WeakReference<T>(control));
 176         }
 177 
 178         void clear() {
 179             if (log.isLoggable(Level.FINE)) {
 180                 log.fine("size: " + pool.size() + ", controls: " + pool.values());
 181             }
 182             if (pool.size() == 0) {
 183                 return;
 184             }
 185             ids.clear();
 186             for (WeakReference<T> controlRef : pool.values()) {
 187                 T control = controlRef.get();
 188                 if (control != null) {
 189                     notifier.notifyRemoved(control);
 190                 }
 191             }
 192             pool.clear();
 193             capacity = INITIAL_CAPACITY;
 194         }
 195     }
 196 
 197     static class ViewListener implements InvalidationListener {
 198         private final Pool pool;
 199         private final Accessor accessor;
 200         private LoadListenerClient loadListener;
 201 
 202         ViewListener(Pool pool, Accessor accessor) {
 203             this.pool = pool;
 204             this.accessor = accessor;
 205         }
 206 
 207         @Override public void invalidated(Observable ov) {
 208             pool.clear(); // clear the pool when WebView changes
 209 
 210             // Add the LoadListenerClient when the page is available.
 211             if (accessor.getPage() != null && loadListener == null) {
 212                 loadListener = new LoadListenerClient() {
 213                     @Override
 214                     public void dispatchLoadEvent(long frame, int state, String url,
 215                                                   String contentType, double progress, int errorCode)
 216                     {
 217                         if (state == LoadListenerClient.PAGE_STARTED) {
 218                             // An html page with new content is being loaded.
 219                             // Clear the controls associated with the previous html page.
 220                             pool.clear();
 221                         }
 222                     }
 223                     @Override
 224                     public void dispatchResourceLoadEvent(long frame, int state, String url,
 225                                                           String contentType, double progress,
 226                                                           int errorCode) {}
 227                 };
 228                 accessor.getPage().addLoadListenerClient(loadListener);
 229             }
 230         }
 231     }
 232 
 233     public RenderThemeImpl(final Accessor accessor) {
 234         this.accessor = accessor;
 235         pool = new Pool<FormControl>(fc -> {
 236             // Remove the control from WebView when it's removed from the pool.
 237             accessor.removeChild(fc.asControl());
 238         }, FormControl.class);
 239         accessor.addViewListener(new ViewListener(pool, accessor));
 240     }
 241 
 242     public RenderThemeImpl() {
 243         isDefault = true;
 244     }
 245 
 246     private void ensureNotDefault() {
 247         if (isDefault) {
 248             throw new IllegalStateException("the method should not be called in this context");
 249         }
 250     }
 251 
 252     @Override
 253     protected Ref createWidget(
 254         long id,
 255         int widgetIndex,
 256         int state,
 257         int w, int h,
 258         int bgColor,
 259         ByteBuffer extParams)
 260     {
 261         ensureNotDefault();
 262 
 263         FormControl fc = pool.get(id);
 264         WidgetType type = WidgetType.convert(widgetIndex);
 265 
 266         if (fc == null || fc.getType() != type) {
 267             if (fc  != null) {
 268                 // Remove the unmatching control.
 269                 accessor.removeChild(fc.asControl());
 270             }
 271             switch (type) {
 272                 case TEXTFIELD:
 273                     fc = new FormTextField();
 274                     break;
 275                 case BUTTON:
 276                     fc = new FormButton();
 277                     break;
 278                 case CHECKBOX:
 279                     fc = new FormCheckBox();
 280                     break;
 281                 case RADIOBUTTON:
 282                     fc = new FormRadioButton();
 283                     break;
 284                 case MENULIST:
 285                     fc = new FormMenuList();
 286                     break;
 287                 case MENULISTBUTTON:
 288                     fc = new FormMenuListButton();
 289                     break;
 290                 case SLIDER:
 291                     fc = new FormSlider();
 292                     break;
 293                 case PROGRESSBAR:
 294                     fc = new FormProgressBar(WidgetType.PROGRESSBAR);
 295                     break;
 296                 case METER:
 297                     fc = new FormProgressBar(WidgetType.METER);
 298                     break;
 299                 default:
 300                     log.severe("unknown widget index: {0}", widgetIndex);
 301                     return null;
 302             }
 303             fc.asControl().setFocusTraversable(false);
 304             pool.put(id, fc, accessor.getPage().getUpdateContentCycleID()); // put or replace the entry
 305             accessor.addChild(fc.asControl());
 306         }
 307 
 308         fc.setState(state);
 309         Control ctrl = fc.asControl();
 310         if (ctrl.getWidth() != w || ctrl.getHeight() != h) {
 311             ctrl.resize(w, h);
 312         }
 313         if (ctrl.isManaged()) {
 314             ctrl.setManaged(false);
 315         }
 316         if (type == WidgetType.SLIDER) {
 317             Slider slider = (Slider)ctrl;
 318             extParams.order(ByteOrder.nativeOrder());
 319             slider.setOrientation(extParams.getInt()==0
 320                 ? Orientation.HORIZONTAL
 321                 : Orientation.VERTICAL);
 322             slider.setMax(extParams.getFloat());
 323             slider.setMin(extParams.getFloat());
 324             slider.setValue(extParams.getFloat());
 325         } else if (type == WidgetType.PROGRESSBAR) {
 326             ProgressBar progress = (ProgressBar)ctrl;
 327             extParams.order(ByteOrder.nativeOrder());
 328             progress.setProgress(extParams.getInt() == 1
 329                     ? extParams.getFloat()
 330                     : progress.INDETERMINATE_PROGRESS);
 331         } else if (type == WidgetType.METER) {
 332             ProgressBar progress = (ProgressBar) ctrl;
 333             extParams.order(ByteOrder.nativeOrder());
 334             progress.setProgress(extParams.getFloat());
 335             progress.setStyle(getMeterStyle(extParams.getInt()));
 336         }
 337         return new FormControlRef(fc);
 338     }
 339 
 340     private String getMeterStyle(int region) {
 341         // see GaugeRegion in HTMLMeterElement.h
 342         switch (region) {
 343             case 1: // GaugeRegionSuboptimal
 344                 return "-fx-accent: yellow";
 345             case 2: // GaugeRegionEvenLessGood
 346                 return "-fx-accent: red";
 347             default: // GaugeRegionOptimum
 348                 return "-fx-accent: green";
 349         }
 350     }
 351 
 352     @Override
 353     public void drawWidget(
 354         WCGraphicsContext g,
 355         final Ref widget,
 356         int x, int y)
 357     {
 358         ensureNotDefault();
 359 
 360         FormControl fcontrol = ((FormControlRef) widget).asFormControl();
 361         if (fcontrol != null) {
 362             Control control = fcontrol.asControl();
 363             if (control != null) {
 364                 g.saveState();
 365                 g.translate(x, y);
 366                 Renderer.getRenderer().render(control, g);
 367                 g.restoreState();
 368             }
 369         }
 370     }
 371 
 372     @Override
 373     public WCSize getWidgetSize(Ref widget) {
 374         ensureNotDefault();
 375 
 376         FormControl fcontrol = ((FormControlRef)widget).asFormControl();
 377         if (fcontrol != null) {
 378             Control control = fcontrol.asControl();
 379             return new WCSize((float)control.getWidth(), (float)control.getHeight());
 380         }
 381         return new WCSize(0, 0);
 382     }
 383 
 384     @Override
 385     protected int getRadioButtonSize() {
 386         String style = Application.getUserAgentStylesheet();
 387         if (Application.STYLESHEET_MODENA.equalsIgnoreCase(style)) {
 388             return 20; // 18 + 2; size + focus outline
 389         } else if (Application.STYLESHEET_CASPIAN.equalsIgnoreCase(style)) {
 390             return 19; // 16 + 3; size + focus outline
 391         }
 392         return 20;
 393     }
 394 
 395     // TODO: get theme value
 396     @Override
 397     protected int getSelectionColor(int index) {
 398         switch (index) {
 399             case BACKGROUND: return 0xff0093ff;
 400             case FOREGROUND: return 0xffffffff;
 401             default: return 0;
 402         }
 403     }
 404 
 405     private static boolean hasState(int state, int mask) {
 406         return (state & mask) != 0;
 407     }
 408 
 409     private static final class FormControlRef extends Ref {
 410         private final WeakReference<FormControl> fcRef;
 411 
 412         private FormControlRef(FormControl fc) {
 413             this.fcRef = new WeakReference<FormControl>(fc);
 414         }
 415 
 416         private FormControl asFormControl() {
 417             return fcRef.get();
 418         }
 419     }
 420 
 421     interface Widget {
 422         public WidgetType getType();
 423     }
 424 
 425     private interface FormControl extends Widget {
 426         public Control asControl();
 427         public void setState(int state);
 428     }
 429 
 430     private static final class FormButton extends Button implements FormControl {
 431 
 432         @Override public Control asControl() { return this; }
 433 
 434         @Override public void setState(int state) {
 435             setDisabled(! hasState(state, RenderTheme.ENABLED));
 436             setFocused(hasState(state, RenderTheme.FOCUSED));
 437             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 438             setPressed(hasState(state, RenderTheme.PRESSED));
 439             if (isPressed()) arm(); else disarm();
 440         }
 441 
 442         @Override public WidgetType getType() { return WidgetType.BUTTON; };
 443     }
 444 
 445     private static final class FormTextField extends TextField implements FormControl {
 446 
 447         private FormTextField() {
 448             setStyle("-fx-display-caret: false");
 449         }
 450 
 451         @Override public Control asControl() { return this; }
 452 
 453         @Override public void setState(int state) {
 454             setDisabled(! hasState(state, RenderTheme.ENABLED));
 455             setEditable(hasState(state, RenderTheme.READ_ONLY));
 456             setFocused(hasState(state, RenderTheme.FOCUSED));
 457             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 458         }
 459 
 460         @Override public WidgetType getType() { return WidgetType.TEXTFIELD; };
 461     }
 462 
 463     private static final class FormCheckBox extends CheckBox implements FormControl {
 464 
 465         @Override public Control asControl() { return this; }
 466 
 467         @Override public void setState(int state) {
 468             setDisabled(! hasState(state, RenderTheme.ENABLED));
 469             setFocused(hasState(state, RenderTheme.FOCUSED));
 470             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 471             setSelected(hasState(state, RenderTheme.CHECKED));
 472         }
 473 
 474         @Override public WidgetType getType() { return WidgetType.CHECKBOX; };
 475     }
 476 
 477     private static final class FormRadioButton extends RadioButton implements FormControl {
 478 
 479         @Override public Control asControl() { return this; }
 480 
 481         @Override public void setState(int state) {
 482             setDisabled(! hasState(state, RenderTheme.ENABLED));
 483             setFocused(hasState(state, RenderTheme.FOCUSED));
 484             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 485             setSelected(hasState(state, RenderTheme.CHECKED));
 486         }
 487 
 488         @Override public WidgetType getType() { return WidgetType.RADIOBUTTON; };
 489     }
 490 
 491     private static final class FormSlider extends Slider implements FormControl {
 492 
 493         @Override public Control asControl() { return this; }
 494 
 495         @Override public void setState(int state) {
 496             setDisabled(! hasState(state, RenderTheme.ENABLED));
 497             setFocused(hasState(state, RenderTheme.FOCUSED));
 498             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 499         }
 500 
 501         @Override public WidgetType getType() { return WidgetType.SLIDER; };
 502     }
 503 
 504     private static final class FormProgressBar extends ProgressBar implements FormControl {
 505         private final WidgetType type;
 506 
 507         private FormProgressBar(WidgetType type) {
 508             this.type = type;
 509         }
 510 
 511         @Override public Control asControl() { return this; }
 512 
 513         @Override public void setState(int state) {
 514             setDisabled(! hasState(state, RenderTheme.ENABLED));
 515             setFocused(hasState(state, RenderTheme.FOCUSED));
 516             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 517         }
 518 
 519         @Override public WidgetType getType() { return type; };
 520     }
 521 
 522     private static final class FormMenuList extends ChoiceBox implements FormControl {
 523 
 524         private FormMenuList() {
 525             // Adding a dummy item to please ChoiceBox.
 526             List<String> l = new ArrayList<String>();
 527             l.add("");
 528             setItems(FXCollections.observableList(l));
 529         }
 530 
 531         @Override public Control asControl() { return this; }
 532 
 533         @Override public void setState(int state) {
 534             setDisabled(! hasState(state, RenderTheme.ENABLED));
 535             setFocused(hasState(state, RenderTheme.FOCUSED));
 536             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 537         }
 538 
 539         @Override public WidgetType getType() { return WidgetType.MENULIST; };
 540     }
 541 
 542     private static final class FormMenuListButton extends Button implements FormControl {
 543 
 544         private static final int MAX_WIDTH = 20;
 545         private static final int MIN_WIDTH = 16;
 546 
 547         @Override public Control asControl() { return this; }
 548 
 549         @Override public void setState(int state) {
 550             setDisabled(! hasState(state, RenderTheme.ENABLED));
 551             setHover(hasState(state, RenderTheme.HOVERED));
 552             setPressed(hasState(state, RenderTheme.PRESSED));
 553             if (isPressed()) arm(); else disarm();
 554         }
 555 
 556         private FormMenuListButton() {
 557             setSkin(new Skin());
 558             setFocusTraversable(false);
 559             getStyleClass().add("form-select-button");
 560         }
 561 
 562         /**
 563          * @param height is the height of the FormMenuList widget
 564          * @param width is passed equal to height
 565          */
 566         @Override public void resize(double width, double height) {
 567             width = height > MAX_WIDTH ? MAX_WIDTH : height < MIN_WIDTH ? MIN_WIDTH : height;
 568 
 569             super.resize(width, height);
 570 
 571             // [x] is originally aligned with the right edge of
 572             // the menulist control, and here we adjust it
 573             setTranslateX(-width);
 574         }
 575 
 576         private final class Skin extends SkinBase {
 577             Skin() {
 578                 super(FormMenuListButton.this);
 579 
 580                 Region arrow = new Region();
 581                 arrow.getStyleClass().add("arrow");
 582                 arrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
 583                 BorderPane pane = new BorderPane();
 584                 pane.setCenter(arrow);
 585                 getChildren().add(pane);
 586             }
 587         }
 588 
 589         @Override public WidgetType getType() { return WidgetType.MENULISTBUTTON; };
 590     }
 591 }