mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-22 07:30:24 +00:00
Improve file browser performance by caching icons
This commit is contained in:
parent
dbd0cb2d68
commit
405c024e9c
7 changed files with 155 additions and 71 deletions
|
@ -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,27 +325,34 @@ 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) {
|
||||
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 isDirectory = getTableRow().getItem().isDirectory();
|
||||
var box = new HBox(imageView, textField);
|
||||
box.setSpacing(10);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
|
@ -352,18 +362,20 @@ final class FileListComp extends AnchorPane {
|
|||
var isParentLink = getTableRow()
|
||||
.getItem()
|
||||
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
|
||||
img.set(FileIconManager.getFileIcon(isParentLink ? fileList.getFileSystemModel().getCurrentDirectory() : getTableRow().getItem(), isParentLink));
|
||||
img.set(FileIconManager.getFileIcon(
|
||||
isParentLink
|
||||
? fileList.getFileSystemModel().getCurrentDirectory()
|
||||
: getTableRow().getItem(),
|
||||
isParentLink));
|
||||
|
||||
var isDirectory = getTableRow().getItem().isDirectory();
|
||||
pseudoClassStateChanged(FOLDER, isDirectory);
|
||||
|
||||
var fileName = isParentLink
|
||||
? ".."
|
||||
: FileNames.getFileName(fullPath);
|
||||
var fileName = isParentLink ? ".." : FileNames.getFileName(fullPath);
|
||||
var hidden = !isParentLink && (getTableRow().getItem().isHidden() || fileName.startsWith("."));
|
||||
getTableRow().pseudoClassStateChanged(HIDDEN, hidden);
|
||||
text.set(fileName);
|
||||
|
||||
text.addListener(listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -6,5 +6,7 @@ import java.util.Optional;
|
|||
|
||||
public interface SvgCache {
|
||||
|
||||
void put(String image, Image value);
|
||||
|
||||
Optional<Image> getCached(String image);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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(
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue