1 /*
   2  * Copyright (c) 2010, 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 javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior;
  29 import com.sun.javafx.scene.control.behavior.ComboBoxListViewBehavior;
  30 
  31 import java.util.List;
  32 
  33 import javafx.beans.InvalidationListener;
  34 import javafx.beans.WeakInvalidationListener;
  35 import javafx.beans.property.BooleanProperty;
  36 import javafx.beans.property.SimpleBooleanProperty;
  37 import javafx.collections.FXCollections;
  38 import javafx.collections.ListChangeListener;
  39 import javafx.collections.ObservableList;
  40 import javafx.collections.WeakListChangeListener;
  41 import javafx.css.PseudoClass;
  42 import javafx.event.ActionEvent;
  43 import javafx.event.EventTarget;
  44 import javafx.scene.AccessibleAttribute;
  45 import javafx.scene.AccessibleRole;
  46 import javafx.scene.Node;
  47 import javafx.scene.Parent;
  48 import javafx.scene.control.ComboBox;
  49 import javafx.scene.control.Control;
  50 import javafx.scene.control.ListCell;
  51 import javafx.scene.control.ListView;
  52 import javafx.scene.control.SelectionMode;
  53 import javafx.scene.control.SelectionModel;
  54 import javafx.scene.control.TextField;
  55 import javafx.scene.input.*;
  56 import javafx.util.Callback;
  57 import javafx.util.StringConverter;
  58 
  59 /**
  60  * Default skin implementation for the {@link ComboBox} control.
  61  *
  62  * @see ComboBox
  63  * @since 9
  64  */
  65 public class ComboBoxListViewSkin<T> extends ComboBoxPopupControl<T> {
  66 
  67     /***************************************************************************
  68      *                                                                         *
  69      * Static fields                                                           *
  70      *                                                                         *
  71      **************************************************************************/
  72 
  73     // By default we measure the width of all cells in the ListView. If this
  74     // is too burdensome, the developer may set a property in the ComboBox
  75     // properties map with this key to specify the number of rows to measure.
  76     // This may one day become a property on the ComboBox itself.
  77     private static final String COMBO_BOX_ROWS_TO_MEASURE_WIDTH_KEY = "comboBoxRowsToMeasureWidth";
  78 
  79 
  80 
  81     /***************************************************************************
  82      *                                                                         *
  83      * Private fields                                                          *
  84      *                                                                         *
  85      **************************************************************************/
  86 
  87     private final ComboBox<T> comboBox;
  88     private ObservableList<T> comboBoxItems;
  89 
  90     private ListCell<T> buttonCell;
  91     private Callback<ListView<T>, ListCell<T>> cellFactory;
  92 
  93     private final ListView<T> listView;
  94     private ObservableList<T> listViewItems;
  95 
  96     private boolean listSelectionLock = false;
  97     private boolean listViewSelectionDirty = false;
  98 
  99     private final ComboBoxListViewBehavior behavior;
 100 
 101 
 102 
 103     /***************************************************************************
 104      *                                                                         *
 105      * Listeners                                                               *
 106      *                                                                         *
 107      **************************************************************************/
 108 
 109     private boolean itemCountDirty;
 110     private final ListChangeListener<T> listViewItemsListener = new ListChangeListener<T>() {
 111         @Override public void onChanged(ListChangeListener.Change<? extends T> c) {
 112             itemCountDirty = true;
 113             getSkinnable().requestLayout();
 114         }
 115     };
 116 
 117     private final InvalidationListener itemsObserver;
 118 
 119     private final WeakListChangeListener<T> weakListViewItemsListener =
 120             new WeakListChangeListener<T>(listViewItemsListener);
 121 
 122 
 123     /***************************************************************************
 124      *                                                                         *
 125      * Constructors                                                            *
 126      *                                                                         *
 127      **************************************************************************/
 128 
 129     /**
 130      * Creates a new ComboBoxListViewSkin instance, installing the necessary child
 131      * nodes into the Control {@link Control#getChildren() children} list, as
 132      * well as the necessary input mappings for handling key, mouse, etc events.
 133      *
 134      * @param control The control that this skin should be installed onto.
 135      */
 136     public ComboBoxListViewSkin(final ComboBox<T> control) {
 137         super(control);
 138 
 139         // install default input map for the control
 140         this.behavior = new ComboBoxListViewBehavior<>(control);
 141 //        control.setInputMap(behavior.getInputMap());
 142 
 143         this.comboBox = control;
 144         updateComboBoxItems();
 145 
 146         itemsObserver = observable -> {
 147             updateComboBoxItems();
 148             updateListViewItems();
 149         };
 150         control.itemsProperty().addListener(new WeakInvalidationListener(itemsObserver));
 151 
 152         // listview for popup
 153         this.listView = createListView();
 154 
 155         // Fix for RT-21207. Additional code related to this bug is further below.
 156         this.listView.setManaged(false);
 157         getChildren().add(listView);
 158         // -- end of fix
 159 
 160         updateListViewItems();
 161         updateCellFactory();
 162 
 163         updateButtonCell();
 164 
 165         // Fix for RT-19431 (also tested via ComboBoxListViewSkinTest)
 166         updateValue();
 167 
 168         registerChangeListener(control.itemsProperty(), e -> {
 169             updateComboBoxItems();
 170             updateListViewItems();
 171         });
 172         registerChangeListener(control.promptTextProperty(), e -> updateDisplayNode());
 173         registerChangeListener(control.cellFactoryProperty(), e -> updateCellFactory());
 174         registerChangeListener(control.visibleRowCountProperty(), e -> {
 175             if (listView == null) return;
 176             listView.requestLayout();
 177         });
 178         registerChangeListener(control.converterProperty(), e -> updateListViewItems());
 179         registerChangeListener(control.buttonCellProperty(), e -> {
 180             updateButtonCell();
 181             updateDisplayArea();
 182         });
 183         registerChangeListener(control.valueProperty(), e -> {
 184             updateValue();
 185             control.fireEvent(new ActionEvent());
 186         });
 187         registerChangeListener(control.editableProperty(), e -> updateEditable());
 188 
 189         // Refer to JDK-8095306
 190         if (comboBox.isShowing()) {
 191             show();
 192         }
 193     }
 194 
 195 
 196 
 197     /***************************************************************************
 198      *                                                                         *
 199      * Properties                                                              *
 200      *                                                                         *
 201      **************************************************************************/
 202 
 203     /**
 204      * By default this skin hides the popup whenever the ListView is clicked in.
 205      * By setting hideOnClick to false, the popup will not be hidden when the
 206      * ListView is clicked in. This is beneficial in some scenarios (for example,
 207      * when the ListView cells have checkboxes).
 208      */
 209     // --- hide on click
 210     private final BooleanProperty hideOnClick = new SimpleBooleanProperty(this, "hideOnClick", true);
 211     public final BooleanProperty hideOnClickProperty() {
 212         return hideOnClick;
 213     }
 214     public final boolean isHideOnClick() {
 215         return hideOnClick.get();
 216     }
 217     public final void setHideOnClick(boolean value) {
 218         hideOnClick.set(value);
 219     }
 220 
 221 
 222 
 223     /***************************************************************************
 224      *                                                                         *
 225      * Public API                                                              *
 226      *                                                                         *
 227      **************************************************************************/
 228 
 229     /** {@inheritDoc} */
 230     @Override public void dispose() {
 231         super.dispose();
 232 
 233         if (behavior != null) {
 234             behavior.dispose();
 235         }
 236     }
 237 
 238     /** {@inheritDoc} */
 239     @Override protected TextField getEditor() {
 240         // Return null if editable is false, even if the ComboBox has an editor set.
 241         // Use getSkinnable() here because this method is called from the super
 242         // constructor before comboBox is initialized.
 243         return getSkinnable().isEditable() ? ((ComboBox)getSkinnable()).getEditor() : null;
 244     }
 245 
 246     /** {@inheritDoc} */
 247     @Override protected StringConverter<T> getConverter() {
 248         return ((ComboBox)getSkinnable()).getConverter();
 249     }
 250 
 251     /** {@inheritDoc} */
 252     @Override public Node getDisplayNode() {
 253         Node displayNode;
 254         if (comboBox.isEditable()) {
 255             displayNode = getEditableInputNode();
 256         } else {
 257             displayNode = buttonCell;
 258         }
 259 
 260         updateDisplayNode();
 261 
 262         return displayNode;
 263     }
 264 
 265     /** {@inheritDoc} */
 266     @Override public Node getPopupContent() {
 267         return listView;
 268     }
 269 
 270     /** {@inheritDoc} */
 271     @Override protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 272         reconfigurePopup();
 273         return 50;
 274     }
 275 
 276     /** {@inheritDoc} */
 277     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 278         double superPrefWidth = super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset);
 279         double listViewWidth = listView.prefWidth(height);
 280         double pw = Math.max(superPrefWidth, listViewWidth);
 281 
 282         reconfigurePopup();
 283 
 284         return pw;
 285     }
 286 
 287     /** {@inheritDoc} */
 288     @Override protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 289         reconfigurePopup();
 290         return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset);
 291     }
 292 
 293     /** {@inheritDoc} */
 294     @Override protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 295         reconfigurePopup();
 296         return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
 297     }
 298 
 299     /** {@inheritDoc} */
 300     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 301         reconfigurePopup();
 302         return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
 303     }
 304 
 305     /** {@inheritDoc} */
 306     @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 307         reconfigurePopup();
 308         return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
 309     }
 310 
 311     /** {@inheritDoc} */
 312     @Override protected void layoutChildren(final double x, final double y,
 313             final double w, final double h) {
 314         if (listViewSelectionDirty) {
 315             try {
 316                 listSelectionLock = true;
 317                 T item = comboBox.getSelectionModel().getSelectedItem();
 318                 listView.getSelectionModel().clearSelection();
 319                 listView.getSelectionModel().select(item);
 320             } finally {
 321                 listSelectionLock = false;
 322                 listViewSelectionDirty = false;
 323             }
 324         }
 325 
 326         super.layoutChildren(x, y, w, h);
 327     }
 328 
 329 
 330 
 331     /***************************************************************************
 332      *                                                                         *
 333      * Private methods                                                         *
 334      *                                                                         *
 335      **************************************************************************/
 336 
 337     /** {@inheritDoc} */
 338     @Override void updateDisplayNode() {
 339         if (getEditor() != null) {
 340             super.updateDisplayNode();
 341         } else {
 342             T value = comboBox.getValue();
 343             int index = getIndexOfComboBoxValueInItemsList();
 344             if (index > -1) {
 345                 buttonCell.setItem(null);
 346                 buttonCell.updateIndex(index);
 347             } else {
 348                 // RT-21336 Show the ComboBox value even though it doesn't
 349                 // exist in the ComboBox items list (part two of fix)
 350                 buttonCell.updateIndex(-1);
 351                 boolean empty = updateDisplayText(buttonCell, value, false);
 352 
 353                 // Note that empty boolean collected above. This is used to resolve
 354                 // RT-27834, where we were getting different styling based on whether
 355                 // the cell was updated via the updateIndex method above, or just
 356                 // by directly updating the text. We fake the pseudoclass state
 357                 // for empty, filled, and selected here.
 358                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_EMPTY,    empty);
 359                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_FILLED,   !empty);
 360                 buttonCell.pseudoClassStateChanged(PSEUDO_CLASS_SELECTED, true);
 361             }
 362         }
 363     }
 364 
 365     /** {@inheritDoc} */
 366     @Override ComboBoxBaseBehavior getBehavior() {
 367         return behavior;
 368     }
 369 
 370     private void updateComboBoxItems() {
 371         comboBoxItems = comboBox.getItems();
 372         comboBoxItems = comboBoxItems == null ? FXCollections.<T>emptyObservableList() : comboBoxItems;
 373     }
 374 
 375     private void updateListViewItems() {
 376         if (listViewItems != null) {
 377             listViewItems.removeListener(weakListViewItemsListener);
 378         }
 379 
 380         this.listViewItems = comboBoxItems;
 381         listView.setItems(listViewItems);
 382 
 383         if (listViewItems != null) {
 384             listViewItems.addListener(weakListViewItemsListener);
 385         }
 386 
 387         itemCountDirty = true;
 388         getSkinnable().requestLayout();
 389     }
 390 
 391     private void updateValue() {
 392         T newValue = comboBox.getValue();
 393 
 394         SelectionModel<T> listViewSM = listView.getSelectionModel();
 395 
 396         // RT-22386: We need to test to see if the value is in the comboBox
 397         // items list. If it isn't, then we should clear the listview
 398         // selection
 399         final int indexOfNewValue = getIndexOfComboBoxValueInItemsList();
 400 
 401         if (newValue == null && indexOfNewValue == -1) {
 402             listViewSM.clearSelection();
 403         } else {
 404             if (indexOfNewValue == -1) {
 405                 listSelectionLock = true;
 406                 listViewSM.clearSelection();
 407                 listSelectionLock = false;
 408             } else {
 409                 int index = comboBox.getSelectionModel().getSelectedIndex();
 410                 if (index >= 0 && index < comboBoxItems.size()) {
 411                     T itemsObj = comboBoxItems.get(index);
 412                     if ((itemsObj != null && itemsObj.equals(newValue)) || (itemsObj == null && newValue == null)) {
 413                         listViewSM.select(index);
 414                     } else {
 415                         listViewSM.select(newValue);
 416                     }
 417                 } else {
 418                     // just select the first instance of newValue in the list
 419                     int listViewIndex = comboBoxItems.indexOf(newValue);
 420                     if (listViewIndex == -1) {
 421                         // RT-21336 Show the ComboBox value even though it doesn't
 422                         // exist in the ComboBox items list (part one of fix)
 423                         updateDisplayNode();
 424                     } else {
 425                         listViewSM.select(listViewIndex);
 426                     }
 427                 }
 428             }
 429         }
 430     }
 431 
 432     // return a boolean to indicate that the cell is empty (and therefore not filled)
 433     private boolean updateDisplayText(ListCell<T> cell, T item, boolean empty) {
 434         if (empty) {
 435             if (cell == null) return true;
 436             cell.setGraphic(null);
 437             cell.setText(null);
 438             return true;
 439         } else if (item instanceof Node) {
 440             Node currentNode = cell.getGraphic();
 441             Node newNode = (Node) item;
 442             if (currentNode == null || ! currentNode.equals(newNode)) {
 443                 cell.setText(null);
 444                 cell.setGraphic(newNode);
 445             }
 446             return newNode == null;
 447         } else {
 448             // run item through StringConverter if it isn't null
 449             final StringConverter<T> c = comboBox.getConverter();
 450             final String promptText = comboBox.getPromptText();
 451             String s = item == null && promptText != null ? promptText :
 452                        c == null ? (item == null ? null : item.toString()) : c.toString(item);
 453             cell.setText(s);
 454             cell.setGraphic(null);
 455             return s == null || s.isEmpty();
 456         }
 457     }
 458 
 459     private int getIndexOfComboBoxValueInItemsList() {
 460         T value = comboBox.getValue();
 461         int index = comboBoxItems.indexOf(value);
 462         return index;
 463     }
 464 
 465     private void updateButtonCell() {
 466         buttonCell = comboBox.getButtonCell() != null ?
 467                 comboBox.getButtonCell() : getDefaultCellFactory().call(listView);
 468         buttonCell.setMouseTransparent(true);
 469         buttonCell.updateListView(listView);
 470 
 471         // As long as the screen-reader is concerned this node is not a list item.
 472         // This matters because the screen-reader counts the number of list item
 473         // within combo and speaks it to the user.
 474         buttonCell.setAccessibleRole(AccessibleRole.NODE);
 475     }
 476 
 477     private void updateCellFactory() {
 478         Callback<ListView<T>, ListCell<T>> cf = comboBox.getCellFactory();
 479         cellFactory = cf != null ? cf : getDefaultCellFactory();
 480         listView.setCellFactory(cellFactory);
 481     }
 482 
 483     private Callback<ListView<T>, ListCell<T>> getDefaultCellFactory() {
 484         return new Callback<ListView<T>, ListCell<T>>() {
 485             @Override public ListCell<T> call(ListView<T> listView) {
 486                 return new ListCell<T>() {
 487                     @Override public void updateItem(T item, boolean empty) {
 488                         super.updateItem(item, empty);
 489                         updateDisplayText(this, item, empty);
 490                     }
 491                 };
 492             }
 493         };
 494     }
 495 
 496     private ListView<T> createListView() {
 497         final ListView<T> _listView = new ListView<T>() {
 498 
 499             {
 500                 getProperties().put("selectFirstRowByDefault", false);
 501             }
 502 
 503             @Override protected double computeMinHeight(double width) {
 504                 return 30;
 505             }
 506 
 507             @Override protected double computePrefWidth(double height) {
 508                 double pw;
 509                 if (getSkin() instanceof ListViewSkin) {
 510                     ListViewSkin<?> skin = (ListViewSkin<?>)getSkin();
 511                     if (itemCountDirty) {
 512                         skin.updateItemCount();
 513                         itemCountDirty = false;
 514                     }
 515 
 516                     int rowsToMeasure = -1;
 517                     if (comboBox.getProperties().containsKey(COMBO_BOX_ROWS_TO_MEASURE_WIDTH_KEY)) {
 518                         rowsToMeasure = (Integer) comboBox.getProperties().get(COMBO_BOX_ROWS_TO_MEASURE_WIDTH_KEY);
 519                     }
 520 
 521                     pw = Math.max(comboBox.getWidth(), skin.getMaxCellWidth(rowsToMeasure) + 30);
 522                 } else {
 523                     pw = Math.max(100, comboBox.getWidth());
 524                 }
 525 
 526                 // need to check the ListView pref height in the case that the
 527                 // placeholder node is showing
 528                 if (getItems().isEmpty() && getPlaceholder() != null) {
 529                     pw = Math.max(super.computePrefWidth(height), pw);
 530                 }
 531 
 532                 return Math.max(50, pw);
 533             }
 534 
 535             @Override protected double computePrefHeight(double width) {
 536                 return getListViewPrefHeight();
 537             }
 538         };
 539 
 540         _listView.setId("list-view");
 541         _listView.placeholderProperty().bind(comboBox.placeholderProperty());
 542         _listView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
 543         _listView.setFocusTraversable(false);
 544 
 545         _listView.getSelectionModel().selectedIndexProperty().addListener(o -> {
 546             if (listSelectionLock) return;
 547             int index = listView.getSelectionModel().getSelectedIndex();
 548             comboBox.getSelectionModel().select(index);
 549             updateDisplayNode();
 550             comboBox.notifyAccessibleAttributeChanged(AccessibleAttribute.TEXT);
 551         });
 552 
 553         comboBox.getSelectionModel().selectedItemProperty().addListener(o -> {
 554             listViewSelectionDirty = true;
 555         });
 556 
 557         _listView.addEventFilter(MouseEvent.MOUSE_RELEASED, t -> {
 558             // RT-18672: Without checking if the user is clicking in the
 559             // scrollbar area of the ListView, the comboBox will hide. Therefore,
 560             // we add the check below to prevent this from happening.
 561             EventTarget target = t.getTarget();
 562             if (target instanceof Parent) {
 563                 List<String> s = ((Parent) target).getStyleClass();
 564                 if (s.contains("thumb")
 565                         || s.contains("track")
 566                         || s.contains("decrement-arrow")
 567                         || s.contains("increment-arrow")) {
 568                     return;
 569                 }
 570             }
 571 
 572             if (isHideOnClick()) {
 573                 comboBox.hide();
 574             }
 575         });
 576 
 577         _listView.setOnKeyPressed(t -> {
 578             // TODO move to behavior, when (or if) this class becomes a SkinBase
 579             if (t.getCode() == KeyCode.ENTER ||
 580                     t.getCode() == KeyCode.SPACE ||
 581                     t.getCode() == KeyCode.ESCAPE) {
 582                 comboBox.hide();
 583             }
 584         });
 585 
 586         return _listView;
 587     }
 588 
 589     private double getListViewPrefHeight() {
 590         double ph;
 591         if (listView.getSkin() instanceof VirtualContainerBase) {
 592             int maxRows = comboBox.getVisibleRowCount();
 593             VirtualContainerBase<?,?> skin = (VirtualContainerBase<?,?>)listView.getSkin();
 594             ph = skin.getVirtualFlowPreferredHeight(maxRows);
 595         } else {
 596             double ch = comboBoxItems.size() * 25;
 597             ph = Math.min(ch, 200);
 598         }
 599 
 600         return ph;
 601     }
 602 
 603 
 604 
 605     /**************************************************************************
 606      *
 607      * API for testing
 608      *
 609      *************************************************************************/
 610 
 611     ListView<T> getListView() {
 612         return listView;
 613     }
 614 
 615 
 616 
 617 
 618     /***************************************************************************
 619      *                                                                         *
 620      * Stylesheet Handling                                                     *
 621      *                                                                         *
 622      **************************************************************************/
 623 
 624     // These three pseudo class states are duplicated from Cell
 625     private static final PseudoClass PSEUDO_CLASS_SELECTED =
 626             PseudoClass.getPseudoClass("selected");
 627     private static final PseudoClass PSEUDO_CLASS_EMPTY =
 628             PseudoClass.getPseudoClass("empty");
 629     private static final PseudoClass PSEUDO_CLASS_FILLED =
 630             PseudoClass.getPseudoClass("filled");
 631 
 632 
 633     /** {@inheritDoc} */
 634     @Override public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
 635         switch (attribute) {
 636             case FOCUS_ITEM: {
 637                 if (comboBox.isShowing()) {
 638                     /* On Mac, for some reason, changing the selection on the list is not
 639                      * reported by VoiceOver the first time it shows.
 640                      * Note that this fix returns a child of the PopupWindow back to the main
 641                      * Stage, which doesn't seem to cause problems.
 642                      */
 643                     return listView.queryAccessibleAttribute(attribute, parameters);
 644                 }
 645                 return null;
 646             }
 647             case TEXT: {
 648                 String accText = comboBox.getAccessibleText();
 649                 if (accText != null && !accText.isEmpty()) return accText;
 650                 String title = comboBox.isEditable() ? getEditor().getText() : buttonCell.getText();
 651                 if (title == null || title.isEmpty()) {
 652                     title = comboBox.getPromptText();
 653                 }
 654                 return title;
 655             }
 656             case SELECTION_START:
 657                 return (getEditor() != null) ? getEditor().getSelection().getStart() : null;
 658             case SELECTION_END:
 659                 return (getEditor() != null) ? getEditor().getSelection().getEnd() : null;
 660             default: return super.queryAccessibleAttribute(attribute, parameters);
 661         }
 662     }
 663 }
 664