1 /*
2 * Copyright (c) 2011, 2014, 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.scene.control.skin;
27
28 import javafx.collections.WeakListChangeListener;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.Map;
32 import java.util.WeakHashMap;
33
34 import javafx.collections.FXCollections;
35 import javafx.collections.ListChangeListener;
36 import javafx.collections.ObservableList;
37 import javafx.event.EventHandler;
38 import javafx.geometry.NodeOrientation;
39 import javafx.scene.Cursor;
40 import javafx.scene.Node;
41 import javafx.scene.control.*;
42 import javafx.scene.input.MouseEvent;
43 import javafx.scene.paint.Color;
44 import javafx.scene.shape.Rectangle;
45 import javafx.util.Callback;
46
47 /**
48 * <p>This class is used to construct the header of a TableView. We take the approach
49 * that every TableView header is nested - even if it isn't. This allows for us
50 * to use the same code for building a single row of TableColumns as we would
51 * with a heavily nested sequences of TableColumns. Because of this, the
52 * TableHeaderRow class consists of just one instance of a NestedTableColumnHeader.
53 *
54 */
55 public class NestedTableColumnHeader extends TableColumnHeader {
56
57 /***************************************************************************
58 * *
59 * Static Fields *
60 * *
61 **************************************************************************/
62
63 private static final int DRAG_RECT_WIDTH = 4;
64
65 private static final String TABLE_COLUMN_KEY = "TableColumn";
66 private static final String TABLE_COLUMN_HEADER_KEY = "TableColumnHeader";
67
68
69
70 /***************************************************************************
71 * *
72 * Private Fields *
73 * *
74 **************************************************************************/
75
76 /**
77 * Represents the actual columns directly contained in this nested column.
78 * It does NOT include ANY of the children of these columns, if any exist.
79 */
80 private ObservableList<? extends TableColumnBase> columns;
81
82 private TableColumnHeader label;
83
84 private ObservableList<TableColumnHeader> columnHeaders;
85
86 // used for column resizing
87 private double lastX = 0.0F;
88 private double dragAnchorX = 0.0;
89
90 // drag rectangle overlays
91 private Map<TableColumnBase<?,?>, Rectangle> dragRects = new WeakHashMap<>();
92
93 boolean updateColumns = true;
94
95
96
97 /***************************************************************************
98 * *
99 * Constructor *
100 * *
101 **************************************************************************/
102
103 public NestedTableColumnHeader(final TableViewSkinBase skin, final TableColumnBase tc) {
104 super(skin, tc);
105
106 getStyleClass().setAll("nested-column-header");
107 setFocusTraversable(false);
108
109 // init UI
110 label = new TableColumnHeader(skin, getTableColumn());
111 label.setTableHeaderRow(getTableHeaderRow());
112 label.setParentHeader(getParentHeader());
113 label.setNestedColumnHeader(this);
114
115 if (getTableColumn() != null) {
116 changeListenerHandler.registerChangeListener(getTableColumn().textProperty(), "TABLE_COLUMN_TEXT");
117 }
118
119 changeListenerHandler.registerChangeListener(skin.columnResizePolicyProperty(), "TABLE_VIEW_COLUMN_RESIZE_POLICY");
120 }
121
122
123
124 /***************************************************************************
125 * *
126 * Listeners *
127 * *
128 **************************************************************************/
129
130 private final ListChangeListener<TableColumnBase> columnsListener = c -> {
131 setHeadersNeedUpdate();
132 };
133
134 private final WeakListChangeListener weakColumnsListener =
135 new WeakListChangeListener(columnsListener);
136
137 private static final EventHandler<MouseEvent> rectMousePressed = new EventHandler<MouseEvent>() {
138 @Override public void handle(MouseEvent me) {
139 Rectangle rect = (Rectangle) me.getSource();
140 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
141 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
142
143 if (! header.isColumnResizingEnabled()) return;
144
145 if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
146 // the user wants to resize the column such that its
147 // width is equal to the widest element in the column
148 header.getTableViewSkin().resizeColumnToFitContent(column, -1);
149 } else {
150 // rather than refer to the rect variable, we just grab
151 // it from the source to prevent a small memory leak.
152 Rectangle innerRect = (Rectangle) me.getSource();
153 double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
154 header.dragAnchorX = me.getSceneX();
155 header.columnResizingStarted(startX);
156 }
157 me.consume();
158 }
159 };
160
161 private static final EventHandler<MouseEvent> rectMouseDragged = new EventHandler<MouseEvent>() {
162 @Override public void handle(MouseEvent me) {
163 Rectangle rect = (Rectangle) me.getSource();
164 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
165 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
166
167 if (! header.isColumnResizingEnabled()) return;
168
169 header.columnResizing(column, me);
170 me.consume();
171 }
172 };
173
174 private static final EventHandler<MouseEvent> rectMouseReleased = new EventHandler<MouseEvent>() {
175 @Override public void handle(MouseEvent me) {
176 Rectangle rect = (Rectangle) me.getSource();
177 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
178 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
179
180 if (! header.isColumnResizingEnabled()) return;
181
182 header.columnResizingComplete(column, me);
183 me.consume();
184 }
185 };
186
187 private static final EventHandler<MouseEvent> rectCursorChangeListener = new EventHandler<MouseEvent>() {
188 @Override public void handle(MouseEvent me) {
189 Rectangle rect = (Rectangle) me.getSource();
190 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
191 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
192
193 if (header.getCursor() == null) { // If there's a cursor for the whole header, don't override it
194 rect.setCursor(header.isColumnResizingEnabled() && rect.isHover() &&
195 column.isResizable() ? Cursor.H_RESIZE : null);
196 }
197 }
198 };
199
200
201
202 /***************************************************************************
203 * *
204 * Public Methods *
205 * *
206 **************************************************************************/
207
208 @Override protected void handlePropertyChanged(String p) {
209 super.handlePropertyChanged(p);
210
211 if ("TABLE_VIEW_COLUMN_RESIZE_POLICY".equals(p)) {
212 updateContent();
213 } else if ("TABLE_COLUMN_TEXT".equals(p)) {
214 label.setVisible(getTableColumn().getText() != null && ! getTableColumn().getText().isEmpty());
215 }
216 }
217
218 @Override public void setTableHeaderRow(TableHeaderRow header) {
219 super.setTableHeaderRow(header);
220
221 label.setTableHeaderRow(header);
222
223 // tell all children columns what TableHeader they belong to
224 for (TableColumnHeader c : getColumnHeaders()) {
225 c.setTableHeaderRow(header);
226 }
227 }
228
229 @Override public void setParentHeader(NestedTableColumnHeader parentHeader) {
230 super.setParentHeader(parentHeader);
231 label.setParentHeader(parentHeader);
232 }
233
234 ObservableList<? extends TableColumnBase> getColumns() {
235 return columns;
236 }
237
238 void setColumns(ObservableList<? extends TableColumnBase> newColumns) {
239 if (this.columns != null) {
240 this.columns.removeListener(weakColumnsListener);
241 }
242
243 this.columns = newColumns;
244
245 if (this.columns != null) {
246 this.columns.addListener(weakColumnsListener);
247 }
248 }
249
260 // iterate through all columns, unless we've got no child columns
261 // any longer, in which case we should switch to a TableColumnHeader
262 // instead
263 if (getColumns().isEmpty()) {
264 // iterate through all current headers, telling them to clean up
265 for (int i = 0; i < getColumnHeaders().size(); i++) {
266 TableColumnHeader header = getColumnHeaders().get(i);
267 header.dispose();
268 }
269
270 // switch out to be a TableColumn instead, if we have a parent header
271 NestedTableColumnHeader parentHeader = getParentHeader();
272 if (parentHeader != null) {
273 List<TableColumnHeader> parentColumnHeaders = parentHeader.getColumnHeaders();
274 int index = parentColumnHeaders.indexOf(this);
275 if (index >= 0 && index < parentColumnHeaders.size()) {
276 parentColumnHeaders.set(index, createColumnHeader(getTableColumn()));
277 }
278 } else {
279 // otherwise just remove all the columns
280 getColumnHeaders().clear();
281 }
282 } else {
283 List<TableColumnHeader> oldHeaders = new ArrayList<>(getColumnHeaders());
284 List<TableColumnHeader> newHeaders = new ArrayList<>();
285
286 for (int i = 0; i < getColumns().size(); i++) {
287 TableColumnBase<?,?> column = getColumns().get(i);
288 if (column == null || ! column.isVisible()) continue;
289
290 // check if the header already exists and reuse it
291 boolean found = false;
292 for (int j = 0; j < oldHeaders.size(); j++) {
293 TableColumnHeader oldColumn = oldHeaders.get(j);
294 if (column == oldColumn.getTableColumn()) {
295 newHeaders.add(oldColumn);
296 found = true;
297 break;
298 }
299 }
300
301 // otherwise create a new table column header
302 if (!found) {
303 newHeaders.add(createColumnHeader(column));
304 }
305 }
306
307 getColumnHeaders().setAll(newHeaders);
308
309 // dispose all old headers
310 oldHeaders.removeAll(newHeaders);
311 for (int i = 0; i < oldHeaders.size(); i++) {
312 oldHeaders.get(i).dispose();
313 }
314 }
315
316 // update the content
317 updateContent();
318
319 // RT-33596: Do CSS now, as we are in the middle of layout pass and the headers are new Nodes w/o CSS done
320 for (TableColumnHeader header : getColumnHeaders()) {
321 header.applyCss();
322 }
323 }
324
325 @Override void dispose() {
326 super.dispose();
327
328 if (label != null) {
329 label.dispose();
330 }
331
332 if (getColumns() != null) {
333 getColumns().removeListener(weakColumnsListener);
334 }
335
336 for (int i = 0; i < getColumnHeaders().size(); i++) {
337 TableColumnHeader header = getColumnHeaders().get(i);
338 header.dispose();
339 }
340
341 for (Rectangle rect : dragRects.values()) {
342 if (rect != null) {
343 rect.visibleProperty().unbind();
344 }
345 }
346 dragRects.clear();
347 getChildren().clear();
348
349 changeListenerHandler.dispose();
350 }
351
352 public ObservableList<TableColumnHeader> getColumnHeaders() {
353 if (columnHeaders == null) columnHeaders = FXCollections.<TableColumnHeader>observableArrayList();
354 return columnHeaders;
355 }
356
357 @Override protected void layoutChildren() {
358 double w = getWidth() - snappedLeftInset() - snappedRightInset();
359 double h = getHeight() - snappedTopInset() - snappedBottomInset();
360
361 int labelHeight = (int) label.prefHeight(-1);
362
363 if (label.isVisible()) {
364 // label gets to span whole width and sits at top
365 label.resize(w, labelHeight);
366 label.relocate(snappedLeftInset(), snappedTopInset());
367 }
368
369 // children columns need to share the total available width
370 double x = snappedLeftInset();
371 int pos = 0;
372 for (int i = 0, max = getColumnHeaders().size(); i < max; i++) {
373 TableColumnHeader n = getColumnHeaders().get(i);
374 if (! n.isVisible()) continue;
375
376 double prefWidth = snapSize(n.prefWidth(-1));
377 // double prefHeight = n.prefHeight(-1);
378
379 // position the column header in the default location...
380 n.resize(prefWidth, snapSize(h - labelHeight));
381 n.relocate(x, labelHeight + snappedTopInset());
382
383 // // ...but, if there are no children of this column, we should ensure
384 // // that it is resized vertically such that it goes to the very
385 // // bottom of the table header row.
386 // if (getTableHeaderRow() != null && n.getCol().getColumns().isEmpty()) {
387 // Bounds bounds = getTableHeaderRow().sceneToLocal(n.localToScene(n.getBoundsInLocal()));
388 // prefHeight = getTableHeaderRow().getHeight() - bounds.getMinY();
389 // n.resize(prefWidth, prefHeight);
390 // }
391
392 // shuffle along the x-axis appropriately
393 x += prefWidth;
394
395 // position drag overlay to intercept column resize requests
396 Rectangle dragRect = dragRects.get(n.getTableColumn());
397 if (dragRect != null) {
398 dragRect.setHeight(n.getDragRectHeight());
399 dragRect.relocate(x - DRAG_RECT_WIDTH / 2, snappedTopInset() + labelHeight);
400 }
401 }
402 }
403
404 @Override
405 double getDragRectHeight() {
406 return label.prefHeight(-1);
407 }
408
409 // sum up all children columns
410 @Override protected double computePrefWidth(double height) {
411 checkState();
412
413 double width = 0.0F;
414
415 if (getColumns() != null) {
416 for (TableColumnHeader c : getColumnHeaders()) {
417 if (c.isVisible()) {
418 width += snapSize(c.computePrefWidth(height));
419 }
420 }
421 }
422
423 return width;
424 }
425
426 @Override protected double computePrefHeight(double width) {
427 checkState();
428
429 double height = 0.0F;
430
431 if (getColumnHeaders() != null) {
432 for (TableColumnHeader n : getColumnHeaders()) {
433 height = Math.max(height, n.prefHeight(-1));
434 }
435 }
436
437 return height + label.prefHeight(-1) + snappedTopInset() + snappedBottomInset();
438 }
439
440 // protected to allow subclasses to customise the column header types
441 protected TableColumnHeader createTableColumnHeader(TableColumnBase col) {
442 return col.getColumns().isEmpty() ?
443 new TableColumnHeader(getTableViewSkin(), col) :
444 new NestedTableColumnHeader(getTableViewSkin(), col);
445 }
446
447 // allowing subclasses to force an update on the headers
448 protected void setHeadersNeedUpdate() {
449 updateColumns = true;
450
451 // go through children columns - they should update too
452 for (int i = 0; i < getColumnHeaders().size(); i++) {
453 TableColumnHeader header = getColumnHeaders().get(i);
454 if (header instanceof NestedTableColumnHeader) {
455 ((NestedTableColumnHeader)header).setHeadersNeedUpdate();
456 }
457 }
458 requestLayout();
459 }
460
461
462
463 /***************************************************************************
464 * *
465 * Private Implementation *
466 * *
467 **************************************************************************/
468
469 private void updateContent() {
470 // create a temporary list so we only do addAll into the main content
471 // observableArrayList once.
472 final List<Node> content = new ArrayList<Node>();
473
474 // the label is the region that sits above the children columns
475 content.add(label);
476
477 // all children columns
478 content.addAll(getColumnHeaders());
479
480 // Small transparent overlays that sit at the start and end of each
481 // column to intercept user drag gestures to enable column resizing.
482 if (isColumnResizingEnabled()) {
483 rebuildDragRects();
484 content.addAll(dragRects.values());
485 }
486
487 getChildren().setAll(content);
488 }
489
490 private void rebuildDragRects() {
491 if (! isColumnResizingEnabled()) return;
492
493 getChildren().removeAll(dragRects.values());
494
495 for (Rectangle rect : dragRects.values()) {
496 rect.visibleProperty().unbind();
497 }
498 dragRects.clear();
499
500 List<? extends TableColumnBase> columns = getColumns();
501
502 if (columns == null) {
503 return;
504 }
505
506 final TableViewSkinBase<?,?,?,?,?,?> skin = getTableViewSkin();
507 Callback<ResizeFeaturesBase, Boolean> columnResizePolicy = skin.columnResizePolicyProperty().get();
508 boolean isConstrainedResize =
509 skin instanceof TableViewSkin ? TableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) :
510 skin instanceof TreeTableViewSkin ? TreeTableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) :
511 false;
512
513 // RT-32547 - don't show resize cursor when in constrained resize mode
514 // and there is only one column
515 if (isConstrainedResize && skin.getVisibleLeafColumns().size() == 1) {
516 return;
517 }
518
519 for (int col = 0; col < columns.size(); col++) {
520 if (isConstrainedResize && col == getColumns().size() - 1) {
521 break;
522 }
523
524 final TableColumnBase c = columns.get(col);
525 final Rectangle rect = new Rectangle();
526 rect.getProperties().put(TABLE_COLUMN_KEY, c);
|
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 javafx.scene.control.skin;
27
28 import com.sun.javafx.collections.annotations.ReturnsUnmodifiableCollection;
29 import javafx.collections.WeakListChangeListener;
30 import java.util.ArrayList;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.WeakHashMap;
34
35 import javafx.collections.FXCollections;
36 import javafx.collections.ListChangeListener;
37 import javafx.collections.ObservableList;
38 import javafx.event.EventHandler;
39 import javafx.geometry.NodeOrientation;
40 import javafx.scene.Cursor;
41 import javafx.scene.Node;
42 import javafx.scene.control.*;
43 import javafx.scene.input.MouseEvent;
44 import javafx.scene.paint.Color;
45 import javafx.scene.shape.Rectangle;
46 import javafx.util.Callback;
47
48 /**
49 * <p>This class is used to construct the header of a TableView. We take the approach
50 * that every TableView header is nested - even if it isn't. This allows for us
51 * to use the same code for building a single row of TableColumns as we would
52 * with a heavily nested sequences of TableColumns. Because of this, the
53 * TableHeaderRow class consists of just one instance of a NestedTableColumnHeader.
54 *
55 * @since 9
56 * @see TableColumnHeader
57 * @see TableHeaderRow
58 * @see TableColumnBase
59 */
60 public class NestedTableColumnHeader extends TableColumnHeader {
61
62 /***************************************************************************
63 * *
64 * Static Fields *
65 * *
66 **************************************************************************/
67
68 private static final int DRAG_RECT_WIDTH = 4;
69
70 private static final String TABLE_COLUMN_KEY = "TableColumn";
71 private static final String TABLE_COLUMN_HEADER_KEY = "TableColumnHeader";
72
73
74
75 /***************************************************************************
76 * *
77 * Private Fields *
78 * *
79 **************************************************************************/
80
81 /**
82 * Represents the actual columns directly contained in this nested column.
83 * It does NOT include ANY of the children of these columns, if any exist.
84 */
85 private ObservableList<? extends TableColumnBase> columns;
86
87 private TableColumnHeader label;
88
89 private ObservableList<TableColumnHeader> columnHeaders;
90 private ObservableList<TableColumnHeader> unmodifiableColumnHeaders;
91
92 // used for column resizing
93 private double lastX = 0.0F;
94 private double dragAnchorX = 0.0;
95
96 // drag rectangle overlays
97 private Map<TableColumnBase<?,?>, Rectangle> dragRects = new WeakHashMap<>();
98
99 boolean updateColumns = true;
100
101
102
103 /***************************************************************************
104 * *
105 * Constructor *
106 * *
107 **************************************************************************/
108
109 /**
110 * Creates a new NestedTableColumnHeader instance to visually represent the given
111 * {@link TableColumnBase} instance.
112 *
113 * @param skin The skin used by the UI control.
114 * @param tc The table column to be visually represented by this instance.
115 */
116 public NestedTableColumnHeader(final TableViewSkinBase skin, final TableColumnBase tc) {
117 super(skin, tc);
118
119 getStyleClass().setAll("nested-column-header");
120 setFocusTraversable(false);
121
122 // init UI
123 label = new TableColumnHeader(skin, getTableColumn());
124 label.setTableHeaderRow(getTableHeaderRow());
125 label.setParentHeader(getParentHeader());
126 label.setNestedColumnHeader(this);
127
128 if (getTableColumn() != null) {
129 changeListenerHandler.registerChangeListener(getTableColumn().textProperty(), e ->
130 label.setVisible(getTableColumn().getText() != null && ! getTableColumn().getText().isEmpty()));
131 }
132
133 changeListenerHandler.registerChangeListener(skin.columnResizePolicyProperty(), e -> updateContent());
134 }
135
136
137
138 /***************************************************************************
139 * *
140 * Listeners *
141 * *
142 **************************************************************************/
143
144 private final ListChangeListener<TableColumnBase> columnsListener = c -> {
145 setHeadersNeedUpdate();
146 };
147
148 private final WeakListChangeListener weakColumnsListener =
149 new WeakListChangeListener(columnsListener);
150
151 private static final EventHandler<MouseEvent> rectMousePressed = me -> {
152 Rectangle rect = (Rectangle) me.getSource();
153 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
154 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
155
156 if (! header.isColumnResizingEnabled()) return;
157
158 if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
159 // the user wants to resize the column such that its
160 // width is equal to the widest element in the column
161 header.getTableViewSkin().resizeColumnToFitContent(column, -1);
162 } else {
163 // rather than refer to the rect variable, we just grab
164 // it from the source to prevent a small memory leak.
165 Rectangle innerRect = (Rectangle) me.getSource();
166 double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
167 header.dragAnchorX = me.getSceneX();
168 header.columnResizingStarted(startX);
169 }
170 me.consume();
171 };
172
173 private static final EventHandler<MouseEvent> rectMouseDragged = me -> {
174 Rectangle rect = (Rectangle) me.getSource();
175 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
176 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
177
178 if (! header.isColumnResizingEnabled()) return;
179
180 header.columnResizing(column, me);
181 me.consume();
182 };
183
184 private static final EventHandler<MouseEvent> rectMouseReleased = me -> {
185 Rectangle rect = (Rectangle) me.getSource();
186 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
187 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
188
189 if (! header.isColumnResizingEnabled()) return;
190
191 header.columnResizingComplete(column, me);
192 me.consume();
193 };
194
195 private static final EventHandler<MouseEvent> rectCursorChangeListener = me -> {
196 Rectangle rect = (Rectangle) me.getSource();
197 TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
198 NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
199
200 if (header.getCursor() == null) { // If there's a cursor for the whole header, don't override it
201 rect.setCursor(header.isColumnResizingEnabled() && rect.isHover() &&
202 column.isResizable() ? Cursor.H_RESIZE : null);
203 }
204 };
205
206
207
208 /***************************************************************************
209 * *
210 * Public Methods *
211 * *
212 **************************************************************************/
213
214 /** {@inheritDoc} */
215 @Override void dispose() {
216 super.dispose();
217
218 if (label != null) {
219 label.dispose();
220 }
221
222 if (getColumns() != null) {
223 getColumns().removeListener(weakColumnsListener);
224 }
225
226 for (int i = 0; i < getColumnHeaders().size(); i++) {
227 TableColumnHeader header = getColumnHeaders().get(i);
228 header.dispose();
229 }
230
231 for (Rectangle rect : dragRects.values()) {
232 if (rect != null) {
233 rect.visibleProperty().unbind();
234 }
235 }
236 dragRects.clear();
237 getChildren().clear();
238
239 changeListenerHandler.dispose();
240 }
241
242 /**
243 * Returns an unmodifiable list of the {@link TableColumnHeader} instances
244 * that are children of this NestedTableColumnHeader.
245 */
246 @ReturnsUnmodifiableCollection
247 public final ObservableList<TableColumnHeader> getColumnHeaders() {
248 if (columnHeaders == null) {
249 columnHeaders = FXCollections.<TableColumnHeader>observableArrayList();
250 unmodifiableColumnHeaders = FXCollections.unmodifiableObservableList(columnHeaders);
251 }
252 return unmodifiableColumnHeaders;
253 }
254
255 /** {@inheritDoc} */
256 @Override protected void layoutChildren() {
257 double w = getWidth() - snappedLeftInset() - snappedRightInset();
258 double h = getHeight() - snappedTopInset() - snappedBottomInset();
259
260 int labelHeight = (int) label.prefHeight(-1);
261
262 if (label.isVisible()) {
263 // label gets to span whole width and sits at top
264 label.resize(w, labelHeight);
265 label.relocate(snappedLeftInset(), snappedTopInset());
266 }
267
268 // children columns need to share the total available width
269 double x = snappedLeftInset();
270 int pos = 0;
271 for (int i = 0, max = getColumnHeaders().size(); i < max; i++) {
272 TableColumnHeader n = getColumnHeaders().get(i);
273 if (! n.isVisible()) continue;
274
275 double prefWidth = snapSize(n.prefWidth(-1));
276 // double prefHeight = n.prefHeight(-1);
277
278 // position the column header in the default location...
279 n.resize(prefWidth, snapSize(h - labelHeight));
280 n.relocate(x, labelHeight + snappedTopInset());
281
282 // // ...but, if there are no children of this column, we should ensure
283 // // that it is resized vertically such that it goes to the very
284 // // bottom of the table header row.
285 // if (getTableHeaderRow() != null && n.getCol().getColumns().isEmpty()) {
286 // Bounds bounds = getTableHeaderRow().sceneToLocal(n.localToScene(n.getBoundsInLocal()));
287 // prefHeight = getTableHeaderRow().getHeight() - bounds.getMinY();
288 // n.resize(prefWidth, prefHeight);
289 // }
290
291 // shuffle along the x-axis appropriately
292 x += prefWidth;
293
294 // position drag overlay to intercept column resize requests
295 Rectangle dragRect = dragRects.get(n.getTableColumn());
296 if (dragRect != null) {
297 dragRect.setHeight(n.getDragRectHeight());
298 dragRect.relocate(x - DRAG_RECT_WIDTH / 2, snappedTopInset() + labelHeight);
299 }
300 }
301 }
302
303 // sum up all children columns
304 /** {@inheritDoc} */
305 @Override protected double computePrefWidth(double height) {
306 checkState();
307
308 double width = 0.0F;
309
310 if (getColumns() != null) {
311 for (TableColumnHeader c : getColumnHeaders()) {
312 if (c.isVisible()) {
313 width += snapSize(c.computePrefWidth(height));
314 }
315 }
316 }
317
318 return width;
319 }
320
321 /** {@inheritDoc} */
322 @Override protected double computePrefHeight(double width) {
323 checkState();
324
325 double height = 0.0F;
326
327 if (getColumnHeaders() != null) {
328 for (TableColumnHeader n : getColumnHeaders()) {
329 height = Math.max(height, n.prefHeight(-1));
330 }
331 }
332
333 return height + label.prefHeight(-1) + snappedTopInset() + snappedBottomInset();
334 }
335
336
337
338 /***************************************************************************
339 * *
340 * Private Implementation *
341 * *
342 **************************************************************************/
343
344 @Override void setTableHeaderRow(TableHeaderRow header) {
345 super.setTableHeaderRow(header);
346
347 label.setTableHeaderRow(header);
348
349 // tell all children columns what TableHeader they belong to
350 for (TableColumnHeader c : getColumnHeaders()) {
351 c.setTableHeaderRow(header);
352 }
353 }
354
355 @Override void setParentHeader(NestedTableColumnHeader parentHeader) {
356 super.setParentHeader(parentHeader);
357 label.setParentHeader(parentHeader);
358 }
359
360 ObservableList<? extends TableColumnBase> getColumns() {
361 return columns;
362 }
363
364 void setColumns(ObservableList<? extends TableColumnBase> newColumns) {
365 if (this.columns != null) {
366 this.columns.removeListener(weakColumnsListener);
367 }
368
369 this.columns = newColumns;
370
371 if (this.columns != null) {
372 this.columns.addListener(weakColumnsListener);
373 }
374 }
375
386 // iterate through all columns, unless we've got no child columns
387 // any longer, in which case we should switch to a TableColumnHeader
388 // instead
389 if (getColumns().isEmpty()) {
390 // iterate through all current headers, telling them to clean up
391 for (int i = 0; i < getColumnHeaders().size(); i++) {
392 TableColumnHeader header = getColumnHeaders().get(i);
393 header.dispose();
394 }
395
396 // switch out to be a TableColumn instead, if we have a parent header
397 NestedTableColumnHeader parentHeader = getParentHeader();
398 if (parentHeader != null) {
399 List<TableColumnHeader> parentColumnHeaders = parentHeader.getColumnHeaders();
400 int index = parentColumnHeaders.indexOf(this);
401 if (index >= 0 && index < parentColumnHeaders.size()) {
402 parentColumnHeaders.set(index, createColumnHeader(getTableColumn()));
403 }
404 } else {
405 // otherwise just remove all the columns
406 columnHeaders.clear();
407 }
408 } else {
409 List<TableColumnHeader> oldHeaders = new ArrayList<>(getColumnHeaders());
410 List<TableColumnHeader> newHeaders = new ArrayList<>();
411
412 for (int i = 0; i < getColumns().size(); i++) {
413 TableColumnBase<?,?> column = getColumns().get(i);
414 if (column == null || ! column.isVisible()) continue;
415
416 // check if the header already exists and reuse it
417 boolean found = false;
418 for (int j = 0; j < oldHeaders.size(); j++) {
419 TableColumnHeader oldColumn = oldHeaders.get(j);
420 if (column == oldColumn.getTableColumn()) {
421 newHeaders.add(oldColumn);
422 found = true;
423 break;
424 }
425 }
426
427 // otherwise create a new table column header
428 if (!found) {
429 newHeaders.add(createColumnHeader(column));
430 }
431 }
432
433 columnHeaders.setAll(newHeaders);
434
435 // dispose all old headers
436 oldHeaders.removeAll(newHeaders);
437 for (int i = 0; i < oldHeaders.size(); i++) {
438 oldHeaders.get(i).dispose();
439 }
440 }
441
442 // update the content
443 updateContent();
444
445 // RT-33596: Do CSS now, as we are in the middle of layout pass and the headers are new Nodes w/o CSS done
446 for (TableColumnHeader header : getColumnHeaders()) {
447 header.applyCss();
448 }
449 }
450
451 /** {@inheritDoc} */
452 @Override double getDragRectHeight() {
453 return label.prefHeight(-1);
454 }
455
456 TableColumnHeader createTableColumnHeader(TableColumnBase col) {
457 return col.getColumns().isEmpty() ?
458 new TableColumnHeader(getTableViewSkin(), col) :
459 new NestedTableColumnHeader(getTableViewSkin(), col);
460 }
461
462 void setHeadersNeedUpdate() {
463 updateColumns = true;
464
465 // go through children columns - they should update too
466 for (int i = 0; i < getColumnHeaders().size(); i++) {
467 TableColumnHeader header = getColumnHeaders().get(i);
468 if (header instanceof NestedTableColumnHeader) {
469 ((NestedTableColumnHeader)header).setHeadersNeedUpdate();
470 }
471 }
472 requestLayout();
473 }
474
475 private void updateContent() {
476 // create a temporary list so we only do addAll into the main content
477 // observableArrayList once.
478 final List<Node> content = new ArrayList<Node>();
479
480 // the label is the region that sits above the children columns
481 content.add(label);
482
483 // all children columns
484 content.addAll(getColumnHeaders());
485
486 // Small transparent overlays that sit at the start and end of each
487 // column to intercept user drag gestures to enable column resizing.
488 if (isColumnResizingEnabled()) {
489 rebuildDragRects();
490 content.addAll(dragRects.values());
491 }
492
493 getChildren().setAll(content);
494 }
495
496 private void rebuildDragRects() {
497 if (! isColumnResizingEnabled()) return;
498
499 getChildren().removeAll(dragRects.values());
500
501 for (Rectangle rect : dragRects.values()) {
502 rect.visibleProperty().unbind();
503 }
504 dragRects.clear();
505
506 List<? extends TableColumnBase> columns = getColumns();
507
508 if (columns == null) {
509 return;
510 }
511
512 final TableViewSkinBase<?,?,?,?,?> skin = getTableViewSkin();
513 Callback<ResizeFeaturesBase, Boolean> columnResizePolicy = skin.columnResizePolicyProperty().get();
514 boolean isConstrainedResize =
515 skin instanceof TableViewSkin ? TableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) :
516 skin instanceof TreeTableViewSkin ? TreeTableView.CONSTRAINED_RESIZE_POLICY.equals(columnResizePolicy) :
517 false;
518
519 // RT-32547 - don't show resize cursor when in constrained resize mode
520 // and there is only one column
521 if (isConstrainedResize && skin.getVisibleLeafColumns().size() == 1) {
522 return;
523 }
524
525 for (int col = 0; col < columns.size(); col++) {
526 if (isConstrainedResize && col == getColumns().size() - 1) {
527 break;
528 }
529
530 final TableColumnBase c = columns.get(col);
531 final Rectangle rect = new Rectangle();
532 rect.getProperties().put(TABLE_COLUMN_KEY, c);
|