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 java.util.*;
29
30 import javafx.beans.InvalidationListener;
31 import javafx.beans.WeakInvalidationListener;
32 import javafx.beans.property.BooleanProperty;
33 import javafx.beans.property.SimpleBooleanProperty;
34 import javafx.beans.property.StringProperty;
35 import javafx.collections.ListChangeListener;
36 import javafx.collections.WeakListChangeListener;
37 import javafx.geometry.HPos;
38 import javafx.geometry.Insets;
39 import javafx.geometry.Side;
40 import javafx.geometry.VPos;
41 import javafx.scene.control.CheckMenuItem;
42 import javafx.scene.control.ContextMenu;
43 import javafx.scene.control.Control;
44 import javafx.scene.control.Label;
45 import javafx.scene.control.TableColumn;
46 import javafx.scene.control.TableColumnBase;
47 import javafx.scene.layout.Pane;
48 import javafx.scene.layout.Region;
49 import javafx.scene.layout.StackPane;
50 import javafx.scene.shape.Rectangle;
51
52 import com.sun.javafx.scene.control.skin.resources.ControlResources;
53
54 /**
55 * Region responsible for painting the entire row of column headers.
56 */
57 public class TableHeaderRow extends StackPane {
58
59 /***************************************************************************
60 * *
61 * Static Fields *
62 * *
63 **************************************************************************/
64
65 private static final String MENU_SEPARATOR =
66 ControlResources.getString("TableView.nestedColumnControlMenuSeparator");
67
68
69
70 /***************************************************************************
71 * *
72 * Private Fields *
73 * *
74 **************************************************************************/
75
76 private final VirtualFlow flow;
77
78 private final TableViewSkinBase tableSkin;
79
80 private Map<TableColumnBase, CheckMenuItem> columnMenuItems = new HashMap<TableColumnBase, CheckMenuItem>();
81
82 // Vertical line that is shown when columns are being reordered
83 // private Region columnReorderLine;
84
85 private double scrollX;
86
87 private double tableWidth;
88
89 private Rectangle clip;
90
91 private TableColumnHeader reorderingRegion;
92
93 /**
94 * This is the ghosted region representing the table column that is being
95 * dragged. It moves along the x-axis but is fixed in the y-axis.
96 */
97 private StackPane dragHeader;
98 private final Label dragHeaderLabel = new Label();
99
100 /*
101 * The header row is actually just one NestedTableColumnHeader that spans
102 * the entire width. Nested within this is the TableColumnHeader's and
103 * NestedTableColumnHeader's, as necessary. This makes it nice and clean
104 * to handle column reordering - we basically enforce the rule that column
105 * reordering only occurs within a single NestedTableColumnHeader, and only
106 * at that level.
107 */
108 private final NestedTableColumnHeader header;
109
110 private Region filler;
111
112 /**
113 * This is the region where the user can interact with to show/hide columns.
114 * It is positioned in the top-right hand corner of the TableHeaderRow, and
115 * when clicked shows a PopupMenu consisting of all leaf columns.
116 */
117 private Pane cornerRegion;
118
119 /**
120 * PopupMenu shown to users to allow for them to hide/show columns in the
121 * table.
122 */
123 private ContextMenu columnPopupMenu;
124
125 private BooleanProperty reordering = new SimpleBooleanProperty(this, "reordering", false) {
126 @Override protected void invalidated() {
127 TableColumnHeader r = getReorderingRegion();
128 if (r != null) {
129 double dragHeaderHeight = r.getNestedColumnHeader() != null ?
130 r.getNestedColumnHeader().getHeight() :
131 getReorderingRegion().getHeight();
132
133 dragHeader.resize(dragHeader.getWidth(), dragHeaderHeight);
134 dragHeader.setTranslateY(getHeight() - dragHeaderHeight);
135 }
136 dragHeader.setVisible(isReordering());
137 }
138 };
139
140
141
142 /***************************************************************************
143 * *
144 * Constructor *
145 * *
146 **************************************************************************/
147
148 public TableHeaderRow(final TableViewSkinBase skin) {
149 this.tableSkin = skin;
150 this.flow = skin.flow;
151
152 getStyleClass().setAll("column-header-background");
153
154 // clip the header so it doesn't show outside of the table bounds
155 clip = new Rectangle();
156 clip.setSmooth(false);
157 clip.heightProperty().bind(heightProperty());
158 setClip(clip);
159
160 // listen to table width to keep header in sync
161 updateTableWidth();
162 tableSkin.getSkinnable().widthProperty().addListener(weakTableWidthListener);
163 tableSkin.getSkinnable().paddingProperty().addListener(weakTablePaddingListener);
164 skin.getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
165
166 // popup menu for hiding/showing columns
167 columnPopupMenu = new ContextMenu();
168 updateTableColumnListeners(tableSkin.getColumns(), Collections.<TableColumnBase<?,?>>emptyList());
169 tableSkin.getVisibleLeafColumns().addListener(weakTableColumnsListener);
170 tableSkin.getColumns().addListener(weakTableColumnsListener);
171
172 // drag header region. Used to indicate the current column being reordered
173 dragHeader = new StackPane();
174 dragHeader.setVisible(false);
175 dragHeader.getStyleClass().setAll("column-drag-header");
176 dragHeader.setManaged(false);
177 dragHeader.getChildren().add(dragHeaderLabel);
178
179 // the header lives inside a NestedTableColumnHeader
180 header = createRootHeader();
181 header.setFocusTraversable(false);
182 header.setTableHeaderRow(this);
183
184 // The 'filler' area that extends from the right-most column to the edge
185 // of the tableview, or up to the 'column control' button
186 filler = new Region();
187 filler.getStyleClass().setAll("filler");
188
189 // Give focus to the table when an empty area of the header row is clicked.
190 // This ensures the user knows that the table has focus.
191 setOnMousePressed(e -> {
192 skin.getSkinnable().requestFocus();
193 });
194
195 // build the corner region button for showing the popup menu
196 final StackPane image = new StackPane();
197 image.setSnapToPixel(false);
198 image.getStyleClass().setAll("show-hide-column-image");
199 cornerRegion = new StackPane() {
200 @Override protected void layoutChildren() {
201 double imageWidth = image.snappedLeftInset() + image.snappedRightInset();
202 double imageHeight = image.snappedTopInset() + image.snappedBottomInset();
206 0, HPos.CENTER, VPos.CENTER);
207 }
208 };
209 cornerRegion.getStyleClass().setAll("show-hide-columns-button");
210 cornerRegion.getChildren().addAll(image);
211 cornerRegion.setVisible(tableSkin.tableMenuButtonVisibleProperty().get());
212 tableSkin.tableMenuButtonVisibleProperty().addListener(valueModel -> {
213 cornerRegion.setVisible(tableSkin.tableMenuButtonVisibleProperty().get());
214 requestLayout();
215 });
216 cornerRegion.setOnMousePressed(me -> {
217 // show a popupMenu which lists all columns
218 columnPopupMenu.show(cornerRegion, Side.BOTTOM, 0, 0);
219 me.consume();
220 });
221
222 // the actual header
223 // the region that is anchored above the vertical scrollbar
224 // a 'ghost' of the header being dragged by the user to force column
225 // reordering
226 getChildren().addAll(filler, header, cornerRegion, dragHeader);
227 }
228
229
230
231 /***************************************************************************
232 * *
233 * Listeners *
234 * *
235 **************************************************************************/
236
237 private InvalidationListener tableWidthListener = valueModel -> {
238 updateTableWidth();
239 };
240
241 private InvalidationListener tablePaddingListener = valueModel -> {
242 updateTableWidth();
243 };
244
245 private ListChangeListener visibleLeafColumnsListener = new ListChangeListener<TableColumn<?,?>>() {
246 @Override public void onChanged(ListChangeListener.Change<? extends TableColumn<?,?>> c) {
247 // This is necessary for RT-20300 (but was updated for RT-20840)
248 header.setHeadersNeedUpdate();
249 }
250 };
251
252 private final ListChangeListener tableColumnsListener = c -> {
253 while (c.next()) {
254 updateTableColumnListeners(c.getAddedSubList(), c.getRemoved());
255 }
256 };
257
258 private final InvalidationListener columnTextListener = observable -> {
259 TableColumnBase<?,?> column = (TableColumnBase<?,?>) ((StringProperty)observable).getBean();
260 CheckMenuItem menuItem = columnMenuItems.get(column);
261 if (menuItem != null) {
262 menuItem.setText(getText(column.getText(), column));
263 }
264 };
265
266 private final WeakInvalidationListener weakTableWidthListener =
267 new WeakInvalidationListener(tableWidthListener);
268
269 private final WeakInvalidationListener weakTablePaddingListener =
270 new WeakInvalidationListener(tablePaddingListener);
271
272 private final WeakListChangeListener weakVisibleLeafColumnsListener =
273 new WeakListChangeListener(visibleLeafColumnsListener);
274
275 private final WeakListChangeListener weakTableColumnsListener =
276 new WeakListChangeListener(tableColumnsListener);
277
278 private final WeakInvalidationListener weakColumnTextListener =
279 new WeakInvalidationListener(columnTextListener);
280
281
282
283 /***************************************************************************
284 * *
285 * Public Methods *
286 * *
287 **************************************************************************/
288
289 /** {@inheritDoc} */
290 @Override protected void layoutChildren() {
291 double x = scrollX;
292 double headerWidth = snapSize(header.prefWidth(-1));
293 double prefHeight = getHeight() - snappedTopInset() - snappedBottomInset();
294 double cornerWidth = snapSize(flow.getVbar().prefWidth(-1));
295
296 // position the main nested header
297 header.resizeRelocate(x, snappedTopInset(), headerWidth, prefHeight);
298
299 // position the filler region
300 final Control control = tableSkin.getSkinnable();
301 if (control == null) {
302 return;
303 }
304
305 final double controlInsets = control.snappedLeftInset() + control.snappedRightInset();
306 double fillerWidth = tableWidth - headerWidth + filler.getInsets().getLeft() - controlInsets;
307 fillerWidth -= tableSkin.tableMenuButtonVisibleProperty().get() ? cornerWidth : 0;
308 filler.setVisible(fillerWidth > 0);
309 if (fillerWidth > 0) {
310 filler.resizeRelocate(x + headerWidth, snappedTopInset(), fillerWidth, prefHeight);
311 }
312
313 // position the top-right rectangle (which sits above the scrollbar)
314 cornerRegion.resizeRelocate(tableWidth - cornerWidth, snappedTopInset(), cornerWidth, prefHeight);
315 }
316
317 /** {@inheritDoc} */
318 @Override protected double computePrefWidth(double height) {
319 return header.prefWidth(height);
320 }
321
322 /** {@inheritDoc} */
323 @Override protected double computeMinHeight(double width) {
324 return computePrefHeight(width);
325 }
326
327 /** {@inheritDoc} */
328 @Override protected double computePrefHeight(double width) {
329 // we hardcode 24.0 here to avoid RT-37616, where the
330 // entire header row would disappear when all columns were hidden.
331 double headerPrefHeight = header.prefHeight(width);
332 headerPrefHeight = headerPrefHeight == 0.0 ? 24.0 : headerPrefHeight;
333 return snappedTopInset() + headerPrefHeight + snappedBottomInset();
334 }
335
336 // protected to allow subclasses to provide a custom root header
337 protected NestedTableColumnHeader createRootHeader() {
338 return new NestedTableColumnHeader(tableSkin, null);
339 }
340
341 // protected to allow subclasses access to the TableViewSkinBase instance
342 protected TableViewSkinBase<?,?,?,?,?,?> getTableSkin() {
343 return this.tableSkin;
344 }
345
346 // protected to allow subclasses to modify the horizontal scrolling
347 protected void updateScrollX() {
348 scrollX = flow.getHbar().isVisible() ? -flow.getHbar().getValue() : 0.0F;
349 requestLayout();
350
351 // Fix for RT-36392: without this call even though we call requestLayout()
352 // we don't seem to ever see the layoutChildren() method above called,
353 // which means the layout is not always updated to use the latest scrollX.
354 layout();
355 }
356
357 public final void setReordering(boolean value) {
358 this.reordering.set(value);
359 }
360
361 public final boolean isReordering() {
362 return reordering.get();
363 }
364
365 public final BooleanProperty reorderingProperty() {
366 return reordering;
367 }
368
369 public TableColumnHeader getReorderingRegion() {
370 return reorderingRegion;
371 }
372
373 public void setReorderingColumn(TableColumnBase rc) {
374 dragHeaderLabel.setText(rc == null ? "" : rc.getText());
375 }
376
377 public void setReorderingRegion(TableColumnHeader reorderingRegion) {
378 this.reorderingRegion = reorderingRegion;
379
380 if (reorderingRegion != null) {
381 dragHeader.resize(reorderingRegion.getWidth(), dragHeader.getHeight());
382 }
383 }
384
385 public void setDragHeaderX(double dragHeaderX) {
386 dragHeader.setTranslateX(dragHeaderX);
387 }
388
389 public NestedTableColumnHeader getRootHeader() {
390 return header;
391 }
392
393 // protected to allow subclass to customise the width, to allow for features
394 // such as row headers
395 protected void updateTableWidth() {
396 // snapping added for RT-19428
397 final Control c = tableSkin.getSkinnable();
398 if (c == null) {
399 this.tableWidth = 0;
400 } else {
401 Insets insets = c.getInsets() == null ? Insets.EMPTY : c.getInsets();
402 double padding = snapSize(insets.getLeft()) + snapSize(insets.getRight());
403 this.tableWidth = snapSize(c.getWidth()) - padding;
404 }
405
406 clip.setWidth(tableWidth);
407 }
408
409 public TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col) {
410 if (col == null) return null;
411 List<TableColumnBase<?,?>> columnChain = new ArrayList<>();
412 columnChain.add(col);
413
414 TableColumnBase<?,?> parent = col.getParentColumn();
415 while (parent != null) {
416 columnChain.add(0, parent);
417 parent = parent.getParentColumn();
418 }
419
420 // we now have a list from top to bottom of a nested column hierarchy,
421 // and we can now navigate down to retrieve the header with ease
422 TableColumnHeader currentHeader = getRootHeader();
423 for (int depth = 0; depth < columnChain.size(); depth++) {
424 // this is the column we are looking for at this depth
425 TableColumnBase<?,?> column = columnChain.get(depth);
426
427 // and now we iterate through the nested table column header at this
428 // level to get the header
429 currentHeader = getColumnHeaderFor(column, currentHeader);
430 }
431 return currentHeader;
432 }
433
434 public TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col, TableColumnHeader currentHeader) {
435 if (currentHeader instanceof NestedTableColumnHeader) {
436 List<TableColumnHeader> headers = ((NestedTableColumnHeader)currentHeader).getColumnHeaders();
437
438 for (int i = 0; i < headers.size(); i++) {
439 TableColumnHeader header = headers.get(i);
440 if (header.getTableColumn() == col) {
441 return header;
442 }
443 }
444 }
445
446 return null;
447 }
448
449
450 /***************************************************************************
451 * *
452 * Private Implementation *
453 * *
454 **************************************************************************/
455
456 private void updateTableColumnListeners(List<? extends TableColumnBase<?,?>> added, List<? extends TableColumnBase<?,?>> removed) {
457 // remove binding from all removed items
458 for (TableColumnBase tc : removed) {
459 remove(tc);
460 }
461
462 rebuildColumnMenu();
463 }
464
465 private void remove(TableColumnBase<?,?> col) {
466 if (col == null) return;
467
468 CheckMenuItem item = columnMenuItems.remove(col);
469 if (item != null) {
470 col.textProperty().removeListener(weakColumnTextListener);
471 item.selectedProperty().unbindBidirectional(col.visibleProperty());
472
473 columnPopupMenu.getItems().remove(item);
474 }
475
476 if (! col.getColumns().isEmpty()) {
477 for (TableColumnBase tc : col.getColumns()) {
478 remove(tc);
479 }
480 }
481 }
482
483 private void rebuildColumnMenu() {
484 columnPopupMenu.getItems().clear();
485
486 for (TableColumnBase<?,?> col : getTableSkin().getColumns()) {
487 // we only create menu items for leaf columns, visible or not
488 if (col.getColumns().isEmpty()) {
489 createMenuItem(col);
490 } else {
491 List<TableColumnBase<?,?>> leafColumns = getLeafColumns(col);
492 for (TableColumnBase<?,?> _col : leafColumns) {
493 createMenuItem(_col);
494 }
495 }
496 }
497 }
498
499 private List<TableColumnBase<?,?>> getLeafColumns(TableColumnBase<?,?> col) {
500 List<TableColumnBase<?,?>> leafColumns = new ArrayList<>();
501
502 for (TableColumnBase<?,?> _col : col.getColumns()) {
503 if (_col.getColumns().isEmpty()) {
504 leafColumns.add(_col);
505 } else {
506 leafColumns.addAll(getLeafColumns(_col));
|
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 java.util.*;
29
30 import javafx.beans.InvalidationListener;
31 import javafx.beans.WeakInvalidationListener;
32 import javafx.beans.property.BooleanProperty;
33 import javafx.beans.property.ObjectProperty;
34 import javafx.beans.property.ReadOnlyObjectProperty;
35 import javafx.beans.property.ReadOnlyObjectWrapper;
36 import javafx.beans.property.SimpleBooleanProperty;
37 import javafx.beans.property.SimpleObjectProperty;
38 import javafx.beans.property.StringProperty;
39 import javafx.collections.ListChangeListener;
40 import javafx.collections.WeakListChangeListener;
41 import javafx.geometry.HPos;
42 import javafx.geometry.Insets;
43 import javafx.geometry.Side;
44 import javafx.geometry.VPos;
45 import javafx.scene.control.CheckMenuItem;
46 import javafx.scene.control.ContextMenu;
47 import javafx.scene.control.Control;
48 import javafx.scene.control.Label;
49 import javafx.scene.control.TableColumn;
50 import javafx.scene.control.TableColumnBase;
51 import javafx.scene.layout.Pane;
52 import javafx.scene.layout.Region;
53 import javafx.scene.layout.StackPane;
54 import javafx.scene.shape.Rectangle;
55
56 import com.sun.javafx.scene.control.skin.resources.ControlResources;
57
58 /**
59 * Region responsible for painting the entire row of column headers.
60 *
61 * @since 9
62 * @see TableView
63 * @see TableViewSkin
64 * @see TreeTableView
65 * @see TreeTableViewSkin
66 */
67 public class TableHeaderRow extends StackPane {
68
69 /***************************************************************************
70 * *
71 * Static Fields *
72 * *
73 **************************************************************************/
74
75 private static final String MENU_SEPARATOR =
76 ControlResources.getString("TableView.nestedColumnControlMenuSeparator");
77
78
79
80 /***************************************************************************
81 * *
82 * Private Fields *
83 * *
84 **************************************************************************/
85
86 private final VirtualFlow flow;
87 private final TableViewSkinBase<?,?,?,?,?> tableSkin;
88 private Map<TableColumnBase, CheckMenuItem> columnMenuItems = new HashMap<TableColumnBase, CheckMenuItem>();
89 private double scrollX;
90 private double tableWidth;
91 private Rectangle clip;
92 private TableColumnHeader reorderingRegion;
93
94 /**
95 * This is the ghosted region representing the table column that is being
96 * dragged. It moves along the x-axis but is fixed in the y-axis.
97 */
98 private StackPane dragHeader;
99 private final Label dragHeaderLabel = new Label();
100
101 private Region filler;
102
103 /**
104 * This is the region where the user can interact with to show/hide columns.
105 * It is positioned in the top-right hand corner of the TableHeaderRow, and
106 * when clicked shows a PopupMenu consisting of all leaf columns.
107 */
108 private Pane cornerRegion;
109
110 /**
111 * PopupMenu shown to users to allow for them to hide/show columns in the
112 * table.
113 */
114 private ContextMenu columnPopupMenu;
115
116
117
118 /***************************************************************************
119 * *
120 * Listeners *
121 * *
122 **************************************************************************/
123
124 private InvalidationListener tableWidthListener = o -> updateTableWidth();
125
126 private InvalidationListener tablePaddingListener = o -> updateTableWidth();
127
128 // This is necessary for RT-20300 (but was updated for RT-20840)
129 private ListChangeListener visibleLeafColumnsListener = c -> getRootHeader().setHeadersNeedUpdate();
130
131 private final ListChangeListener tableColumnsListener = c -> {
132 while (c.next()) {
133 updateTableColumnListeners(c.getAddedSubList(), c.getRemoved());
134 }
135 };
136
137 private final InvalidationListener columnTextListener = observable -> {
138 TableColumnBase<?,?> column = (TableColumnBase<?,?>) ((StringProperty)observable).getBean();
139 CheckMenuItem menuItem = columnMenuItems.get(column);
140 if (menuItem != null) {
141 menuItem.setText(getText(column.getText(), column));
142 }
143 };
144
145 private final WeakInvalidationListener weakTableWidthListener =
146 new WeakInvalidationListener(tableWidthListener);
147
148 private final WeakInvalidationListener weakTablePaddingListener =
149 new WeakInvalidationListener(tablePaddingListener);
150
151 private final WeakListChangeListener weakVisibleLeafColumnsListener =
152 new WeakListChangeListener(visibleLeafColumnsListener);
153
154 private final WeakListChangeListener weakTableColumnsListener =
155 new WeakListChangeListener(tableColumnsListener);
156
157 private final WeakInvalidationListener weakColumnTextListener =
158 new WeakInvalidationListener(columnTextListener);
159
160
161
162 /***************************************************************************
163 * *
164 * Constructor *
165 * *
166 **************************************************************************/
167
168 /**
169 * Creates a new TableHeaderRow instance to visually represent the column
170 * header area of controls such as {@link javafx.scene.control.TableView} and
171 * {@link javafx.scene.control.TreeTableView}.
172 *
173 * @param skin The skin used by the UI control.
174 */
175 public TableHeaderRow(final TableViewSkinBase skin) {
176 this.tableSkin = skin;
177 this.flow = skin.flow;
178
179 getStyleClass().setAll("column-header-background");
180
181 // clip the header so it doesn't show outside of the table bounds
182 clip = new Rectangle();
183 clip.setSmooth(false);
184 clip.heightProperty().bind(heightProperty());
185 setClip(clip);
186
187 // listen to table width to keep header in sync
188 updateTableWidth();
189 tableSkin.getSkinnable().widthProperty().addListener(weakTableWidthListener);
190 tableSkin.getSkinnable().paddingProperty().addListener(weakTablePaddingListener);
191 skin.getVisibleLeafColumns().addListener(weakVisibleLeafColumnsListener);
192
193 // popup menu for hiding/showing columns
194 columnPopupMenu = new ContextMenu();
195 updateTableColumnListeners(tableSkin.getColumns(), Collections.<TableColumnBase<?,?>>emptyList());
196 tableSkin.getVisibleLeafColumns().addListener(weakTableColumnsListener);
197 tableSkin.getColumns().addListener(weakTableColumnsListener);
198
199 // drag header region. Used to indicate the current column being reordered
200 dragHeader = new StackPane();
201 dragHeader.setVisible(false);
202 dragHeader.getStyleClass().setAll("column-drag-header");
203 dragHeader.setManaged(false);
204 dragHeader.getChildren().add(dragHeaderLabel);
205
206 // the header lives inside a NestedTableColumnHeader
207 NestedTableColumnHeader rootHeader = new NestedTableColumnHeader(tableSkin, null);
208 setRootHeader(rootHeader);
209 rootHeader.setFocusTraversable(false);
210 rootHeader.setTableHeaderRow(this);
211
212 // The 'filler' area that extends from the right-most column to the edge
213 // of the tableview, or up to the 'column control' button
214 filler = new Region();
215 filler.getStyleClass().setAll("filler");
216
217 // Give focus to the table when an empty area of the header row is clicked.
218 // This ensures the user knows that the table has focus.
219 setOnMousePressed(e -> {
220 skin.getSkinnable().requestFocus();
221 });
222
223 // build the corner region button for showing the popup menu
224 final StackPane image = new StackPane();
225 image.setSnapToPixel(false);
226 image.getStyleClass().setAll("show-hide-column-image");
227 cornerRegion = new StackPane() {
228 @Override protected void layoutChildren() {
229 double imageWidth = image.snappedLeftInset() + image.snappedRightInset();
230 double imageHeight = image.snappedTopInset() + image.snappedBottomInset();
234 0, HPos.CENTER, VPos.CENTER);
235 }
236 };
237 cornerRegion.getStyleClass().setAll("show-hide-columns-button");
238 cornerRegion.getChildren().addAll(image);
239 cornerRegion.setVisible(tableSkin.tableMenuButtonVisibleProperty().get());
240 tableSkin.tableMenuButtonVisibleProperty().addListener(valueModel -> {
241 cornerRegion.setVisible(tableSkin.tableMenuButtonVisibleProperty().get());
242 requestLayout();
243 });
244 cornerRegion.setOnMousePressed(me -> {
245 // show a popupMenu which lists all columns
246 columnPopupMenu.show(cornerRegion, Side.BOTTOM, 0, 0);
247 me.consume();
248 });
249
250 // the actual header
251 // the region that is anchored above the vertical scrollbar
252 // a 'ghost' of the header being dragged by the user to force column
253 // reordering
254 getChildren().addAll(filler, rootHeader, cornerRegion, dragHeader);
255 }
256
257
258
259 /***************************************************************************
260 * *
261 * Properties *
262 * *
263 **************************************************************************/
264
265 // --- reordering
266 private BooleanProperty reordering = new SimpleBooleanProperty(this, "reordering", false) {
267 @Override protected void invalidated() {
268 TableColumnHeader r = getReorderingRegion();
269 if (r != null) {
270 double dragHeaderHeight = r.getNestedColumnHeader() != null ?
271 r.getNestedColumnHeader().getHeight() :
272 getReorderingRegion().getHeight();
273
274 dragHeader.resize(dragHeader.getWidth(), dragHeaderHeight);
275 dragHeader.setTranslateY(getHeight() - dragHeaderHeight);
276 }
277 dragHeader.setVisible(isReordering());
278 }
279 };
280 final void setReordering(boolean value) {
281 this.reordering.set(value);
282 }
283 final boolean isReordering() {
284 return reordering.get();
285 }
286 final BooleanProperty reorderingProperty() {
287 return reordering;
288 }
289
290 // --- root header
291 /*
292 * The header row is actually just one NestedTableColumnHeader that spans
293 * the entire width. Nested within this is the TableColumnHeader's and
294 * NestedTableColumnHeader's, as necessary. This makes it nice and clean
295 * to handle column reordering - we basically enforce the rule that column
296 * reordering only occurs within a single NestedTableColumnHeader, and only
297 * at that level.
298 */
299 private ReadOnlyObjectWrapper<NestedTableColumnHeader> rootHeader = new ReadOnlyObjectWrapper<>(this, "rootHeader");
300 private final ReadOnlyObjectProperty<NestedTableColumnHeader> rootHeaderProperty() {
301 return rootHeader.getReadOnlyProperty();
302 }
303 final NestedTableColumnHeader getRootHeader() {
304 return rootHeader.get();
305 }
306 private final void setRootHeader(NestedTableColumnHeader value) {
307 rootHeader.set(value);
308 }
309
310
311
312 /***************************************************************************
313 * *
314 * Public API *
315 * *
316 **************************************************************************/
317
318 /** {@inheritDoc} */
319 @Override protected void layoutChildren() {
320 double x = scrollX;
321 double headerWidth = snapSize(getRootHeader().prefWidth(-1));
322 double prefHeight = getHeight() - snappedTopInset() - snappedBottomInset();
323 double cornerWidth = snapSize(flow.getVbar().prefWidth(-1));
324
325 // position the main nested header
326 getRootHeader().resizeRelocate(x, snappedTopInset(), headerWidth, prefHeight);
327
328 // position the filler region
329 final Control control = tableSkin.getSkinnable();
330 if (control == null) {
331 return;
332 }
333
334 final double controlInsets = control.snappedLeftInset() + control.snappedRightInset();
335 double fillerWidth = tableWidth - headerWidth + filler.getInsets().getLeft() - controlInsets;
336 fillerWidth -= tableSkin.tableMenuButtonVisibleProperty().get() ? cornerWidth : 0;
337 filler.setVisible(fillerWidth > 0);
338 if (fillerWidth > 0) {
339 filler.resizeRelocate(x + headerWidth, snappedTopInset(), fillerWidth, prefHeight);
340 }
341
342 // position the top-right rectangle (which sits above the scrollbar)
343 cornerRegion.resizeRelocate(tableWidth - cornerWidth, snappedTopInset(), cornerWidth, prefHeight);
344 }
345
346 /** {@inheritDoc} */
347 @Override protected double computePrefWidth(double height) {
348 return getRootHeader().prefWidth(height);
349 }
350
351 /** {@inheritDoc} */
352 @Override protected double computeMinHeight(double width) {
353 return computePrefHeight(width);
354 }
355
356 /** {@inheritDoc} */
357 @Override protected double computePrefHeight(double width) {
358 // we hardcode 24.0 here to avoid RT-37616, where the
359 // entire header row would disappear when all columns were hidden.
360 double headerPrefHeight = getRootHeader().prefHeight(width);
361 headerPrefHeight = headerPrefHeight == 0.0 ? 24.0 : headerPrefHeight;
362 return snappedTopInset() + headerPrefHeight + snappedBottomInset();
363 }
364
365 // used to be protected to allow subclasses to modify the horizontal scrolling,
366 // but made private again for JDK 9
367 void updateScrollX() {
368 scrollX = flow.getHbar().isVisible() ? -flow.getHbar().getValue() : 0.0F;
369 requestLayout();
370
371 // Fix for RT-36392: without this call even though we call requestLayout()
372 // we don't seem to ever see the layoutChildren() method above called,
373 // which means the layout is not always updated to use the latest scrollX.
374 layout();
375 }
376
377 // used to be protected to allow subclass to customise the width, to allow for features
378 // such as row headers, but made private again for JDK 9
379 private void updateTableWidth() {
380 // snapping added for RT-19428
381 final Control c = tableSkin.getSkinnable();
382 if (c == null) {
383 this.tableWidth = 0;
384 } else {
385 Insets insets = c.getInsets() == null ? Insets.EMPTY : c.getInsets();
386 double padding = snapSize(insets.getLeft()) + snapSize(insets.getRight());
387 this.tableWidth = snapSize(c.getWidth()) - padding;
388 }
389
390 clip.setWidth(tableWidth);
391 }
392
393
394
395 /***************************************************************************
396 * *
397 * Private Implementation *
398 * *
399 **************************************************************************/
400
401 TableColumnHeader getReorderingRegion() {
402 return reorderingRegion;
403 }
404
405 void setReorderingColumn(TableColumnBase rc) {
406 dragHeaderLabel.setText(rc == null ? "" : rc.getText());
407 }
408
409 void setReorderingRegion(TableColumnHeader reorderingRegion) {
410 this.reorderingRegion = reorderingRegion;
411
412 if (reorderingRegion != null) {
413 dragHeader.resize(reorderingRegion.getWidth(), dragHeader.getHeight());
414 }
415 }
416
417 void setDragHeaderX(double dragHeaderX) {
418 dragHeader.setTranslateX(dragHeaderX);
419 }
420
421 TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col) {
422 if (col == null) return null;
423 List<TableColumnBase<?,?>> columnChain = new ArrayList<>();
424 columnChain.add(col);
425
426 TableColumnBase<?,?> parent = col.getParentColumn();
427 while (parent != null) {
428 columnChain.add(0, parent);
429 parent = parent.getParentColumn();
430 }
431
432 // we now have a list from top to bottom of a nested column hierarchy,
433 // and we can now navigate down to retrieve the header with ease
434 TableColumnHeader currentHeader = getRootHeader();
435 for (int depth = 0; depth < columnChain.size(); depth++) {
436 // this is the column we are looking for at this depth
437 TableColumnBase<?,?> column = columnChain.get(depth);
438
439 // and now we iterate through the nested table column header at this
440 // level to get the header
441 currentHeader = getColumnHeaderFor(column, currentHeader);
442 }
443 return currentHeader;
444 }
445
446 private TableColumnHeader getColumnHeaderFor(final TableColumnBase<?,?> col, TableColumnHeader currentHeader) {
447 if (currentHeader instanceof NestedTableColumnHeader) {
448 List<TableColumnHeader> headers = ((NestedTableColumnHeader)currentHeader).getColumnHeaders();
449
450 for (int i = 0; i < headers.size(); i++) {
451 TableColumnHeader header = headers.get(i);
452 if (header.getTableColumn() == col) {
453 return header;
454 }
455 }
456 }
457
458 return null;
459 }
460
461 private void updateTableColumnListeners(List<? extends TableColumnBase<?,?>> added, List<? extends TableColumnBase<?,?>> removed) {
462 // remove binding from all removed items
463 for (TableColumnBase tc : removed) {
464 remove(tc);
465 }
466
467 rebuildColumnMenu();
468 }
469
470 private void remove(TableColumnBase<?,?> col) {
471 if (col == null) return;
472
473 CheckMenuItem item = columnMenuItems.remove(col);
474 if (item != null) {
475 col.textProperty().removeListener(weakColumnTextListener);
476 item.selectedProperty().unbindBidirectional(col.visibleProperty());
477
478 columnPopupMenu.getItems().remove(item);
479 }
480
481 if (! col.getColumns().isEmpty()) {
482 for (TableColumnBase tc : col.getColumns()) {
483 remove(tc);
484 }
485 }
486 }
487
488 private void rebuildColumnMenu() {
489 columnPopupMenu.getItems().clear();
490
491 for (TableColumnBase<?,?> col : tableSkin.getColumns()) {
492 // we only create menu items for leaf columns, visible or not
493 if (col.getColumns().isEmpty()) {
494 createMenuItem(col);
495 } else {
496 List<TableColumnBase<?,?>> leafColumns = getLeafColumns(col);
497 for (TableColumnBase<?,?> _col : leafColumns) {
498 createMenuItem(_col);
499 }
500 }
501 }
502 }
503
504 private List<TableColumnBase<?,?>> getLeafColumns(TableColumnBase<?,?> col) {
505 List<TableColumnBase<?,?>> leafColumns = new ArrayList<>();
506
507 for (TableColumnBase<?,?> _col : col.getColumns()) {
508 if (_col.getColumns().isEmpty()) {
509 leafColumns.add(_col);
510 } else {
511 leafColumns.addAll(getLeafColumns(_col));
|