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 }