1 /*
   2  * Copyright (c) 2012, 2013, 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;
  27 
  28 import java.util.ArrayList;
  29 import java.util.Collections;
  30 import java.util.List;
  31 import javafx.beans.InvalidationListener;
  32 import javafx.collections.ListChangeListener;
  33 import javafx.collections.ObservableList;
  34 
  35 /**
  36  * A package protected util class used by TableView and TreeTableView to reduce
  37  * the level of code duplication.
  38  */
  39 class TableUtil {
  40 
  41     private TableUtil() {
  42         // no-op
  43     }
  44 
  45     static void removeTableColumnListener(List<? extends TableColumnBase> list,
  46                         final InvalidationListener columnVisibleObserver,
  47                         final InvalidationListener columnSortableObserver,
  48                         final InvalidationListener columnSortTypeObserver,
  49                         final InvalidationListener columnComparatorObserver) {
  50 
  51         if (list == null) return;
  52         for (TableColumnBase col : list) {
  53             col.visibleProperty().removeListener(columnVisibleObserver);
  54             col.sortableProperty().removeListener(columnSortableObserver);
  55             col.comparatorProperty().removeListener(columnComparatorObserver);
  56 
  57 //            col.sortTypeProperty().removeListener(columnSortTypeObserver);
  58             if (col instanceof TableColumn) {
  59                 ((TableColumn)col).sortTypeProperty().removeListener(columnSortTypeObserver);
  60             } else if (col instanceof TreeTableColumn) {
  61                 ((TreeTableColumn)col).sortTypeProperty().removeListener(columnSortTypeObserver);
  62             }
  63 
  64             removeTableColumnListener(col.getColumns(),
  65                                       columnVisibleObserver,
  66                                       columnSortableObserver,
  67                                       columnSortTypeObserver,
  68                                       columnComparatorObserver);
  69         }
  70     }
  71 
  72     static void addTableColumnListener(List<? extends TableColumnBase> list,
  73                         final InvalidationListener columnVisibleObserver,
  74                         final InvalidationListener columnSortableObserver,
  75                         final InvalidationListener columnSortTypeObserver,
  76                         final InvalidationListener columnComparatorObserver) {
  77 
  78         if (list == null) return;
  79         for (TableColumnBase col : list) {
  80             col.visibleProperty().addListener(columnVisibleObserver);
  81             col.sortableProperty().addListener(columnSortableObserver);
  82             col.comparatorProperty().addListener(columnComparatorObserver);
  83 
  84             if (col instanceof TableColumn) {
  85                 ((TableColumn)col).sortTypeProperty().addListener(columnSortTypeObserver);
  86             } else if (col instanceof TreeTableColumn) {
  87                 ((TreeTableColumn)col).sortTypeProperty().addListener(columnSortTypeObserver);
  88             }
  89 
  90             addTableColumnListener(col.getColumns(),
  91                                    columnVisibleObserver,
  92                                    columnSortableObserver,
  93                                    columnSortTypeObserver,
  94                                    columnComparatorObserver);
  95         }
  96     }
  97 
  98     static void removeColumnsListener(List<? extends TableColumnBase> list, ListChangeListener cl) {
  99         if (list == null) return;
 100 
 101         for (TableColumnBase col : list) {
 102             col.getColumns().removeListener(cl);
 103             removeColumnsListener(col.getColumns(), cl);
 104         }
 105     }
 106 
 107     static void addColumnsListener(List<? extends TableColumnBase> list, ListChangeListener cl) {
 108         if (list == null) return;
 109 
 110         for (TableColumnBase col : list) {
 111             col.getColumns().addListener(cl);
 112             addColumnsListener(col.getColumns(), cl);
 113         }
 114     }
 115 
 116     static void handleSortFailure(ObservableList<? extends TableColumnBase> sortOrder,
 117             SortEventType sortEventType, final Object... supportInfo) {
 118         // if the sort event is consumed we need to back out the previous
 119         // action so that the UI is not in an incorrect state
 120         if (sortEventType == SortEventType.COLUMN_SORT_TYPE_CHANGE) {
 121             // go back to the previous sort type
 122             final TableColumnBase changedColumn = (TableColumnBase) supportInfo[0];
 123             revertSortType(changedColumn);
 124         } else if (sortEventType == SortEventType.SORT_ORDER_CHANGE) {
 125             // Revert the sortOrder list to what it was previously
 126             ListChangeListener.Change change = (ListChangeListener.Change) supportInfo[0];
 127 
 128             final List toRemove = new ArrayList();
 129             final List toAdd = new ArrayList();
 130             while (change.next()) {
 131                 if (change.wasAdded()) {
 132                     toRemove.addAll(change.getAddedSubList());
 133                 }
 134 
 135                 if (change.wasRemoved()) {
 136                     toAdd.addAll(change.getRemoved());
 137                 }
 138             }
 139 
 140             sortOrder.removeAll(toRemove);
 141             sortOrder.addAll(toAdd);
 142         } else if (sortEventType == SortEventType.COLUMN_SORTABLE_CHANGE) {
 143             // no-op - it is ok for the sortable type to remain as-is
 144         } else if (sortEventType == SortEventType.COLUMN_COMPARATOR_CHANGE) {
 145             // no-op - it is ok for the comparator to remain as-is
 146         }
 147     }
 148 
 149     private static void revertSortType(TableColumnBase changedColumn) {
 150         if (changedColumn instanceof TableColumn) {
 151             TableColumn tableColumn = (TableColumn)changedColumn;
 152             final TableColumn.SortType sortType = tableColumn.getSortType();
 153             if (sortType == TableColumn.SortType.ASCENDING) {
 154                 tableColumn.setSortType(null);
 155             } else if (sortType == TableColumn.SortType.DESCENDING) {
 156                 tableColumn.setSortType(TableColumn.SortType.ASCENDING);
 157             } else if (sortType == null) {
 158                 tableColumn.setSortType(TableColumn.SortType.DESCENDING);
 159             }
 160         } else if (changedColumn instanceof TreeTableColumn) {
 161             TreeTableColumn tableColumn = (TreeTableColumn)changedColumn;
 162             final TreeTableColumn.SortType sortType = tableColumn.getSortType();
 163             if (sortType == TreeTableColumn.SortType.ASCENDING) {
 164                 tableColumn.setSortType(null);
 165             } else if (sortType == TreeTableColumn.SortType.DESCENDING) {
 166                 tableColumn.setSortType(TreeTableColumn.SortType.ASCENDING);
 167             } else if (sortType == null) {
 168                 tableColumn.setSortType(TreeTableColumn.SortType.DESCENDING);
 169             }
 170         }
 171     }
 172 
 173     static enum SortEventType {
 174          SORT_ORDER_CHANGE,
 175          COLUMN_SORT_TYPE_CHANGE,
 176          COLUMN_SORTABLE_CHANGE,
 177          COLUMN_COMPARATOR_CHANGE
 178      }
 179 
 180 
 181 
 182 
 183 
 184     /**
 185      * The constrained resize algorithm used by TableView and TreeTableView.
 186      * @param prop
 187      * @param isFirstRun
 188      * @param tableWidth
 189      * @param visibleLeafColumns
 190      * @return
 191      */
 192     static boolean constrainedResize(ResizeFeaturesBase prop,
 193                                      boolean isFirstRun,
 194                                      double tableWidth,
 195                                      List<? extends TableColumnBase<?,?>> visibleLeafColumns) {
 196         TableColumnBase<?,?> column = prop.getColumn();
 197         double delta = prop.getDelta();
 198 
 199         /*
 200          * There are two phases to the constrained resize policy:
 201          *   1) Ensuring internal consistency (i.e. table width == sum of all visible
 202          *      columns width). This is often called when the table is resized.
 203          *   2) Resizing the given column by __up to__ the given delta.
 204          *
 205          * It is possible that phase 1 occur and there be no need for phase 2 to
 206          * occur.
 207          */
 208 
 209         boolean isShrinking;
 210         double target;
 211         double totalLowerBound = 0;
 212         double totalUpperBound = 0;
 213 
 214         if (tableWidth == 0) return false;
 215 
 216         /*
 217          * PHASE 1: Check to ensure we have internal consistency. Based on the
 218          *          Swing JTable implementation.
 219          */
 220         // determine the width of all visible columns, and their preferred width
 221         double colWidth = 0;
 222         for (TableColumnBase<?,?> col : visibleLeafColumns) {
 223             colWidth += col.getWidth();
 224         }
 225 
 226         if (Math.abs(colWidth - tableWidth) > 1) {
 227             isShrinking = colWidth > tableWidth;
 228             target = tableWidth;
 229 
 230             if (isFirstRun) {
 231                 // if we are here we have an inconsistency - these two values should be
 232                 // equal when this resizing policy is being used.
 233                 for (TableColumnBase<?,?> col : visibleLeafColumns) {
 234                     totalLowerBound += col.getMinWidth();
 235                     totalUpperBound += col.getMaxWidth();
 236                 }
 237 
 238                 // We run into trouble if the numbers are set to infinity later on
 239                 totalUpperBound = totalUpperBound == Double.POSITIVE_INFINITY ?
 240                     Double.MAX_VALUE :
 241                     (totalUpperBound == Double.NEGATIVE_INFINITY ? Double.MIN_VALUE : totalUpperBound);
 242 
 243                 for (TableColumnBase col : visibleLeafColumns) {
 244                     double lowerBound = col.getMinWidth();
 245                     double upperBound = col.getMaxWidth();
 246 
 247                     // Check for zero. This happens when the distribution of the delta
 248                     // finishes early due to a series of "fixed" entries at the end.
 249                     // In this case, lowerBound == upperBound, for all subsequent terms.
 250                     double newSize;
 251                     if (Math.abs(totalLowerBound - totalUpperBound) < .0000001) {
 252                         newSize = lowerBound;
 253                     } else {
 254                         double f = (target - totalLowerBound) / (totalUpperBound - totalLowerBound);
 255                         newSize = Math.round(lowerBound + f * (upperBound - lowerBound));
 256                     }
 257 
 258                     double remainder = resize(col, newSize - col.getWidth());
 259 
 260                     target -= newSize + remainder;
 261                     totalLowerBound -= lowerBound;
 262                     totalUpperBound -= upperBound;
 263                 }
 264 
 265                 isFirstRun = false;
 266             } else {
 267                 double actualDelta = tableWidth - colWidth;
 268                 List<? extends TableColumnBase<?,?>> cols = visibleLeafColumns;
 269                 resizeColumns(cols, actualDelta);
 270             }
 271         }
 272 
 273         // At this point we can be happy in the knowledge that we have internal
 274         // consistency, i.e. table width == sum of the width of all visible
 275         // leaf columns.
 276 
 277         /*
 278          * Column may be null if we just changed the resize policy, and we
 279          * just wanted to enforce internal consistency, as mentioned above.
 280          */
 281         if (column == null) {
 282             return false;
 283         }
 284 
 285         /*
 286          * PHASE 2: Handling actual column resizing (by the user). Based on my own
 287          *          implementation (based on the UX spec).
 288          */
 289 
 290         isShrinking = delta < 0;
 291 
 292         // need to find the last leaf column of the given column - it is this
 293         // column that we actually resize from. If this column is a leaf, then we
 294         // use it.
 295         TableColumnBase<?,?> leafColumn = column;
 296         while (leafColumn.getColumns().size() > 0) {
 297             leafColumn = leafColumn.getColumns().get(leafColumn.getColumns().size() - 1);
 298         }
 299 
 300         int colPos = visibleLeafColumns.indexOf(leafColumn);
 301         int endColPos = visibleLeafColumns.size() - 1;
 302 
 303         // we now can split the observableArrayList into two subobservableArrayLists, representing all
 304         // columns that should grow, and all columns that should shrink
 305         //    var growingCols = if (isShrinking)
 306         //        then table.visibleLeafColumns[colPos+1..endColPos]
 307         //        else table.visibleLeafColumns[0..colPos];
 308         //    var shrinkingCols = if (isShrinking)
 309         //        then table.visibleLeafColumns[0..colPos]
 310         //        else table.visibleLeafColumns[colPos+1..endColPos];
 311 
 312 
 313         double remainingDelta = delta;
 314         while (endColPos > colPos && remainingDelta != 0) {
 315             TableColumnBase<?,?> resizingCol = visibleLeafColumns.get(endColPos);
 316             endColPos--;
 317 
 318             // if the column width is fixed, break out and try the next column
 319             if (! resizingCol.isResizable()) continue;
 320 
 321             // for convenience we discern between the shrinking and growing columns
 322             TableColumnBase<?,?> shrinkingCol = isShrinking ? leafColumn : resizingCol;
 323             TableColumnBase<?,?> growingCol = !isShrinking ? leafColumn : resizingCol;
 324 
 325             //        (shrinkingCol.width == shrinkingCol.minWidth) or (growingCol.width == growingCol.maxWidth)
 326 
 327             if (growingCol.getWidth() > growingCol.getPrefWidth()) {
 328                 // growingCol is willing to be generous in this case - it goes
 329                 // off to find a potentially better candidate to grow
 330                 List<? extends TableColumnBase> seq = visibleLeafColumns.subList(colPos + 1, endColPos + 1);
 331                 for (int i = seq.size() - 1; i >= 0; i--) {
 332                     TableColumnBase<?,?> c = seq.get(i);
 333                     if (c.getWidth() < c.getPrefWidth()) {
 334                         growingCol = c;
 335                         break;
 336                     }
 337                 }
 338             }
 339             //
 340             //        if (shrinkingCol.width < shrinkingCol.prefWidth) {
 341             //            for (c in reverse table.visibleLeafColumns[colPos+1..endColPos]) {
 342             //                if (c.width > c.prefWidth) {
 343             //                    shrinkingCol = c;
 344             //                    break;
 345             //                }
 346             //            }
 347             //        }
 348 
 349 
 350 
 351             double sdiff = Math.min(Math.abs(remainingDelta), shrinkingCol.getWidth() - shrinkingCol.getMinWidth());
 352 
 353 //                System.out.println("\tshrinking " + shrinkingCol.getText() + " and growing " + growingCol.getText());
 354 //                System.out.println("\t\tMath.min(Math.abs("+remainingDelta+"), "+shrinkingCol.getWidth()+" - "+shrinkingCol.getMinWidth()+") = " + sdiff);
 355 
 356             double delta1 = resize(shrinkingCol, -sdiff);
 357             double delta2 = resize(growingCol, sdiff);
 358             remainingDelta += isShrinking ? sdiff : -sdiff;
 359         }
 360         return remainingDelta == 0;
 361     }
 362 
 363     // function used to actually perform the resizing of the given column,
 364     // whilst ensuring it stays within the min and max bounds set on the column.
 365     // Returns the remaining delta if it could not all be applied.
 366     static double resize(TableColumnBase column, double delta) {
 367         if (delta == 0) return 0.0F;
 368         if (! column.isResizable()) return delta;
 369 
 370         final boolean isShrinking = delta < 0;
 371         final List<TableColumnBase<?,?>> resizingChildren = getResizableChildren(column, isShrinking);
 372 
 373         if (resizingChildren.size() > 0) {
 374             return resizeColumns(resizingChildren, delta);
 375         } else {
 376             double newWidth = column.getWidth() + delta;
 377 
 378             if (newWidth > column.getMaxWidth()) {
 379                 column.impl_setWidth(column.getMaxWidth());
 380                 return newWidth - column.getMaxWidth();
 381             } else if (newWidth < column.getMinWidth()) {
 382                 column.impl_setWidth(column.getMinWidth());
 383                 return newWidth - column.getMinWidth();
 384             } else {
 385                 column.impl_setWidth(newWidth);
 386                 return 0.0F;
 387             }
 388         }
 389     }
 390 
 391     // Returns all children columns of the given column that are able to be
 392     // resized. This is based on whether they are visible, resizable, and have
 393     // not space before they hit the min / max values.
 394     private static List<TableColumnBase<?,?>> getResizableChildren(TableColumnBase<?,?> column, boolean isShrinking) {
 395         if (column == null || column.getColumns().isEmpty()) {
 396             return Collections.emptyList();
 397         }
 398 
 399         List<TableColumnBase<?,?>> tablecolumns = new ArrayList<TableColumnBase<?,?>>();
 400         for (TableColumnBase c : column.getColumns()) {
 401             if (! c.isVisible()) continue;
 402             if (! c.isResizable()) continue;
 403 
 404             if (isShrinking && c.getWidth() > c.getMinWidth()) {
 405                 tablecolumns.add(c);
 406             } else if (!isShrinking && c.getWidth() < c.getMaxWidth()) {
 407                 tablecolumns.add(c);
 408             }
 409         }
 410         return tablecolumns;
 411     }
 412 
 413     private static double resizeColumns(List<? extends TableColumnBase<?,?>> columns, double delta) {
 414         // distribute space between all visible children who can be resized.
 415         // To do this we need to work out if we're shrinking or growing the
 416         // children, and then which children can be resized based on their
 417         // min/pref/max/fixed properties. The results of this are in the
 418         // resizingChildren observableArrayList above.
 419         final int columnCount = columns.size();
 420 
 421         // work out how much of the delta we should give to each child. It should
 422         // be an equal amount (at present), although perhaps we'll allow for
 423         // functions to calculate this at a later date.
 424         double colDelta = delta / columnCount;
 425 
 426         // we maintain a count of the amount of delta remaining to ensure that
 427         // the column resize operation accurately reflects the location of the
 428         // mouse pointer. Every time this value is not 0, the UI is a teeny bit
 429         // more inaccurate whilst the user continues to resize.
 430         double remainingDelta = delta;
 431 
 432         // We maintain a count of the current column that we're on in case we
 433         // need to redistribute the remainingDelta among remaining sibling.
 434         int col = 0;
 435 
 436         // This is a bit hacky - often times the leftOverDelta is zero, but
 437         // remainingDelta doesn't quite get down to 0. In these instances we
 438         // short-circuit and just return 0.0.
 439         boolean isClean = true;
 440         for (TableColumnBase<?,?> childCol : columns) {
 441             col++;
 442 
 443             // resize each child column
 444             double leftOverDelta = resize(childCol, colDelta);
 445 
 446             // calculate the remaining delta if the was anything left over in
 447             // the last resize operation
 448             remainingDelta = remainingDelta - colDelta + leftOverDelta;
 449 
 450             //      println("\tResized {childCol.text} with {colDelta}, but {leftOverDelta} was left over. RemainingDelta is now {remainingDelta}");
 451 
 452             if (leftOverDelta != 0) {
 453                 isClean = false;
 454                 // and recalculate the distribution of the remaining delta for
 455                 // the remaining siblings.
 456                 colDelta = remainingDelta / (columnCount - col);
 457             }
 458         }
 459 
 460         // see isClean above for why this is done
 461         return isClean ? 0.0 : remainingDelta;
 462     }
 463 
 464 }