@@ -52,7 +52,8 @@ public static void main(String[] args) throws Exception {
5252 error ("Output file %s already exists" .formatted (outputFile ));
5353 }
5454
55- LayoutInfo layoutInfo = LayoutInfo .parse (findLayoutFile (skinDirectory ));
55+ Path layoutFile = findLayoutFile (skinDirectory );
56+ LayoutInfo layoutInfo = LayoutInfo .parse (layoutFile , skinDirectory );
5657 HardwareInfo hardwareInfo = HardwareInfo .parse (skinDirectory .resolve ("hardware.ini" ));
5758
5859 if (!layoutInfo .hasBothOrientations ()) {
@@ -211,9 +212,9 @@ boolean hasBothOrientations() {
211212 return portrait != null && landscape != null ;
212213 }
213214
214- static LayoutInfo parse (Path layoutFile ) {
215+ static LayoutInfo parse (Path layoutFile , Path skinDirectory ) {
215216 try {
216- return new LayoutParser ().parse (Files .readString (layoutFile ));
217+ return new LayoutParser (layoutFile , skinDirectory ).parse (Files .readString (layoutFile ));
217218 } catch (IOException err ) {
218219 throw new UncheckedIOException ("Failed to read layout file " + layoutFile , err );
219220 }
@@ -223,6 +224,13 @@ static LayoutInfo parse(Path layoutFile) {
223224 private static class LayoutParser {
224225 private final EnumMap <OrientationType , OrientationInfoBuilder > builders = new EnumMap <>(OrientationType .class );
225226 private final Deque <Context > contextStack = new ArrayDeque <>();
227+ private final Path skinDirectory ;
228+ private final Path layoutParent ;
229+
230+ LayoutParser (Path layoutFile , Path skinDirectory ) {
231+ this .skinDirectory = skinDirectory ;
232+ this .layoutParent = layoutFile .getParent ();
233+ }
226234
227235 LayoutInfo parse (String text ) {
228236 String [] lines = text .split ("\r ?\n " );
@@ -263,10 +271,10 @@ private void handleKeyValue(String line) {
263271 return ;
264272 }
265273 String key = parts [0 ];
266- String value = parts [1 ];
274+ String value = unquote ( parts [1 ]) ;
267275 String ctxName = ctx .name .toLowerCase (Locale .ROOT );
268276 if (ctxName .contains ("image" ) && key .equalsIgnoreCase ("name" )) {
269- builder .imageName = value ;
277+ builder .considerImage ( value , contextStack , this :: resolveImagePath ) ;
270278 } else if (ctxName .contains ("display" )) {
271279 switch (key .toLowerCase (Locale .ROOT )) {
272280 case "x" -> builder .displayX = parseInt (value );
@@ -311,6 +319,17 @@ private String[] splitKeyValue(String line) {
311319 return parts ;
312320 }
313321
322+ private String unquote (String value ) {
323+ value = value .trim ();
324+ if (value .length () >= 2 && value .startsWith ("\" " ) && value .endsWith ("\" " )) {
325+ return value .substring (1 , value .length () - 1 );
326+ }
327+ if (value .length () >= 2 && value .startsWith ("'" ) && value .endsWith ("'" )) {
328+ return value .substring (1 , value .length () - 1 );
329+ }
330+ return value ;
331+ }
332+
314333 private int parseInt (String value ) {
315334 try {
316335 return Integer .parseInt (value );
@@ -319,6 +338,20 @@ private int parseInt(String value) {
319338 }
320339 }
321340
341+ private Path resolveImagePath (String name ) {
342+ Path candidate = skinDirectory .resolve (name ).normalize ();
343+ if (Files .isRegularFile (candidate )) {
344+ return candidate ;
345+ }
346+ if (layoutParent != null ) {
347+ Path sibling = layoutParent .resolve (name ).normalize ();
348+ if (Files .isRegularFile (sibling )) {
349+ return sibling ;
350+ }
351+ }
352+ return candidate ;
353+ }
354+
322355 private String stripComments (String line ) {
323356 int slash = line .indexOf ("//" );
324357 int hash = line .indexOf ('#' );
@@ -349,17 +382,82 @@ private OrientationType detectOrientation(String name) {
349382 private record Context (String name , OrientationType orientation ) {}
350383
351384 private static class OrientationInfoBuilder {
352- String imageName ;
385+ ImageCandidate selectedImage ;
353386 Integer displayX ;
354387 Integer displayY ;
355388 Integer displayWidth ;
356389 Integer displayHeight ;
357390
391+ void considerImage (String name , Deque <Context > contexts , java .util .function .Function <String , Path > resolver ) {
392+ ImageCandidate candidate = ImageCandidate .from (name , contexts , resolver );
393+ if (selectedImage == null || candidate .isBetterThan (selectedImage )) {
394+ selectedImage = candidate ;
395+ }
396+ }
397+
358398 OrientationInfo build (OrientationType type ) {
359- if (imageName == null || displayX == null || displayY == null || displayWidth == null || displayHeight == null ) {
399+ if (selectedImage == null || displayX == null || displayY == null || displayWidth == null || displayHeight == null ) {
360400 throw new IllegalStateException ("Layout definition for " + type + " is incomplete" );
361401 }
362- return new OrientationInfo (type , imageName , new DisplayArea (displayX , displayY , displayWidth , displayHeight ));
402+ return new OrientationInfo (type , selectedImage .name (), new DisplayArea (displayX , displayY , displayWidth , displayHeight ));
403+ }
404+ }
405+
406+ private record ImageCandidate (String name , long area , boolean frameHint , boolean controlHint ) {
407+ static ImageCandidate from (String name , Deque <Context > contexts , java .util .function .Function <String , Path > resolver ) {
408+ boolean frameHint = false ;
409+ boolean controlHint = false ;
410+ for (Context ctx : contexts ) {
411+ String lower = ctx .name .toLowerCase (Locale .ROOT );
412+ if (lower .contains ("button" ) || lower .contains ("control" ) || lower .contains ("icon" ) || lower .contains ("touch" )) {
413+ controlHint = true ;
414+ }
415+ if (lower .contains ("device" ) || lower .contains ("frame" ) || lower .contains ("skin" ) || lower .contains ("phone" ) || lower .contains ("tablet" )) {
416+ frameHint = true ;
417+ }
418+ }
419+ String lowerName = name .toLowerCase (Locale .ROOT );
420+ if (lowerName .contains ("frame" ) || lowerName .contains ("device" ) || lowerName .contains ("shell" ) || lowerName .contains ("body" )) {
421+ frameHint = true ;
422+ }
423+ if (lowerName .contains ("button" ) || lowerName .contains ("control" ) || lowerName .contains ("icon" )) {
424+ controlHint = true ;
425+ }
426+ long area = computeArea (resolver .apply (name ));
427+ return new ImageCandidate (name , area , frameHint , controlHint );
428+ }
429+
430+ private static long computeArea (Path imagePath ) {
431+ if (imagePath == null || !Files .isRegularFile (imagePath )) {
432+ return -1 ;
433+ }
434+ try {
435+ BufferedImage img = javax .imageio .ImageIO .read (imagePath .toFile ());
436+ if (img == null ) {
437+ return -1 ;
438+ }
439+ return (long ) img .getWidth () * (long ) img .getHeight ();
440+ } catch (IOException err ) {
441+ return -1 ;
442+ }
443+ }
444+
445+ boolean isBetterThan (ImageCandidate other ) {
446+ if (other == null ) {
447+ return true ;
448+ }
449+ if (frameHint != other .frameHint ) {
450+ return frameHint && !controlHint ;
451+ }
452+ if (controlHint != other .controlHint ) {
453+ return !controlHint ;
454+ }
455+ long thisArea = Math .max (area , 0 );
456+ long otherArea = Math .max (other .area , 0 );
457+ if (thisArea != otherArea ) {
458+ return thisArea > otherArea ;
459+ }
460+ return name .compareTo (other .name ) < 0 ;
363461 }
364462 }
365463 }
0 commit comments