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

View file

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

View file

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

View file

@ -6,5 +6,7 @@ import java.util.Optional;
public interface SvgCache { public interface SvgCache {
void put(String image, Image value);
Optional<Image> getCached(String image); 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.binding.Bindings;
import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.css.Size; import javafx.css.Size;
import javafx.css.SizeUnits; import javafx.css.SizeUnits;
import javafx.geometry.Point2D; import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.web.WebView; import javafx.scene.web.WebView;
@ -19,17 +17,19 @@ import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.Value; import lombok.Value;
import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@Getter @Getter
public class SvgComp { public class SvgView {
private final ObservableValue<Number> width; private final ObservableValue<Number> width;
private final ObservableValue<Number> height; private final ObservableValue<Number> height;
private final ObservableValue<String> svgContent; 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.width = PlatformThread.sync(width);
this.height = PlatformThread.sync(height); this.height = PlatformThread.sync(height);
this.svgContent = PlatformThread.sync(svgContent); this.svgContent = PlatformThread.sync(svgContent);
@ -48,7 +48,7 @@ public class SvgComp {
} }
@SneakyThrows @SneakyThrows
public static SvgComp create(ObservableValue<String> content) { public static SvgView create(ObservableValue<String> content) {
var widthProperty = new SimpleIntegerProperty(); var widthProperty = new SimpleIntegerProperty();
var heightProperty = new SimpleIntegerProperty(); var heightProperty = new SimpleIntegerProperty();
SimpleChangeListener.apply(content, val -> { SimpleChangeListener.apply(content, val -> {
@ -57,14 +57,10 @@ public class SvgComp {
} }
var dim = getDimensions(val); var dim = getDimensions(val);
if (dim == null) {
return;
}
widthProperty.set((int) Math.ceil(dim.getX())); widthProperty.set((int) Math.ceil(dim.getX()));
heightProperty.set((int) Math.ceil(dim.getY())); 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) { private static Point2D getDimensions(String val) {
@ -76,7 +72,9 @@ public class SvgComp {
"<svg.+?viewBox=\"([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\"", Pattern.DOTALL); "<svg.+?viewBox=\"([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\\s+([\\d.]+)\"", Pattern.DOTALL);
matcher = viewBox.matcher(val); matcher = viewBox.matcher(val);
if (matcher.find()) { 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() { private WebView createWebView() {
var wv = new WebView(); var wv = new WebView();
wv.setPageFill(Color.TRANSPARENT); wv.setPageFill(Color.TRANSPARENT);
wv.setDisable(true);
wv.getEngine().setJavaScriptEnabled(false); wv.getEngine().setJavaScriptEnabled(false);
wv.getEngine().loadContent(getHtml(svgContent.getValue())); wv.getEngine().loadContent(getHtml(svgContent.getValue()));
@ -108,14 +105,6 @@ public class SvgComp {
wv.getEngine().loadContent(getHtml(n)); 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 // As the aspect ratio of the WebView is kept constant, we can compute the zoom only using the width
wv.zoomProperty() wv.zoomProperty()
.bind(Bindings.createDoubleBinding( .bind(Bindings.createDoubleBinding(

View file

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