-
Notifications
You must be signed in to change notification settings - Fork 264
/
Borders.java
815 lines (695 loc) · 32.9 KB
/
Borders.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
/**
* Copyright (c) 2013, 2024, ControlsFX
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.controlsfx.tools;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javax.swing.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A utility class that allows you to wrap JavaFX {@link Node Nodes} with a border,
* in a way somewhat analogous to the Swing {@link BorderFactory} (although with
* less options as a lot of what the Swing BorderFactory offers resulted in
* ugly borders!).
*
* <p>The Borders class provides a fluent API for specifying the properties of
* each border. It is possible to create multiple borders around a Node simply
* by continuing to call additional methods before you call the final
* {@link Borders#build()} method. To use the Borders class, you simply call
* {@link Borders#wrap(Node)}, passing in the Node you wish to wrap the border(s)
* around.
*
* <h3>Examples</h3>
* <p>Firstly, lets wrap a JavaFX Button node with a simple line border that looks
* like the following:
*
* <br>
* <center><img src="borders-lineBorder.png" alt="Screenshot of Borders.LineBorders"></center>
*
* <p>Here's the code:</p>
*
* <pre>
* {@code
* Button button = new Button("Hello World!");
* Node wrappedButton = Borders.wrap(button).lineBorder().buildAll();
* }</pre>
*
* <p>Easy, isn't it!? You can make the border look a little nicer by replacing
* the line border with an {@link EtchedBorders etched border}. An etched border
* has a subtle inner (or outer) line that makes the border stand out a bit more,
* like this:
*
* <br>
* <center><img src="borders-etchedBorder.png" alt="Screenshot of Borders.EtchedBorders"></center>
*
* <p>Now that's one good looking border! Here's the code:</p>
*
* <pre>
* {@code
* Button button = new Button("Hello World!");
* Node wrappedButton = Borders.wrap(button).etchedBorder().buildAll();
* }</pre>
*
* <p>In some circumstances you want to have multiple borders. For example,
* you might two line borders. That's easy:
*
* <br>
* <center><img src="borders-twoLines.png" alt="Screenshot of two Borders.LineBorders"></center>
*
* <pre>
* {@code
* Node wrappedButton = Borders.wrap(button)
* .lineBorder().color(Color.RED).build()
* .lineBorder().color(Color.GREEN).build()
* .build();
* }</pre>
*
* <p>You simply chain the borders together, going from inside to outside!</p>
*
* <p>Because of all the configuration options it isn't possible to list all the
* functionality of all the border types, so refer to the rest of the javadocs
* for inspiration.</p>
*/
public final class Borders {
/**************************************************************************
*
* Static fields
*
**************************************************************************/
private static final Color DEFAULT_BORDER_COLOR = Color.DARKGRAY;
/**************************************************************************
*
* Internal fields
*
**************************************************************************/
private final Node node;
private final List<Border> borders;
/**************************************************************************
*
* Fluent API entry method(s)
*
**************************************************************************/
public static Borders wrap(Node n) {
return new Borders(n);
}
/**************************************************************************
*
* Private Constructor
*
**************************************************************************/
private Borders(Node n) {
this.node = n;
this.borders = new ArrayList<>();
}
/**************************************************************************
*
* Fluent API
*
**************************************************************************/
/**
* Often times it is useful to have a bit of whitespace around a Node, to
* separate it from what it is next to. Call this method to begin building
* a border that will wrap the node with a given amount of whitespace
* (which can vary between the top, right, bottom, and left sides).
*/
public EmptyBorders emptyBorder() {
return new EmptyBorders(this);
}
/**
* The etched border look is essentially equivalent to the {@link #lineBorder()}
* look, except rather than one line, there are two. What is commonly done in
* this circumstance is that one of the lines is a very light colour (commonly
* white), which gives a nice etched look. Refer to the API in {@link EtchedBorders}
* for more information.
*/
public EtchedBorders etchedBorder() {
return new EtchedBorders(this);
}
/**
* Creates a nice, simple border around the node. Note that there are many
* configuration options in {@link LineBorders}, so explore it carefully.
*/
public LineBorders lineBorder() {
return new LineBorders(this);
}
/**
* Allows for developers to develop custom {@link Border} implementations,
* and to wrap them around a Node. Note that of course this is mostly
* redundant (as you could just call {@link Border#wrap(Node)} directly).
* The only benefit is if you're creating a compound border consisting of
* multiple borders, and you want your custom border included as part of
* this.
*/
public Borders addBorder(Border border) {
borders.add(border);
return this;
}
/**
* Returns the original node wrapped in zero or more borders, as specified
* using the fluent API.
*/
public Node build() {
// we iterate through the borders list in reverse order
Node bundle = node;
for (int i = borders.size() - 1; i >= 0; i--) {
Border border = borders.get(i);
bundle = border.wrap(bundle);
}
return bundle;
}
/**************************************************************************
*
* Support classes
*
**************************************************************************/
/**
* A fluent API that is only indirectly instantiable via the {@link Borders}
* fluent API, and which allows for an {@link Borders#emptyBorder() empty border}
* to be wrapped around a given Node.
*/
public class EmptyBorders {
private final Borders parent;
private double top;
private double right;
private double bottom;
private double left;
// private on purpose - this class is not directly instantiable.
private EmptyBorders(Borders parent) {
this.parent = parent;
}
/**
* Specifies that the wrapped Node should have the given padding around
* all four sides of itself.
*/
public EmptyBorders padding(double padding) {
return padding(padding, padding, padding, padding);
}
/**
* Specifies that the wrapped Node should be wrapped with the given
* padding for each of its four sides, going in the order top, right,
* bottom, and finally left.
*/
public EmptyBorders padding(double top, double right, double bottom, double left) {
this.top = top;
this.right = right;
this.bottom = bottom;
this.left = left;
return this;
}
/**
* Builds the {@link Border} and {@link Borders#addBorder(Border) adds it}
* to the list of borders to wrap around the given Node (which will be
* constructed and returned when {@link Borders#build()} is called.
*/
public Borders build() {
parent.addBorder(new StrokeBorder(null, buildStroke()));
return parent;
}
/**
* A convenience method, this is equivalent to calling
* {@link #build()} followed by {@link Borders#build()}. In other words,
* calling this will return the original Node wrapped in all its borders
* specified.
*/
public Node buildAll() {
build();
return parent.build();
}
private BorderStroke buildStroke() {
return new BorderStroke(
null,
BorderStrokeStyle.NONE,
null,
new BorderWidths(top, right, bottom, left),
Insets.EMPTY);
}
}
/**
* A fluent API that is only indirectly instantiable via the {@link Borders}
* fluent API, and which allows for an {@link Borders#etchedBorder() etched border}
* to be wrapped around a given Node.
*/
public class EtchedBorders {
private final Borders parent;
private String title;
private boolean raised = false;
private double outerTopPadding = 10;
private double outerRightPadding = 10;
private double outerBottomPadding = 10;
private double outerLeftPadding = 10;
private double innerTopPadding = 15;
private double innerRightPadding = 15;
private double innerBottomPadding = 15;
private double innerLeftPadding = 15;
private double topLeftRadius = 0;
private double topRightRadius = 0;
private double bottomRightRadius = 0;
private double bottomLeftRadius = 0;
private Color highlightColor = DEFAULT_BORDER_COLOR;
private Color shadowColor = Color.WHITE;
// private on purpose - this class is not directly instantiable.
private EtchedBorders(Borders parent) {
this.parent = parent;
}
/**
* Specifies the highlight colour to use in the etched border.
*/
public EtchedBorders highlight(Color highlight) {
this.highlightColor = highlight;
return this;
}
/**
* Specifies the shadow colour to use in the etched border.
*/
public EtchedBorders shadow(Color shadow) {
this.shadowColor = shadow;
return this;
}
/**
* Specifies the order in which the highlight and shadow colours are
* placed. A raised etched border has the shadow colour on the outside
* of the border, whereas a non-raised (or lowered) etched border has
* the shadow colour on the inside of the border.
*/
public EtchedBorders raised() {
raised = true;
return this;
}
/**
* If desired, this specifies the title text to show in this border.
*/
public EtchedBorders title(String title) {
this.title = title;
return this;
}
/**
* Specifies the outer padding of the four lines of this border.
*/
public EtchedBorders outerPadding(double padding) {
return outerPadding(padding, padding, padding, padding);
}
/**
* Specifies that the line wrapping the node should have outer padding
* as specified, with each padding being independently configured, going
* in the order top, right, bottom, and left.
*/
public EtchedBorders outerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
this.outerTopPadding = topPadding;
this.outerRightPadding = rightPadding;
this.outerBottomPadding = bottomPadding;
this.outerLeftPadding = leftPadding;
return this;
}
/**
* Specifies the inner padding of the four lines of this border.
*/
public EtchedBorders innerPadding(double padding) {
return innerPadding(padding, padding, padding, padding);
}
/**
* Specifies that the line wrapping the node should have inner padding
* as specified, with each padding being independently configured, going
* in the order top, right, bottom, and left.
*/
public EtchedBorders innerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
this.innerTopPadding = topPadding;
this.innerRightPadding = rightPadding;
this.innerBottomPadding = bottomPadding;
this.innerLeftPadding = leftPadding;
return this;
}
/**
* Specifies the radius of the four corners of the lines of this border.
*/
public EtchedBorders radius(double radius) {
return radius(radius, radius, radius, radius);
}
/**
* Specifies that the etched line wrapping the node should have corner radii
* as specified, with each radius being independently configured, going
* in the order top-left, top-right, bottom-right, and finally bottom-left.
*/
public EtchedBorders radius(double topLeft, double topRight, double bottomRight, double bottomLeft) {
this.topLeftRadius = topLeft;
this.topRightRadius = topRight;
this.bottomRightRadius = bottomRight;
this.bottomLeftRadius = bottomLeft;
return this;
}
/**
* Builds the {@link Border} and {@link Borders#addBorder(Border) adds it}
* to the list of borders to wrap around the given Node (which will be
* constructed and returned when {@link Borders#build()} is called.
*/
public Borders build() {
Color inner = raised ? shadowColor : highlightColor;
Color outer = raised ? highlightColor : shadowColor;
BorderStroke innerStroke = new BorderStroke(
inner,
BorderStrokeStyle.SOLID,
new CornerRadii(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, false),
new BorderWidths(1));
BorderStroke outerStroke = new BorderStroke(
outer,
BorderStrokeStyle.SOLID,
new CornerRadii(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, false),
new BorderWidths(1),
new Insets(1));
BorderStroke outerPadding = new EmptyBorders(parent)
.padding(outerTopPadding, outerRightPadding, outerBottomPadding, outerLeftPadding)
.buildStroke();
BorderStroke innerPadding = new EmptyBorders(parent)
.padding(innerTopPadding, innerRightPadding, innerBottomPadding, innerLeftPadding)
.buildStroke();
parent.addBorder(new StrokeBorder(null, outerPadding));
parent.addBorder(new StrokeBorder(title, innerStroke, outerStroke));
parent.addBorder(new StrokeBorder(null, innerPadding));
return parent;
}
/**
* A convenience method, this is equivalent to calling
* {@link #build()} followed by {@link Borders#build()}. In other words,
* calling this will return the original Node wrapped in all its borders
* specified.
*/
public Node buildAll() {
build();
return parent.build();
}
}
/**
* A fluent API that is only indirectly instantiable via the {@link Borders}
* fluent API, and which allows for a {@link Borders#lineBorder() line border}
* to be wrapped around a given Node.
*/
public class LineBorders {
private final Borders parent;
private String title;
private BorderStrokeStyle strokeStyle = BorderStrokeStyle.SOLID;
private Color topColor = DEFAULT_BORDER_COLOR;
private Color rightColor = DEFAULT_BORDER_COLOR;
private Color bottomColor = DEFAULT_BORDER_COLOR;
private Color leftColor = DEFAULT_BORDER_COLOR;
private double outerTopPadding = 10;
private double outerRightPadding = 10;
private double outerBottomPadding = 10;
private double outerLeftPadding = 10;
private double innerTopPadding = 15;
private double innerRightPadding = 15;
private double innerBottomPadding = 15;
private double innerLeftPadding = 15;
private double topThickness = 1;
private double rightThickness = 1;
private double bottomThickness = 1;
private double leftThickness = 1;
private double topLeftRadius = 0;
private double topRightRadius = 0;
private double bottomRightRadius = 0;
private double bottomLeftRadius = 0;
// private on purpose - this class is not directly instantiable.
private LineBorders(Borders parent) {
this.parent = parent;
}
/**
* Specifies the colour to use for all four sides of this border.
*/
public LineBorders color(Color color) {
return color(color, color, color, color);
}
/**
* Specifies that the wrapped Node should be wrapped with the given
* colours for each of its four sides, going in the order top, right,
* bottom, and finally left.
*/
public LineBorders color(Color topColor, Color rightColor, Color bottomColor, Color leftColor) {
this.topColor = topColor;
this.rightColor = rightColor;
this.bottomColor = bottomColor;
this.leftColor = leftColor;
return this;
}
/**
* Specifies which {@link BorderStrokeStyle} to use for this line border.
* By default this is {@link BorderStrokeStyle#SOLID}, but you can use
* any other style (such as {@link BorderStrokeStyle#DASHED},
* {@link BorderStrokeStyle#DOTTED}, or a custom style built using
* {@link BorderStrokeStyle#BorderStrokeStyle(javafx.scene.shape.StrokeType, javafx.scene.shape.StrokeLineJoin, javafx.scene.shape.StrokeLineCap, double, double, List)}.
*/
public LineBorders strokeStyle(BorderStrokeStyle strokeStyle) {
this.strokeStyle = strokeStyle;
return this;
}
/**
* Specifies the inner padding of the four lines of this border.
*/
public LineBorders outerPadding(double padding) {
return outerPadding(padding, padding, padding, padding);
}
/**
* Specifies that the line wrapping the node should have outer padding
* as specified, with each padding being independently configured, going
* in the order top, right, bottom, and left.
*/
public LineBorders outerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
this.outerTopPadding = topPadding;
this.outerRightPadding = rightPadding;
this.outerBottomPadding = bottomPadding;
this.outerLeftPadding = leftPadding;
return this;
}
/**
* Specifies the outer padding of the four lines of this border.
*/
public LineBorders innerPadding(double padding) {
return innerPadding(padding, padding, padding, padding);
}
/**
* Specifies that the line wrapping the node should have inner padding
* as specified, with each padding being independently configured, going
* in the order top, right, bottom, and left.
*/
public LineBorders innerPadding(double topPadding, double rightPadding, double bottomPadding, double leftPadding) {
this.innerTopPadding = topPadding;
this.innerRightPadding = rightPadding;
this.innerBottomPadding = bottomPadding;
this.innerLeftPadding = leftPadding;
return this;
}
/**
* Specifies the thickness of the line to use on all four sides of this
* border.
*/
public LineBorders thickness(double thickness) {
return thickness(thickness, thickness, thickness, thickness);
}
/**
* Specifies that the wrapped Node should be wrapped with the given
* line thickness for each of its four sides, going in the order top, right,
* bottom, and finally left.
*/
public LineBorders thickness(double topThickness, double rightThickness, double bottomThickness, double leftThickness) {
this.topThickness = topThickness;
this.rightThickness = rightThickness;
this.bottomThickness = bottomThickness;
this.leftThickness = leftThickness;
return this;
}
/**
* Specifies the radius of the four corners of the line of this border.
*/
public LineBorders radius(double radius) {
return radius(radius, radius, radius, radius);
}
/**
* Specifies that the line wrapping the node should have corner radii
* as specified, with each radius being independently configured, going
* in the order top-left, top-right, bottom-right, and finally bottom-left.
*/
public LineBorders radius(double topLeft, double topRight, double bottomRight, double bottomLeft) {
this.topLeftRadius = topLeft;
this.topRightRadius = topRight;
this.bottomRightRadius = bottomRight;
this.bottomLeftRadius = bottomLeft;
return this;
}
/**
* If desired, this specifies the title text to show in this border.
*/
public LineBorders title(String title) {
this.title = title;
return this;
}
/**
* Builds the {@link Border} and {@link Borders#addBorder(Border) adds it}
* to the list of borders to wrap around the given Node (which will be
* constructed and returned when {@link Borders#build()} is called.
*/
public Borders build() {
BorderStroke borderStroke = new BorderStroke(
topColor, rightColor, bottomColor, leftColor,
strokeStyle, strokeStyle, strokeStyle, strokeStyle,
new CornerRadii(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius, false),
new BorderWidths(topThickness, rightThickness, bottomThickness, leftThickness),
null);
BorderStroke outerPadding = new EmptyBorders(parent)
.padding(outerTopPadding, outerRightPadding, outerBottomPadding, outerLeftPadding)
.buildStroke();
BorderStroke innerPadding = new EmptyBorders(parent)
.padding(innerTopPadding, innerRightPadding, innerBottomPadding, innerLeftPadding)
.buildStroke();
parent.addBorder(new StrokeBorder(null, outerPadding));
parent.addBorder(new StrokeBorder(title, borderStroke));
parent.addBorder(new StrokeBorder(null, innerPadding));
return parent;
}
/**
* A convenience method, this is equivalent to calling
* {@link #build()} followed by {@link Borders#build()}. In other words,
* calling this will return the original Node wrapped in all its borders
* specified.
*/
public Node buildAll() {
build();
return parent.build();
}
}
/**************************************************************************
*
* Support interfaces
*
**************************************************************************/
/**
* The public interface used by the {@link Borders} API to wrap nodes with
* zero or more Border implementations. ControlsFX ships with a few
* Border implementations (current {@link EmptyBorders}, {@link LineBorders},
* and {@link EtchedBorders}). As noted in {@link Borders#addBorder(Border)},
* this interface is relatively pointless, unless you plan to wrap a node
* with multiple borders and you want to use a custom {@link Border}
* implementation for at least one border. In this case, you can simply
* call {@link Borders#addBorder(Border)} with your custom border, when
* appropriate.
*/
@FunctionalInterface
public static interface Border {
/**
* Given a {@link Node}, this method should return a Node that contains
* the original Node and also has wrapped it with an appropriate border.
*/
public Node wrap(Node n);
}
/**************************************************************************
*
* Private support classes
*
**************************************************************************/
// --- Border implementations
private static class StrokeBorder implements Border {
private static final int TITLE_PADDING = 3;
private static final double GAP_PADDING = TITLE_PADDING * 2 - 1;
private final String title;
private final BorderStroke[] borderStrokes;
public StrokeBorder(String title, BorderStroke... borderStrokes) {
this.title = title;
this.borderStrokes = borderStrokes;
}
@Override public Node wrap(final Node n) {
return new StackPane() {
Label titleLabel;
{
// add in the node we are wrapping
getChildren().add(n);
// if the title string is set, then also add in the title label
if (title != null) {
titleLabel = new Label(title) {
@Override
protected double computePrefWidth(double v) {
return super.computePrefWidth(v) + 2 * TITLE_PADDING;
}
@Override
protected void layoutChildren() {
super.layoutChildren();
final double labelHeight = titleLabel.prefHeight(-1);
titleLabel.relocate(TITLE_PADDING * 2, -labelHeight / 2.0 - 1);
}
};
// give the text a bit of space on the left...
titleLabel.setPadding(new Insets(0, 0, 0, TITLE_PADDING));
getChildren().add(titleLabel);
needsLayoutProperty().addListener((obs, ov, nv) -> titleLabel.requestLayout());
widthProperty().addListener(o -> setBorder(createTitledBorder(titleLabel, getWidth())));
setBorder(createTitledBorder(titleLabel, getWidth()));
} else {
setBorder(new javafx.scene.layout.Border(borderStrokes));
}
}
private javafx.scene.layout.Border createTitledBorder(Label titleLabel, double width) {
final double labelHeight = titleLabel.prefHeight(-1);
final double labelWidth = titleLabel.prefWidth(labelHeight);
List<BorderStroke> newBorderStrokes = new ArrayList<>(2);
// create a line gap for the title label
for (BorderStroke bs : borderStrokes) {
List<Double> dashList = new ArrayList<>();
// Create a dash list for the line gap or add it at the beginning of an existing dash list. This gap should be wide enough for the title label.
if (bs.getTopStyle().getDashArray().isEmpty())
dashList.addAll(Arrays.asList(GAP_PADDING, labelWidth, Double.MAX_VALUE));
else { // dash pattern exists
// insert gap in existing dash pattern and multiply original pattern so that gap does not show more then once
double origDashWidth = bs.getTopStyle().getDashArray().stream().mapToDouble(d -> d).sum();
if (origDashWidth > GAP_PADDING) {
dashList.add(GAP_PADDING);
dashList.add(labelWidth);
} else { // need to insert dash pattern before the gap
int no = (int) (GAP_PADDING / origDashWidth);
for (int i = 0; i < no; i++)
dashList.addAll(bs.getTopStyle().getDashArray());
if ((dashList.size() & 1) == 0) // if size is even number, add one more element because gap must be at odd index to be transparent
dashList.add(0d);
dashList.add(labelWidth + GAP_PADDING - no * origDashWidth);
}
for (int i = 0; i < (width - labelWidth - origDashWidth) / origDashWidth; i++)
dashList.addAll(bs.getTopStyle().getDashArray());
}
// create new border stroke style for the top border line with new dash list
BorderStrokeStyle topStrokeStyle = new BorderStrokeStyle(
bs.getTopStyle().getType(), bs.getTopStyle().getLineJoin(), bs.getTopStyle().getLineCap(),
bs.getTopStyle().getMiterLimit(), bs.getTopStyle().getDashOffset(), dashList);
// change existing border stroke to utilize new top border line stroke style
newBorderStrokes.add(new BorderStroke(
bs.getTopStroke(), bs.getRightStroke(), bs.getBottomStroke(), bs.getLeftStroke(),
topStrokeStyle, bs.getRightStyle(), bs.getBottomStyle(), bs.getLeftStyle(),
bs.getRadii(), bs.getWidths(), null));
}
return new javafx.scene.layout.Border(newBorderStrokes.toArray(new BorderStroke[newBorderStrokes.size()]));
}
};
}
}
}