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 }