Improve file browser performance by caching icons

This commit is contained in:
crschnick 2023-04-27 19:37:54 +00:00
parent dbd0cb2d68
commit 405c024e9c
7 changed files with 155 additions and 71 deletions

View file

@ -6,9 +6,10 @@ import atlantafx.base.theme.Styles;
import atlantafx.base.theme.Tweaks;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.fxcomps.impl.PrettyImageComp;
import io.xpipe.app.fxcomps.impl.SvgCacheComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.BusyProperty;
import io.xpipe.app.util.Containers;
import io.xpipe.app.util.HumanReadableFormat;
import io.xpipe.core.impl.FileNames;
@ -309,11 +310,13 @@ final class FileListComp extends AnchorPane {
private final StringProperty img = new SimpleStringProperty();
private final StringProperty text = new SimpleStringProperty();
private final Node imageView = new PrettyImageComp(img, 24, 24).createRegion();
private final Node imageView = new SvgCacheComp(new SimpleDoubleProperty(24), new SimpleDoubleProperty(24), img, FileIconManager.getSvgCache()).createRegion();
private final StackPane textField =
new LazyTextFieldComp(text).createStructure().get();
private final ChangeListener<String> listener;
private final BooleanProperty updating = new SimpleBooleanProperty();
public FilenameCell(Property<FileSystem.FileEntry> editing) {
editing.addListener((observable, oldValue, newValue) -> {
if (getTableRow().getItem() != null && getTableRow().getItem().equals(newValue)) {
@ -322,48 +325,57 @@ final class FileListComp extends AnchorPane {
});
listener = (observable, oldValue, newValue) -> {
if (updating.get()) {
return;
}
fileList.rename(oldValue, newValue);
textField.getScene().getRoot().requestFocus();
editing.setValue(null);
updateItem(getItem(), isEmpty());
};
text.addListener(listener);
}
@Override
protected void updateItem(String fullPath, boolean empty) {
super.updateItem(fullPath, empty);
if (updating.get()) {
super.updateItem(fullPath, empty);
return;
}
text.removeListener(listener);
text.setValue(fullPath);
try (var b = new BusyProperty(updating)) {
super.updateItem(fullPath, empty);
setText(null);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
// Don't set image as that would trigger image comp update
// and cells are emptied on each change, leading to unnecessary changes
// img.set(null);
setGraphic(null);
} else {
var box = new HBox(imageView, textField);
box.setSpacing(10);
box.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(textField, Priority.ALWAYS);
setGraphic(box);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
// Don't set image as that would trigger image comp update
// and cells are emptied on each change, leading to unnecessary changes
// img.set(null);
setGraphic(null);
} else {
var isDirectory = getTableRow().getItem().isDirectory();
var box = new HBox(imageView, textField);
box.setSpacing(10);
box.setAlignment(Pos.CENTER_LEFT);
HBox.setHgrow(textField, Priority.ALWAYS);
setGraphic(box);
var isParentLink = getTableRow()
.getItem()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
img.set(FileIconManager.getFileIcon(
isParentLink
? fileList.getFileSystemModel().getCurrentDirectory()
: getTableRow().getItem(),
isParentLink));
var isParentLink = getTableRow()
.getItem()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
img.set(FileIconManager.getFileIcon(isParentLink ? fileList.getFileSystemModel().getCurrentDirectory() : getTableRow().getItem(), isParentLink));
var isDirectory = getTableRow().getItem().isDirectory();
pseudoClassStateChanged(FOLDER, isDirectory);
pseudoClassStateChanged(FOLDER, isDirectory);
var fileName = isParentLink
? ".."
: FileNames.getFileName(fullPath);
var hidden = !isParentLink && (getTableRow().getItem().isHidden() || fileName.startsWith("."));
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName);
text.addListener(listener);
var fileName = isParentLink ? ".." : FileNames.getFileName(fullPath);
var hidden = !isParentLink && (getTableRow().getItem().isHidden() || fileName.startsWith("."));
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
text.set(fileName);
}
}
}
}

View file

