/* * Copyright (c) 2011, 2018, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.javafx.webkit.theme; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.collections.FXCollections; import javafx.geometry.Orientation; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; import javafx.scene.control.Control; import javafx.scene.control.ProgressBar; import javafx.scene.control.RadioButton; import javafx.scene.control.SkinBase; import javafx.scene.control.Slider; import javafx.scene.control.TextField; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.LinkedHashMap; import com.sun.javafx.logging.PlatformLogger; import com.sun.javafx.logging.PlatformLogger.Level; import com.sun.javafx.webkit.Accessor; import com.sun.webkit.LoadListenerClient; import com.sun.webkit.graphics.Ref; import com.sun.webkit.graphics.RenderTheme; import com.sun.webkit.graphics.WCGraphicsContext; import com.sun.webkit.graphics.WCSize; import javafx.application.Application; public final class RenderThemeImpl extends RenderTheme { private final static PlatformLogger log = PlatformLogger.getLogger(RenderThemeImpl.class.getName()); enum WidgetType { TEXTFIELD (0), BUTTON (1), CHECKBOX (2), RADIOBUTTON (3), MENULIST (4), MENULISTBUTTON (5), SLIDER (6), PROGRESSBAR (7), METER (8), SCROLLBAR (9); private static final HashMap map = new HashMap(); private final int value; private WidgetType(int value) { this.value = value; } static { for (WidgetType v: values()) map.put(v.value, v); } private static WidgetType convert(int index) { return map.get(index); } }; private Accessor accessor; private boolean isDefault; // indicates if the instance is used in non-page context private Pool pool; /** * A pool of controls. * Based on a hash map where a control is the value and its ID is the key. * The pool size is limited. When a new control is added to the pool and * the limit is reached, a control which is used most rarely is removed. */ static final class Pool { private static final int INITIAL_CAPACITY = 100; private int capacity = INITIAL_CAPACITY; // A map of control IDs used to track the rate of accociated controls // based on their "popularity". // Maps an ID to an updateContentCycleID corresponding to the cycle // at which the control was added to the pool. private final LinkedHashMap ids = new LinkedHashMap<>(); // A map b/w the IDs and associated controls. // The {@code ids} map is kept in sync with the set of keys. private final Map> pool = new HashMap<>(); private final Notifier notifier; private final String type; // used for logging /** * An interface used to notify the implementor of removal * of a control from the pool. */ interface Notifier { public void notifyRemoved(T control); } Pool(Notifier notifier, Class type) { this.notifier = notifier; this.type = type.getSimpleName(); } T get(long id) { if (log.isLoggable(Level.FINE)) { log.fine("type: {0}, size: {1}, id: 0x{2}", new Object[] {type, pool.size(), Long.toHexString(id)}); } assert ids.size() == pool.size(); WeakReference controlRef = pool.get(id); if (controlRef == null) { return null; } T control = controlRef.get(); if (control == null) { return null; } // "Bubble" the id. Integer value = ids.remove(Long.valueOf(id)); ids.put(id, value); return control; } void put(long id, T control, int updateContentCycleID) { if (log.isLoggable(Level.FINEST)) { log.finest("size: {0}, id: 0x{1}, control: {2}", new Object[] {pool.size(), Long.toHexString(id), control.getType()}); } if (ids.size() >= capacity) { // Pull a control from the bottom of the map, least used. Long _id = ids.keySet().iterator().next(); Integer cycleID = ids.get(_id); // Remove that "unpopular" control in case it wasn't added // during the current update cycle. if (cycleID != updateContentCycleID) { ids.remove(_id); T _control = pool.remove(_id).get(); if (_control != null) { notifier.notifyRemoved(_control); } // Otherwise, double the pool capacity. } else { capacity = Math.min(capacity, (int)Math.ceil(Integer.MAX_VALUE/2)) * 2; } } ids.put(id, updateContentCycleID); pool.put(id, new WeakReference(control)); } void clear() { if (log.isLoggable(Level.FINE)) { log.fine("size: " + pool.size() + ", controls: " + pool.values()); } if (pool.size() == 0) { return; } ids.clear(); for (WeakReference controlRef : pool.values()) { T control = controlRef.get(); if (control != null) { notifier.notifyRemoved(control); } } pool.clear(); capacity = INITIAL_CAPACITY; } } static class ViewListener implements InvalidationListener { private final Pool pool; private final Accessor accessor; private LoadListenerClient loadListener; ViewListener(Pool pool, Accessor accessor) { this.pool = pool; this.accessor = accessor; } @Override public void invalidated(Observable ov) { pool.clear(); // clear the pool when WebView changes // Add the LoadListenerClient when the page is available. if (accessor.getPage() != null && loadListener == null) { loadListener = new LoadListenerClient() { @Override public void dispatchLoadEvent(long frame, int state, String url, String contentType, double progress, int errorCode) { if (state == LoadListenerClient.PAGE_STARTED) { // An html page with new content is being loaded. // Clear the controls associated with the previous html page. pool.clear(); } } @Override public void dispatchResourceLoadEvent(long frame, int state, String url, String contentType, double progress, int errorCode) {} }; accessor.getPage().addLoadListenerClient(loadListener); } } } public RenderThemeImpl(final Accessor accessor) { this.accessor = accessor; pool = new Pool(fc -> { // Remove the control from WebView when it's removed from the pool. accessor.removeChild(fc.asControl()); }, FormControl.class); accessor.addViewListener(new ViewListener(pool, accessor)); } public RenderThemeImpl() { isDefault = true; } private void ensureNotDefault() { if (isDefault) { throw new IllegalStateException("the method should not be called in this context"); } } @Override protected Ref createWidget( long id, int widgetIndex, int state, int w, int h, int bgColor, ByteBuffer extParams) { ensureNotDefault(); FormControl fc = pool.get(id); WidgetType type = WidgetType.convert(widgetIndex); if (fc == null || fc.getType() != type) { if (fc != null) { // Remove the unmatching control. accessor.removeChild(fc.asControl()); } switch (type) { case TEXTFIELD: fc = new FormTextField(); break; case BUTTON: fc = new FormButton(); break; case CHECKBOX: fc = new FormCheckBox(); break; case RADIOBUTTON: fc = new FormRadioButton(); break; case MENULIST: fc = new FormMenuList(); break; case MENULISTBUTTON: fc = new FormMenuListButton(); break; case SLIDER: fc = new FormSlider(); break; case PROGRESSBAR: fc = new FormProgressBar(WidgetType.PROGRESSBAR); break; case METER: fc = new FormProgressBar(WidgetType.METER); break; default: log.severe("unknown widget index: {0}", widgetIndex); return null; } fc.asControl().setFocusTraversable(false); pool.put(id, fc, accessor.getPage().getUpdateContentCycleID()); // put or replace the entry accessor.addChild(fc.asControl()); } fc.setState(state); Control ctrl = fc.asControl(); if (ctrl.getWidth() != w || ctrl.getHeight() != h) { ctrl.resize(w, h); } if (ctrl.isManaged()) { ctrl.setManaged(false); } if (extParams != null) { if (type == WidgetType.SLIDER) { Slider slider = (Slider)ctrl; extParams.order(ByteOrder.nativeOrder()); slider.setOrientation(extParams.getInt()==0 ? Orientation.HORIZONTAL : Orientation.VERTICAL); slider.setMax(extParams.getFloat()); slider.setMin(extParams.getFloat()); slider.setValue(extParams.getFloat()); } else if (type == WidgetType.PROGRESSBAR) { ProgressBar progress = (ProgressBar)ctrl; extParams.order(ByteOrder.nativeOrder()); progress.setProgress(extParams.getInt() == 1 ? extParams.getFloat() : progress.INDETERMINATE_PROGRESS); } else if (type == WidgetType.METER) { ProgressBar progress = (ProgressBar) ctrl; extParams.order(ByteOrder.nativeOrder()); progress.setProgress(extParams.getFloat()); progress.setStyle(getMeterStyle(extParams.getInt())); } } return new FormControlRef(fc); } private String getMeterStyle(int region) { // see GaugeRegion in HTMLMeterElement.h switch (region) { case 1: // GaugeRegionSuboptimal return "-fx-accent: yellow"; case 2: // GaugeRegionEvenLessGood return "-fx-accent: red"; default: // GaugeRegionOptimum return "-fx-accent: green"; } } @Override public void drawWidget( WCGraphicsContext g, final Ref widget, int x, int y) { ensureNotDefault(); FormControl fcontrol = ((FormControlRef) widget).asFormControl(); if (fcontrol != null) { Control control = fcontrol.asControl(); if (control != null) { g.saveState(); g.translate(x, y); Renderer.getRenderer().render(control, g); g.restoreState(); } } } @Override public WCSize getWidgetSize(Ref widget) { ensureNotDefault(); FormControl fcontrol = ((FormControlRef)widget).asFormControl(); if (fcontrol != null) { Control control = fcontrol.asControl(); return new WCSize((float)control.getWidth(), (float)control.getHeight()); } return new WCSize(0, 0); } @Override protected int getRadioButtonSize() { String style = Application.getUserAgentStylesheet(); if (Application.STYLESHEET_MODENA.equalsIgnoreCase(style)) { return 20; // 18 + 2; size + focus outline } else if (Application.STYLESHEET_CASPIAN.equalsIgnoreCase(style)) { return 19; // 16 + 3; size + focus outline } return 20; } // TODO: get theme value @Override protected int getSelectionColor(int index) { switch (index) { case BACKGROUND: return 0xff0093ff; case FOREGROUND: return 0xffffffff; default: return 0; } } private static boolean hasState(int state, int mask) { return (state & mask) != 0; } private static final class FormControlRef extends Ref { private final WeakReference fcRef; private FormControlRef(FormControl fc) { this.fcRef = new WeakReference(fc); } private FormControl asFormControl() { return fcRef.get(); } } interface Widget { public WidgetType getType(); } private interface FormControl extends Widget { public Control asControl(); public void setState(int state); } private static final class FormButton extends Button implements FormControl { @Override public Control asControl() { return this; } @Override public void setState(int state) { setDisabled(! hasState(state, RenderTheme.ENABLED)); setFocused(hasState(state, RenderTheme.FOCUSED)); setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled()); setPressed(hasState(state, RenderTheme.PRESSED)); if (isPressed()) arm(); else disarm(); } @Override public WidgetType getType() { return WidgetType.BUTTON; }; } private static final class FormTextField extends TextField implements FormControl { private FormTextField() { setStyle("-fx-display-caret: false"); } @Override public Control asControl() { return this; } @Override public void setState(int state) { setDisabled(! hasState(state, RenderTheme.ENABLED)); setEditable(hasState(state, RenderTheme.READ_ONLY)); setFocused(hasState(state, RenderTheme.FOCUSED)); setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled()); } @Override public WidgetType getType() { return WidgetType.TEXTFIELD; }; } private static final class FormCheckBox extends CheckBox implements FormControl { @Override public Control asControl() { return this; } @Override public void setState(int state) { setDisabled(! hasState(state, RenderTheme.ENABLED)); setFocused(hasState(state, RenderTheme.FOCUSED)); setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled()); setSelected(hasState(state, RenderTheme.CHECKED)); } @Override public WidgetType getType() { return WidgetType.CHECKBOX; }; } private static final class FormRadioButton extends RadioButton implements FormControl { @Override public Control asControl() { return this; } @Override public void setState(int state) { setDisabled(! hasState(state, RenderTheme.ENABLED)); setFocused(hasState(state, RenderTheme.FOCUSED)); setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled()); setSelected(hasState(state, RenderTheme.CHECKED)); } @Override public WidgetType getType() { return WidgetType.RADIOBUTTON; }; } private static final class FormSlider extends Slider implements FormControl { @Override public Control asControl() { return this; } @Override public void setState(int state) { setDisabled(! hasState(state, RenderTheme.ENABLED)); setFocused(hasState(state, RenderTheme.FOCUSED)); setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled()); } @Override public WidgetType getType() { return WidgetType.SLIDER; }; } private static final class FormProgressBar extends ProgressBar implements FormControl { private final WidgetType type; private FormProgressBar(WidgetType type) { this.type = type; } @Override public Control asControl() { return this; } @Override public void setState(int state) { setDisabled(! hasState(state, RenderTheme.ENABLED)); setFocused(hasState(state, RenderTheme.FOCUSED)); setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled()); } @Override public WidgetType getType() { return type; }; } private static final class FormMenuList extends ChoiceBox implements FormControl { private FormMenuList() { // Adding a dummy item to please ChoiceBox. List l = new ArrayList(); l.add(""); setItems(FXCollections.observableList(l)); } @Override public Control asControl() { return this; } @Override public void setState(int state) { setDisabled(! hasState(state, RenderTheme.ENABLED)); setFocused(hasState(state, RenderTheme.FOCUSED)); setHover(hasState(state, RenderTheme.HOVERED) && !isDisabled()); } @Override public WidgetType getType() { return WidgetType.MENULIST; }; } private static final class FormMenuListButton extends Button implements FormControl { private static final int MAX_WIDTH = 20; private static final int MIN_WIDTH = 16; @Override public Control asControl() { return this; } @Override public void setState(int state) { setDisabled(! hasState(state, RenderTheme.ENABLED)); setHover(hasState(state, RenderTheme.HOVERED)); setPressed(hasState(state, RenderTheme.PRESSED)); if (isPressed()) arm(); else disarm(); } private FormMenuListButton() { setSkin(new Skin()); setFocusTraversable(false); getStyleClass().add("form-select-button"); } /** * @param height is the height of the FormMenuList widget * @param width is passed equal to height */ @Override public void resize(double width, double height) { width = height > MAX_WIDTH ? MAX_WIDTH : height < MIN_WIDTH ? MIN_WIDTH : height; super.resize(width, height); // [x] is originally aligned with the right edge of // the menulist control, and here we adjust it setTranslateX(-width); } private final class Skin extends SkinBase { Skin() { super(FormMenuListButton.this); Region arrow = new Region(); arrow.getStyleClass().add("arrow"); arrow.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE); BorderPane pane = new BorderPane(); pane.setCenter(arrow); getChildren().add(pane); } } @Override public WidgetType getType() { return WidgetType.MENULISTBUTTON; }; } }