54
54
55
55
import org .junit .jupiter .api .AfterEach ;
56
56
import org .junit .jupiter .api .BeforeEach ;
57
+ import org .junit .jupiter .api .Nested ;
57
58
import org .junit .jupiter .api .Test ;
58
- import static org .junit .jupiter .api .Assertions .assertEquals ;
59
- import static org .junit .jupiter .api .Assertions .assertFalse ;
60
- import static org .junit .jupiter .api .Assertions .assertNull ;
61
- import static org .junit .jupiter .api .Assertions .assertSame ;
62
- import static org .junit .jupiter .api .Assertions .assertTrue ;
63
- import static org .junit .jupiter .api .Assertions .fail ;
59
+
60
+ import static org .junit .jupiter .api .Assertions .*;
64
61
65
62
public class FocusTest {
66
63
@@ -121,6 +118,12 @@ private void assertIsFocused(Node n) {
121
118
assertTrue (n .getPseudoClassStates ().stream ().anyMatch (pc -> pc .getPseudoClassName ().equals ("focused" )));
122
119
}
123
120
121
+ private void assertIsFocused (Node ... ns ) {
122
+ for (Node n : ns ) {
123
+ assertIsFocused (n );
124
+ }
125
+ }
126
+
124
127
private void assertIsFocused (Scene s , Node n ) {
125
128
assertEquals (n , s .getFocusOwner ());
126
129
assertIsFocused (n );
@@ -131,6 +134,12 @@ private void assertNotFocused(Node n) {
131
134
assertFalse (n .getPseudoClassStates ().stream ().anyMatch (pc -> pc .getPseudoClassName ().equals ("focused" )));
132
135
}
133
136
137
+ private void assertNotFocused (Node ... ns ) {
138
+ for (Node n : ns ) {
139
+ assertNotFocused (n );
140
+ }
141
+ }
142
+
134
143
private void assertNotFocused (Scene s , Node n ) {
135
144
assertTrue (n != s .getFocusOwner ());
136
145
assertNotFocused (n );
@@ -1161,4 +1170,284 @@ class N extends Group {
1161
1170
assertNotFocusWithin (node2 );
1162
1171
}
1163
1172
1173
+ @ Nested
1174
+ class FocusDelegateTest {
1175
+ static class N extends Group {
1176
+ final Node delegate ;
1177
+ final String name ;
1178
+
1179
+ N (String name , Node child , Node delegate ) {
1180
+ super (child == null ? new Node [0 ] : new Node [] { child });
1181
+ this .delegate = delegate ;
1182
+ this .name = name ;
1183
+ }
1184
+
1185
+ N (Node child , Node delegate ) {
1186
+ this (null , child , delegate );
1187
+ }
1188
+
1189
+ @ Override
1190
+ protected Node getFocusDelegate () {
1191
+ return delegate ;
1192
+ }
1193
+
1194
+ @ Override
1195
+ public String toString () {
1196
+ return name ;
1197
+ }
1198
+ }
1199
+
1200
+ static class Trace extends ArrayList <String > {
1201
+ Trace (Node ... nodes ) {
1202
+ for (Node node : nodes ) {
1203
+ node .addEventFilter (
1204
+ Event .ANY ,
1205
+ e -> add (String .format ("EventFilter[source=%s, target=%s]" , e .getSource (), e .getTarget ())));
1206
+
1207
+ node .addEventHandler (
1208
+ Event .ANY ,
1209
+ e -> add (String .format ("EventHandler[source=%s, target=%s]" , e .getSource (), e .getTarget ())));
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ /**
1215
+ * Focusing a node that delegates focus to a descendant will focus both the original node
1216
+ * and its descendant. This test asserts that this also works in nested scenarios, when the
1217
+ * delegation target delegates focus further down the scene graph.
1218
+ */
1219
+ @ Test
1220
+ void focusIsDelegatedToDescendants () {
1221
+ N node1 , node2 , node3 , node4 , node5 ;
1222
+
1223
+ // node1 . . . . . . . . . focusDelegate = node3
1224
+ // └── node2
1225
+ // └── node3 . . . . . focusDelegate = node5
1226
+ // └── node4
1227
+ // └── node5
1228
+ scene .setRoot (
1229
+ node1 = new N (
1230
+ node2 = new N (
1231
+ node3 = new N (
1232
+ node4 = new N (
1233
+ node5 = new N (null , null ), null
1234
+ ), node5
1235
+ ), null
1236
+ ), node3
1237
+ ));
1238
+
1239
+ assertNotFocused (node1 , node2 , node3 , node4 , node5 );
1240
+
1241
+ node1 .requestFocus ();
1242
+
1243
+ assertIsFocused (node1 , node3 , node5 );
1244
+ assertIsFocused (scene , node1 );
1245
+ }
1246
+
1247
+ /**
1248
+ * This test is similar to {@link #focusIsDelegatedToDescendants()}, except that instead of
1249
+ * focusing the root node, the intermediate focus delegate is focused. This will result in
1250
+ * the root node being unfocused, while the intermediate focus delegate and its delegation
1251
+ * target are focused.
1252
+ */
1253
+ @ Test
1254
+ void focusIsDelegatedToSubtreeDescendants () {
1255
+ N node1 , node2 , node3 , node4 , node5 ;
1256
+
1257
+ // node1 . . . . . . . . . focusDelegate = node3
1258
+ // └── node2
1259
+ // └── node3 . . . . . focusDelegate = node5
1260
+ // └── node4
1261
+ // └── node5
1262
+ scene .setRoot (
1263
+ node1 = new N (
1264
+ node2 = new N (
1265
+ node3 = new N (
1266
+ node4 = new N (
1267
+ node5 = new N (null , null ), null
1268
+ ), node5
1269
+ ), null
1270
+ ), node3
1271
+ ));
1272
+
1273
+ assertNotFocused (node1 , node2 , node3 , node4 , node5 );
1274
+
1275
+ node3 .requestFocus ();
1276
+
1277
+ assertIsFocused (node3 , node5 );
1278
+ assertIsFocused (scene , node3 );
1279
+ }
1280
+
1281
+ /**
1282
+ * When a scene sends a key event to the focus owner, it is delivered up to the final target
1283
+ * of delegation. The event target changes as the event travels through the scene graph:
1284
+ * First, the event is targeted at the focus owner. For each descendant node, the event is
1285
+ * targeted at the next focus delegate. When the capturing phase completes and the event starts
1286
+ * to bubble up, it is targeted back to its original targets as it crosses the boundary between
1287
+ * focus delegates.
1288
+ */
1289
+ @ Test
1290
+ void keyEventIsDeliveredToFocusedDescendants () {
1291
+ N node1 , node2 , node3 , node4 , node5 ;
1292
+
1293
+ // node1 . . . . . . . . . focusDelegate = node3, focusOwner
1294
+ // └── node2
1295
+ // └── node3 . . . . . focusDelegate = node5
1296
+ // └── node4
1297
+ // └── node5
1298
+ scene .setRoot (
1299
+ node1 = new N ("node1" ,
1300
+ node2 = new N ("node2" ,
1301
+ node3 = new N ("node3" ,
1302
+ node4 = new N ("node4" ,
1303
+ node5 = new N ("node5" ,null , null ), null
1304
+ ), node5
1305
+ ), null
1306
+ ), node3
1307
+ ));
1308
+
1309
+ node1 .requestFocus ();
1310
+
1311
+ var trace = new Trace (node1 , node2 , node3 , node4 , node5 );
1312
+
1313
+ SceneShim .processKeyEvent (
1314
+ scene , new KeyEvent (KeyEvent .KEY_PRESSED , null , null , KeyCode .A , false , false , false , false ));
1315
+
1316
+ assertEquals (
1317
+ List .of (
1318
+ "EventFilter[source=node1, target=node1]" , // <-- target is node1 (focusOwner)
1319
+ "EventFilter[source=node2, target=node3]" , // <-- change target to node3 (delegate)
1320
+ "EventFilter[source=node3, target=node3]" ,
1321
+ "EventFilter[source=node4, target=node5]" , // <-- change target to node5 (delegate)
1322
+ "EventFilter[source=node5, target=node5]" ,
1323
+ "EventHandler[source=node5, target=node5]" ,
1324
+ "EventHandler[source=node4, target=node5]" ,
1325
+ "EventHandler[source=node3, target=node3]" , // <-- change target back to node3
1326
+ "EventHandler[source=node2, target=node3]" ,
1327
+ "EventHandler[source=node1, target=node1]" // <-- change target back to node1
1328
+ ),
1329
+ trace
1330
+ );
1331
+ }
1332
+ }
1333
+
1334
+ @ Nested
1335
+ class FocusScopeTest {
1336
+ static class N extends Group {
1337
+ final boolean focusScope ;
1338
+
1339
+ N (boolean focusScope , boolean hoistFocus , Node child ) {
1340
+ super (child == null ? new Node [0 ] : new Node [] { child });
1341
+ this .focusScope = focusScope ;
1342
+ setHoistFocus (hoistFocus );
1343
+ }
1344
+
1345
+ @ Override
1346
+ protected boolean isFocusScope () {
1347
+ return focusScope ;
1348
+ }
1349
+ }
1350
+
1351
+ /**
1352
+ * A node with the {@link Node#hoistFocusProperty()} flag will hoist a focus request to the next
1353
+ * focus scope. The result is that the node that defines the focus scope is focused, and the node
1354
+ * that initially received the focus request is not.
1355
+ */
1356
+ @ Test
1357
+ void hoistFocusRequestToNextScope () {
1358
+ N node1 , node2 , node3 , node4 , node5 ;
1359
+
1360
+ // node1 . . . . . . . . . . [hoistFocus]
1361
+ // └── node2 . . . . . . . . [hoistFocus]
1362
+ // └── node3 . . . . . . [focusScope, hoistFocus]
1363
+ // └── node4 . . . . [hoistFocus]
1364
+ // └── node5 . . [hoistFocus]
1365
+ scene .setRoot (
1366
+ node1 = new N (false , false ,
1367
+ node2 = new N (false , true ,
1368
+ node3 = new N (true , true ,
1369
+ node4 = new N (false , true ,
1370
+ node5 = new N (false , true , null )
1371
+ )
1372
+ )
1373
+ )
1374
+ ));
1375
+
1376
+ node4 .requestFocus ();
1377
+ assertIsFocused (node3 );
1378
+ assertNotFocused (node1 , node2 , node4 , node5 );
1379
+
1380
+ node5 .requestFocus ();
1381
+ assertIsFocused (node3 );
1382
+ assertNotFocused (node1 , node2 , node4 , node5 );
1383
+ }
1384
+
1385
+ /**
1386
+ * A node with the {@link Node#hoistFocusProperty()} flag will hoist a focus request to the next
1387
+ * focus scope. In this test, the result is that the node that defines the outermost focus scope
1388
+ * is focused, and all other nodes are not focused (this includes the intermediate focus scope).
1389
+ */
1390
+ @ Test
1391
+ void hoistFocusRequestToOutermostScope () {
1392
+ N node1 , node2 , node3 , node4 , node5 ;
1393
+
1394
+ // node1 . . . . . . . . . . [focusScope]
1395
+ // └── node2 . . . . . . . . [hoistFocus]
1396
+ // └── node3 . . . . . . [focusScope, hoistFocus]
1397
+ // └── node4 . . . . [hoistFocus]
1398
+ // └── node5 . . [hoistFocus]
1399
+ scene .setRoot (
1400
+ node1 = new N (true , false ,
1401
+ node2 = new N (false , true ,
1402
+ node3 = new N (true , true ,
1403
+ node4 = new N (false , true ,
1404
+ node5 = new N (false , true , null )
1405
+ )
1406
+ )
1407
+ )
1408
+ ));
1409
+
1410
+ node4 .requestFocus ();
1411
+ assertIsFocused (node1 );
1412
+ assertNotFocused (node2 , node3 , node4 , node5 );
1413
+
1414
+ node5 .requestFocus ();
1415
+ assertIsFocused (node1 );
1416
+ assertNotFocused (node2 , node3 , node4 , node5 );
1417
+ }
1418
+
1419
+ /**
1420
+ * A node with the {@link Node#hoistFocusProperty()} flag will hoist a focus request to the next
1421
+ * focus scope. If the next focus scope does not have the {@link Node#hoistFocusProperty()} flag,
1422
+ * the focus request will not be hoisted again, even when an ancestor defines another focus scope.
1423
+ */
1424
+ @ Test
1425
+ void hoistFocusRequestStopsWhenHoistFocusIsFalse () {
1426
+ N node1 , node2 , node3 , node4 , node5 ;
1427
+
1428
+ // node1 . . . . . . . . . . [focusScope]
1429
+ // └── node2 . . . . . . . . [hoistFocus]
1430
+ // └── node3 . . . . . . [focusScope]
1431
+ // └── node4 . . . . [hoistFocus]
1432
+ // └── node5 . . [hoistFocus]
1433
+ scene .setRoot (
1434
+ node1 = new N (true , false ,
1435
+ node2 = new N (false , true ,
1436
+ node3 = new N (true , false ,
1437
+ node4 = new N (false , true ,
1438
+ node5 = new N (false , true , null )
1439
+ )
1440
+ )
1441
+ )
1442
+ ));
1443
+
1444
+ node4 .requestFocus ();
1445
+ assertIsFocused (node3 );
1446
+ assertNotFocused (node1 , node2 , node4 , node5 );
1447
+
1448
+ node5 .requestFocus ();
1449
+ assertIsFocused (node3 );
1450
+ assertNotFocused (node1 , node2 , node4 , node5 );
1451
+ }
1452
+ }
1164
1453
}
0 commit comments