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