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