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 
 317         if (extParams != null) {
 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         }
 340         return new FormControlRef(fc);
 341     }
 342 
 343     private String getMeterStyle(int region) {
 344         // see GaugeRegion in HTMLMeterElement.h
 345         switch (region) {
 346             case 1: // GaugeRegionSuboptimal
 347                 return "-fx-accent: yellow";
 348             case 2: // GaugeRegionEvenLessGood
 349                 return "-fx-accent: red";
 350             default: // GaugeRegionOptimum
 351                 return "-fx-accent: green";
 352         }
 353     }
 354 
 355     @Override
 356     public void drawWidget(
 357         WCGraphicsContext g,
 358         final Ref widget,
 359         int x, int y)
 360     {
 361         ensureNotDefault();
 362 
 363         FormControl fcontrol = ((FormControlRef) widget).asFormControl();
 364         if (fcontrol != null) {
 365             Control control = fcontrol.asControl();
 366             if (control != null) {
 367                 g.saveState();
 368                 g.translate(x, y);
 369                 Renderer.getRenderer().render(control, g);
 370                 g.restoreState();
 371             }
 372         }
 373     }
 374 
 375     @Override
 376     public WCSize getWidgetSize(Ref widget) {
 377         ensureNotDefault();
 378 
 379         FormControl fcontrol = ((FormControlRef)widget).asFormControl();
 380         if (fcontrol != null) {
 381             Control control = fcontrol.asControl();
 382             return new WCSize((float)control.getWidth(), (float)control.getHeight());
 383         }
 384         return new WCSize(0, 0);
 385     }
 386 
 387     @Override
 388     protected int getRadioButtonSize() {
 389         String style = Application.getUserAgentStylesheet();
 390         if (Application.STYLESHEET_MODENA.equalsIgnoreCase(style)) {
 391             return 20; // 18 + 2; size + focus outline
 392         } else if (Application.STYLESHEET_CASPIAN.equalsIgnoreCase(style)) {
 393             return 19; // 16 + 3; size + focus outline
 394         }
 395         return 20;
 396     }
 397 
 398     // TODO: get theme value
 399     @Override
 400     protected int getSelectionColor(int index) {
 401         switch (index) {
 402             case BACKGROUND: return 0xff0093ff;
 403             case FOREGROUND: return 0xffffffff;
 404             default: return 0;
 405         }
 406     }
 407 
 408     private static boolean hasState(int state, int mask) {
 409         return (state & mask) != 0;
 410     }
 411 
 412     private static final class FormControlRef extends Ref {
 413         private final WeakReference<FormControl> fcRef;
 414 
 415         private FormControlRef(FormControl fc) {
 416             this.fcRef = new WeakReference<FormControl>(fc);
 417         }
 418 
 419         private FormControl asFormControl() {
 420             return fcRef.get();
 421         }
 422     }
 423 
 424     interface Widget {
 425         public WidgetType getType();
 426     }
 427 
 428     private interface FormControl extends Widget {
 429         public Control asControl();
 430         public void setState(int state);
 431     }
 432 
 433     private static final class FormButton extends Button implements FormControl {
 434 
 435         @Override public Control asControl() { return this; }
 436 
 437         @Override public void setState(int state) {
 438             setDisabled(! hasState(state, RenderTheme.ENABLED));
 439             setFocused(hasState(state, RenderTheme.FOCUSED));
 440             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 441             setPressed(hasState(state, RenderTheme.PRESSED));
 442             if (isPressed()) arm(); else disarm();
 443         }
 444 
 445         @Override public WidgetType getType() { return WidgetType.BUTTON; };
 446     }
 447 
 448     private static final class FormTextField extends TextField implements FormControl {
 449 
 450         private FormTextField() {
 451             setStyle("-fx-display-caret: false");
 452         }
 453 
 454         @Override public Control asControl() { return this; }
 455 
 456         @Override public void setState(int state) {
 457             setDisabled(! hasState(state, RenderTheme.ENABLED));
 458             setEditable(hasState(state, RenderTheme.READ_ONLY));
 459             setFocused(hasState(state, RenderTheme.FOCUSED));
 460             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 461         }
 462 
 463         @Override public WidgetType getType() { return WidgetType.TEXTFIELD; };
 464     }
 465 
 466     private static final class FormCheckBox extends CheckBox implements FormControl {
 467 
 468         @Override public Control asControl() { return this; }
 469 
 470         @Override public void setState(int state) {
 471             setDisabled(! hasState(state, RenderTheme.ENABLED));
 472             setFocused(hasState(state, RenderTheme.FOCUSED));
 473             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 474             setSelected(hasState(state, RenderTheme.CHECKED));
 475         }
 476 
 477         @Override public WidgetType getType() { return WidgetType.CHECKBOX; };
 478     }
 479 
 480     private static final class FormRadioButton extends RadioButton implements FormControl {
 481 
 482         @Override public Control asControl() { return this; }
 483 
 484         @Override public void setState(int state) {
 485             setDisabled(! hasState(state, RenderTheme.ENABLED));
 486             setFocused(hasState(state, RenderTheme.FOCUSED));
 487             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 488             setSelected(hasState(state, RenderTheme.CHECKED));
 489         }
 490 
 491         @Override public WidgetType getType() { return WidgetType.RADIOBUTTON; };
 492     }
 493 
 494     private static final class FormSlider extends Slider implements FormControl {
 495 
 496         @Override public Control asControl() { return this; }
 497 
 498         @Override public void setState(int state) {
 499             setDisabled(! hasState(state, RenderTheme.ENABLED));
 500             setFocused(hasState(state, RenderTheme.FOCUSED));
 501             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 502         }
 503 
 504         @Override public WidgetType getType() { return WidgetType.SLIDER; };
 505     }
 506 
 507     private static final class FormProgressBar extends ProgressBar implements FormControl {
 508         private final WidgetType type;
 509 
 510         private FormProgressBar(WidgetType type) {
 511             this.type = type;
 512         }
 513 
 514         @Override public Control asControl() { return this; }
 515 
 516         @Override public void setState(int state) {
 517             setDisabled(! hasState(state, RenderTheme.ENABLED));
 518             setFocused(hasState(state, RenderTheme.FOCUSED));
 519             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 520         }
 521 
 522         @Override public WidgetType getType() { return type; };
 523     }
 524 
 525     private static final class FormMenuList extends ChoiceBox implements FormControl {
 526 
 527         private FormMenuList() {
 528             // Adding a dummy item to please ChoiceBox.
 529             List<String> l = new ArrayList<String>();
 530             l.add("");
 531             setItems(FXCollections.observableList(l));
 532         }
 533 
 534         @Override public Control asControl() { return this; }
 535 
 536         @Override public void setState(int state) {
 537             setDisabled(! hasState(state, RenderTheme.ENABLED));
 538             setFocused(hasState(state, RenderTheme.FOCUSED));
 539             setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled());
 540         }
 541 
 542         @Override public WidgetType getType() { return WidgetType.MENULIST; };
 543     }
 544 
 545     private static final class FormMenuListButton extends Button implements FormControl {
 546 
 547         private static final int MAX_WIDTH = 20;
 548         private static final int MIN_WIDTH = 16;
 549 
 550         @Override public Control asControl() { return this; }
 551 
 552         @Override public void setState(int state) {
 553             setDisabled(! hasState(state, RenderTheme.ENABLED));
 554             setHover(hasState(state, RenderTheme.HOVERED));
 555             setPressed(hasState(state, RenderTheme.PRESSED));
 556             if (isPressed()) arm(); else disarm();
 557         }
 558 
 559         private FormMenuListButton() {
 560             setSkin(new Skin());
 561             setFocusTraversable(false);
 562             getStyleClass().add("form-select-button");
 563         }
 564 
 565         /**
 566          * @param height is the height of the FormMenuList widget
 567          * @param width is passed equal to height
 568          */
 569         @Override public void resize(double width, double height) {
 570             width = height > MAX_WIDTH ? MAX_WIDTH : height < MIN_WIDTH ? MIN_WIDTH : height;
 571 
 572             super.resize(width, height);
 573 
 574             // [x] is originally aligned with the right edge of
 575             // the menulist control, and here we adjust it
 576             setTranslateX(-width);
 577         }
 578 
 579         private final class Skin extends SkinBase {
 580             Skin() {
 581                 super(FormMenuListButton.this);
 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 }