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