@ -5,6 +5,7 @@ import io.xpipe.app.core.AppResources;
import io.xpipe.app.fxcomps.impl.SvgCache;
import io.xpipe.core.store.FileSystem;
import javafx.scene.image.Image;
import lombok.Getter;
import java.io.BufferedReader;
import java.io.InputStreamReader;
@ -16,7 +17,8 @@ public class FileIconManager {
private static final List<FileIconFactory> factories = new ArrayList<>();
private static final List<FolderIconFactory> folderFactories = new ArrayList<>();
private static SvgCache svgCache;
@Getter
private static SvgCache svgCache = createCache();
private static boolean loaded;
private static void loadDefinitions() {
@ -88,20 +90,19 @@ public class FileIconManager {
});
}
private static void createCache() {
svgCache = new SvgCache() {
private static SvgCache createCache() {
return new SvgCache() {
private final Map<String, Integer> hits = new HashMap<>();
private final Map<String, Image> images = new HashMap<>();
@Override
public Optional<Image> getCached(String image) {
var hitCount = hits.computeIfAbsent(image, s -> 1);
if (hitCount > 5) {
//images.computeIfAbsent(image, s -> AppImages.image())
}
public synchronized void put(String image, Image value) {
images.put(image, value);
}
return Optional.empty();
@Override
public synchronized Optional<Image> getCached(String image) {
return Optional.ofNullable(images.get(image));
}
};
}
@ -140,11 +141,6 @@ public class FileIconManager {
return entry.isDirectory() ? (open ? "default_folder_opened.svg" : "default_folder.svg") : "default_file.svg";
}
public static String getParentLinkIcon() {
loadIfNecessary();
return "default_folder_opened.svg";
}
private static String getIconPath(String name) {
return name;
}

View file

@ -69,7 +69,7 @@ public class PrettyImageComp extends SimpleComp {
}
else if (val.endsWith(".svg")) {
var storeIcon = SvgComp.create(
var storeIcon = SvgView.create(
Bindings.createStringBinding(() -> {
if (!AppImages.hasSvgImage(image.getValue())) {
return null;

View file

@ -6,5 +6,7 @@ import java.util.Optional;
public interface SvgCache {
void put(String image, Image value);
Optional<Image> getCached(String image);
}

View file

@ -0,0 +1,85 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.animation.PauseTransition;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
public class SvgCacheComp extends SimpleComp {
private final ObservableValue<Number> width;
private final ObservableValue<Number> height;
private final ObservableValue<String> svgFile;
private final SvgCache cache;
public SvgCacheComp(
ObservableValue<Number> width,
ObservableValue<Number> height,
ObservableValue<String> svgFile,
SvgCache cache) {
this.width = PlatformThread.sync(width);
this.height = PlatformThread.sync(height);
this.svgFile = PlatformThread.sync(svgFile);
this.cache = cache;
}
@Override
protected Region createSimple() {
var frontContent = new SimpleObjectProperty<Image>();
var front = new ImageView();
front.fitWidthProperty().bind(width);
front.fitHeightProperty().bind(height);
front.setSmooth(true);
frontContent.addListener((observable, oldValue, newValue) -> {
front.setImage(newValue);
});
var webViewContent = new SimpleStringProperty();
var back = SvgView.create(webViewContent).createWebview();
svgFile.addListener((observable, oldValue, newValue) -> {
var pt = new PauseTransition();
pt.setDuration(Duration.millis(1000));
pt.setOnFinished(actionEvent -> {
if (newValue == null || cache.getCached(newValue).isPresent()) {
return;
}
if (!newValue.equals(svgFile.getValue())) {
return;
}
WritableImage image = back.snapshot(null, null);
if (image.getWidth() < 10) {
return;
}
cache.put(newValue, image);
});
pt.play();
});
back.prefWidthProperty().bind(width);
back.prefHeightProperty().bind(height);
svgFile.addListener((observable, oldValue, newValue) -> {
var cached = cache.getCached(newValue);
webViewContent.setValue(newValue != null || cached.isEmpty() ? AppImages.svgImage(newValue) : null);
frontContent.setValue(cached.orElse(null));
back.setVisible(cached.isEmpty());
front.setVisible(cached.isPresent());
});
var stack = new StackPane(back, front);
stack.prefWidthProperty().bind(width);
stack.prefHeightProperty().bind(height);
return stack;
}
}

View file

@ -6,11 +6,9 @@ import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.web.WebView;
@ -19,17 +17,19 @@ import lombok.Getter;
import lombok.SneakyThrows;
import lombok.Value;
import java.util.Set;
import java.util.regex.Pattern;
@Getter
public class SvgComp {
public class SvgView {
private final ObservableValue<Number> width;
private final ObservableValue<Number> height;
private final ObservableValue<String> svgContent;
public SvgComp(ObservableValue<Number> width, ObservableValue<Number> height, ObservableValue<String> svgContent) {
private SvgView(
ObservableValue<Number> width,
ObservableValue<Number> height,
ObservableValue<String> svgContent) {
this.width = PlatformThread.sync(width);
this.height = PlatformThread.sync(height);
this.svgContent = PlatformThread.sync(svgContent);
@ -48,7 +48,7 @@ public class SvgComp {
}
@SneakyThrows
public static SvgComp create(ObservableValue<String> content) {
public static SvgView create(ObservableValue<String> content) {
var widthProperty = new SimpleIntegerProperty();
var heightProperty = new SimpleIntegerProperty();
SimpleChangeListener.apply(content, val -> {
@ -57,14 +57,10 @@ public class SvgComp {
}
var dim = getDimensions(val);
if (dim == null) {
return;
}
widthProperty.set((int) Math.ceil(dim.getX()));
heightProperty.set((int) Math.ceil(dim.getY()));
});
return new SvgComp(widthProperty, heightProperty, content);
return new SvgView(widthProperty, heightProperty, content);
}
private static Point2D getDimensions(String val) {
@ -76,7 +72,9 @@ public class SvgComp {
"<svg.+?viewBox=\"([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\"", Pattern.DOTALL);
matcher = viewBox.matcher(val);
if (matcher.find()) {
return new Point2D(parseSize(matcher.group(3)).pixels(), parseSize(matcher.group(4)).pixels());
return new Point2D(
parseSize(matcher.group(3)).pixels(),
parseSize(matcher.group(4)).pixels());
}
}
@ -95,7 +93,6 @@ public class SvgComp {
private WebView createWebView() {
var wv = new WebView();
wv.setPageFill(Color.TRANSPARENT);
wv.setDisable(true);
wv.getEngine().setJavaScriptEnabled(false);
wv.getEngine().loadContent(getHtml(svgContent.getValue()));
@ -108,14 +105,6 @@ public class SvgComp {
wv.getEngine().loadContent(getHtml(n));
});
// Hide scrollbars that popup on every content change. Bug in WebView?
wv.getChildrenUnmodifiable().addListener((ListChangeListener<Node>) change -> {
Set<Node> scrolls = wv.lookupAll(".scroll-bar");
for (Node scroll : scrolls) {
scroll.setVisible(false);
}
});
// As the aspect ratio of the WebView is kept constant, we can compute the zoom only using the width
wv.zoomProperty()
.bind(Bindings.createDoubleBinding(

View file

@ -14,9 +14,9 @@ public class ProcessOutputException extends Exception {
public static ProcessOutputException of(int exitCode, String output, String accumulatedError) {
var combinedError = (accumulatedError != null ? accumulatedError.trim() + "\n" : "") + (output != null ? output.trim() : "");
var message = switch (exitCode) {
case CommandControl.KILLED_EXIT_CODE -> "Process timed out" + combinedError;
case CommandControl.TIMEOUT_EXIT_CODE -> "Process timed out" + combinedError;
default -> "Process returned with exit code " + combinedError;
case CommandControl.KILLED_EXIT_CODE -> "Process timed out (exit code " + exitCode + ") " + combinedError;
case CommandControl.TIMEOUT_EXIT_CODE -> "Process timed out (exit code " + exitCode + ") " + combinedError;
default -> "Process returned with exit code " + exitCode + ": " + combinedError;
};
return new ProcessOutputException(message, exitCode, combinedError);
}