VAqua.src.org.violetlib.aqua.fc.CatalinaFileIconServiceImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of vaqua Show documentation
Show all versions of vaqua Show documentation
An improved native Swing look and feel for macOS
The newest version!
/*
* Copyright (c) 2019-2021 Alan Snyder.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the license agreement. For details see
* accompanying license terms.
*/
package org.violetlib.aqua.fc;
import java.awt.*;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.violetlib.aqua.*;
/**
* An implementation of the file icon service that uses the Quick Look Thumbnailing framework, introduced in macOS
* 10.15 (Catalina).
*/
public class CatalinaFileIconServiceImpl
extends FileIconServiceImplBase
implements FileIconService {
private static final @NotNull ConcurrentDispatcher dispatcher = new ConcurrentDispatcher();
public CatalinaFileIconServiceImpl() {
if (debugFlag) {
Utils.logDebug("File Icon Service: Using Quick Look Thumbnailing");
}
}
@Override
public @NotNull Request requestIcon(@NotNull File f, int size, float scale, @NotNull Handler handler) {
// Finder specific issues:
// Custom icons are used for specific folders and for images. Other files use generic, type-specific icons.
// Except for image files, the Finder apparently uses Launch Services to get the icon.
// Launch Services returns type-specific icons for files that are clipped to a document shape and have the
// file type name superimposed.
// The icon for an image file is given a border, installed by Finder.
// The border is 2 points wide. The image is painted over a white background.
// TBD: the border is not implemented
RequestImpl request = new RequestImpl(f, handler, FileIconService.ICON_CUSTOM_LOW);
// Deliver a generic icon, if we can determine that the file is a folder or an ordinary file.
// This icon is a backup in case the other attempts fail.
installGenericFileIcon(f, request);
if (OSXFile.isImageFile(f)) {
long upcallID = upcallRegistry.registerRequest(request);
if (debugFlag) {
Utils.logDebug("Thumbnail request #" + upcallID + ": " + f.getAbsolutePath());
}
installQuickLookFileIcon(f, size, scale, upcallID);
} else {
dispatcher.dispatch(() -> {
installLaunchServicesFileIcon(f, size, scale, request, FileIconService.ICON_TYPE);
});
}
return request;
}
private void installLaunchServicesFileIcon(@NotNull File f, int size, float scale,
@NotNull RequestImpl request, int priority)
{
String path = f.getAbsolutePath();
int[][] buffers = new int[2][];
if (!AquaFileIcons.nativeRenderFileImage(path, false, true, buffers, size, size)) {
if (AquaImageFactory.debugNativeRendering) {
String type = "Launch Services";
Utils.logDebug("Failed to render " + type + " image for " + path);
}
} else {
if (AquaImageFactory.debugNativeRendering) {
String type = "Launch Services";
Utils.logDebug("Rendered " + type + " image for " + path);
}
Image image = AquaMultiResolutionImage.createImage(size, size, buffers[0], buffers[1]);
request.installImage(image, priority);
}
}
private void installQuickLookFileIcon(@NotNull File f, int size, float scale, long upcallID)
{
// Workaround: macOS 10.15 delivers an incorrect generic icon for a symlink.
// This workaround does not produce a correct generic icon for a broken symlink.
if (OSXFile.getFileType(f) == OSXFile.FILE_TYPE_ALIAS) {
File rf = OSXFile.resolve(f);
if (rf != null) {
f = rf;
}
}
nativeInstallThumbnails(f.getAbsolutePath(), size, scale, upcallID);
}
public static class MyHandler {
// called from native code
public void installImage(long upcallID, int width, int height, @NotNull int[] data, float scale, int priority) {
RequestImpl request = upcallRegistry.getRequest(upcallID);
if (request != null) {
if (debugFlag) {
Utils.logDebug("Received image " + priority + " for request #" + upcallID
+ width + "x" + height + " " + data.length + " " + scale);
}
AquaMultiResolutionImage image = JavaSupport.createImage(width, height, data, scale);
request.installImage(image, priority);
} else {
Utils.logDebug("Image delivered to obsolete request #" + upcallID);
}
}
}
private static final @NotNull MyHandler myHandlerInstance = new MyHandler();
static {
nativeInstallThumbnailHandler(myHandlerInstance);
}
private static final @NotNull ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(0);
private static class UpcallRegistry {
private final int delayMinutes = 5;
private final @NotNull Map map = new HashMap<>();
private long nextID = 0;
private @Nullable List requestsToRemove;
public synchronized long registerRequest(@NotNull RequestImpl request) {
long id = nextID++;
map.put(id, request);
if (requestsToRemove == null) {
requestsToRemove = new ArrayList<>(map.keySet());
scheduler.schedule(this::cleanup, delayMinutes, TimeUnit.MINUTES);
}
return id;
}
public synchronized @Nullable RequestImpl getRequest(long id) {
return map.get(id);
}
private synchronized void cleanup() {
assert requestsToRemove != null;
if (debugFlag) {
Utils.logDebug("Removing " + requestsToRemove.size() + " thumbnail requests");
}
for (Long id : requestsToRemove) {
map.remove(id);
}
if (!map.isEmpty()) {
requestsToRemove = new ArrayList<>(map.keySet());
scheduler.schedule(this::cleanup, delayMinutes, TimeUnit.MINUTES);
} else {
requestsToRemove = null;
}
}
}
private static @NotNull UpcallRegistry upcallRegistry = new UpcallRegistry();
public static native boolean isAvailable();
private static native void nativeInstallThumbnailHandler(@NotNull MyHandler handler);
private static native void nativeInstallThumbnails(@NotNull String filePath, int size, float scale, long upcallID);
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy