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 }