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 }