From 355de8e2fcc4b9d25c82d7d945acb1528ab58609 Mon Sep 17 00:00:00 2001 From: crschnick Date: Tue, 14 Mar 2023 22:02:40 +0000 Subject: [PATCH] File browser improvements --- app/build.gradle | 2 +- .../app/browser/FileBrowserClipboard.java | 9 +- .../io/xpipe/app/browser/FileContextMenu.java | 6 +- .../io/xpipe/app/browser/FileListComp.java | 163 ++++++----------- .../io/xpipe/app/browser/FileListEntry.java | 164 ++++++++++++++++++ .../io/xpipe/app/browser/FileListModel.java | 33 ++-- .../xpipe/app/prefs/ExternalTerminalType.java | 1 + .../io/xpipe/app/resources/style/browser.css | 13 +- .../style/storage-group-list-comp.css | 1 - 9 files changed, 256 insertions(+), 136 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/browser/FileListEntry.java diff --git a/app/build.gradle b/app/build.gradle index ed3fc133..f475d74a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -136,7 +136,7 @@ run { systemProperty 'io.xpipe.app.writeLogs', "true" systemProperty 'io.xpipe.app.writeSysOut', "true" systemProperty 'io.xpipe.app.developerMode', "true" - systemProperty 'io.xpipe.app.logLevel', "trace" + systemProperty 'io.xpipe.app.logLevel', "debug" systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion // systemProperty "io.xpipe.beacon.port", "21724" // systemProperty "io.xpipe.beacon.printMessages", "true" diff --git a/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java index 9b0f4ec9..9abcc928 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/FileBrowserClipboard.java @@ -15,6 +15,7 @@ public class FileBrowserClipboard { @Value public static class Instance { UUID uuid; + FileSystem.FileEntry baseDirectory; List entries; } @@ -22,18 +23,18 @@ public class FileBrowserClipboard { public static Instance currentDragClipboard; @SneakyThrows - public static ClipboardContent startDrag(List selected) { + public static ClipboardContent startDrag(FileSystem.FileEntry base, List selected) { var content = new ClipboardContent(); var idea = UUID.randomUUID(); - currentDragClipboard = new Instance(idea, selected); + currentDragClipboard = new Instance(idea, base, new ArrayList<>(selected)); content.putString(idea.toString()); return content; } @SneakyThrows - public static void startCopy(List selected) { + public static void startCopy(FileSystem.FileEntry base, List selected) { var id = UUID.randomUUID(); - currentCopyClipboard = new Instance(id, new ArrayList<>(selected)); + currentCopyClipboard = new Instance(id, base, new ArrayList<>(selected)); } public static Instance retrieveCopy() { diff --git a/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java b/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java index 964142e4..54cb21e3 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java +++ b/app/src/main/java/io/xpipe/app/browser/FileContextMenu.java @@ -55,9 +55,9 @@ final class FileContextMenu extends ContextMenu { private final OpenFileSystemModel model; private final FileSystem.FileEntry entry; - private final Property editing; + private final Property editing; - public FileContextMenu(OpenFileSystemModel model, FileSystem.FileEntry entry, Property editing) { + public FileContextMenu(OpenFileSystemModel model, FileSystem.FileEntry entry, Property editing) { super(); this.model = model; this.entry = entry; @@ -154,7 +154,7 @@ final class FileContextMenu extends ContextMenu { var rename = new MenuItem("Rename"); rename.setOnAction(event -> { event.consume(); - editing.setValue(entry.getPath()); + editing.setValue(entry); }); getItems().addAll(new SeparatorMenuItem(), rename, delete); diff --git a/app/src/main/java/io/xpipe/app/browser/FileListComp.java b/app/src/main/java/io/xpipe/app/browser/FileListComp.java index db1ba04c..f9f6585a 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileListComp.java +++ b/app/src/main/java/io/xpipe/app/browser/FileListComp.java @@ -5,7 +5,6 @@ package io.xpipe.app.browser; import atlantafx.base.theme.Styles; import atlantafx.base.theme.Tweaks; import io.xpipe.app.comp.base.LazyTextFieldComp; -import io.xpipe.app.core.AppResources; import io.xpipe.app.fxcomps.impl.PrettyImageComp; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.SimpleChangeListener; @@ -13,6 +12,7 @@ import io.xpipe.app.util.Containers; import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileSystem; +import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ChangeListener; import javafx.collections.ListChangeListener; @@ -21,11 +21,11 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.*; -import javafx.scene.image.Image; -import javafx.scene.input.*; +import javafx.scene.input.KeyCode; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.*; -import java.io.File; import java.time.Instant; import java.time.ZoneId; import java.util.Comparator; @@ -39,6 +39,8 @@ final class FileListComp extends AnchorPane { private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden"); private static final PseudoClass FOLDER = PseudoClass.getPseudoClass("folder"); private static final PseudoClass DRAG = PseudoClass.getPseudoClass("drag"); + private static final PseudoClass DRAG_OVER = PseudoClass.getPseudoClass("drag-over"); + private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current"); private static final String UNKNOWN = "unknown"; private final FileListModel fileList; @@ -57,7 +59,7 @@ final class FileListComp extends AnchorPane { @SuppressWarnings("unchecked") private TableView createTable() { - var editing = new SimpleObjectProperty(); + var editing = new SimpleObjectProperty(); var filenameCol = new TableColumn("Name"); filenameCol.setCellValueFactory(param -> new SimpleStringProperty( param.getValue() != null @@ -96,16 +98,15 @@ final class FileListComp extends AnchorPane { } table.getSelectionModel().getSelectedItems().addListener((ListChangeListener) c -> { - fileList.getModel().getBrowserModel().getSelectedFiles().setAll(c.getList()); + fileList.getSelected().setAll(c.getList()); + fileList.getFileSystemModel().getBrowserModel().getSelectedFiles().setAll(c.getList()); }); - var draggedOverDirectory = new SimpleBooleanProperty(); - table.setOnKeyPressed(event -> { if (event.isControlDown() && event.getCode().equals(KeyCode.C) && table.getSelectionModel().getSelectedItems().size() > 0) { - FileBrowserClipboard.startCopy(table.getSelectionModel().getSelectedItems()); + FileBrowserClipboard.startCopy(fileList.getFileSystemModel().getCurrentDirectory(), table.getSelectionModel().getSelectedItems()); event.consume(); } @@ -113,15 +114,38 @@ final class FileListComp extends AnchorPane { var clipboard = FileBrowserClipboard.retrieveCopy(); if (clipboard != null) { var files = clipboard.getEntries(); - var target = fileList.getModel().getCurrentDirectory(); - fileList.getModel().dropFilesIntoAsync(target, files, true); + var target = fileList.getFileSystemModel().getCurrentDirectory(); + fileList.getFileSystemModel().dropFilesIntoAsync(target, files, true); event.consume(); } } }); + var emptyEntry = new FileListEntry(table, null, fileList); + table.setOnDragOver(event -> { + emptyEntry.onDragOver(event); + }); + table.setOnDragEntered(event -> { + emptyEntry.onDragEntered(event); + }); + table.setOnDragDetected(event -> { + emptyEntry.startDrag(event); + }); + table.setOnDragExited(event -> { + emptyEntry.onDragExited(event); + }); + table.setOnDragDropped(event -> { + emptyEntry.onDragDrop(event); + }); + table.setRowFactory(param -> { TableRow row = new TableRow<>(); + var listEntry = Bindings.createObjectBinding(() -> new FileListEntry(row, row.getItem(), fileList), row.itemProperty()); + + row.itemProperty().addListener((observable, oldValue, newValue) -> { + row.pseudoClassStateChanged(DRAG, false); + row.pseudoClassStateChanged(DRAG_OVER, false); + }); row.addEventHandler(MouseEvent.MOUSE_CLICKED, t -> { t.consume(); @@ -129,7 +153,7 @@ final class FileListComp extends AnchorPane { return; } - var cm = new FileContextMenu(fileList.getModel(), row.getItem(), editing); + var cm = new FileContextMenu(fileList.getFileSystemModel(), row.getItem(), editing); if (t.getButton() == MouseButton.SECONDARY) { cm.show(row, t.getScreenX(), t.getScreenY()); } @@ -141,113 +165,28 @@ final class FileListComp extends AnchorPane { } }); - draggedOverDirectory.addListener((observable, oldValue, newValue) -> { - row.pseudoClassStateChanged(DRAG, newValue); + fileList.getDraggedOverDirectory().addListener((observable, oldValue, newValue) -> { + row.pseudoClassStateChanged(DRAG_OVER, newValue != null && newValue == row.getItem()); }); + fileList.getDraggedOverEmpty().addListener((observable, oldValue, newValue) -> { + table.pseudoClassStateChanged(DRAG_INTO_CURRENT, newValue); + }); + + row.setOnDragEntered(event -> { + listEntry.get().onDragEntered(event); + }); row.setOnDragOver(event -> { - if (row.equals(event.getGestureSource())) { - return; - } - - if (row.getItem() == null || !row.getItem().isDirectory()) { - draggedOverDirectory.set(true); - } else { - row.pseudoClassStateChanged(DRAG, true); - } - event.acceptTransferModes(TransferMode.ANY); - event.consume(); + listEntry.get().onDragOver(event); }); - row.setOnDragDetected(event -> { - if (row.isEmpty()) { - return; - } - - var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "img/file_drag_icon.png") - .orElseThrow(); - var image = new Image(url.toString(), 80, 80, true, false); - - var selected = table.getSelectionModel().getSelectedItems(); - Dragboard db = row.startDragAndDrop(TransferMode.COPY); - db.setContent(FileBrowserClipboard.startDrag(selected)); - db.setDragView(image, 30, 60); - event.setDragDetect(true); - event.consume(); + listEntry.get().startDrag(event); }); - row.setOnDragExited(event -> { - if (row.getItem() != null && row.getItem().isDirectory()) { - row.pseudoClassStateChanged(DRAG, false); - } else { - draggedOverDirectory.set(false); - } - - if (event.getGestureSource() == null && event.getDragboard().hasFiles()) { - } - - // if (event.getGestureSource() != null) { - // try { - // var f = Files.createTempFile(null, null); - // var cc = new ClipboardContent(); - // cc.putFiles(List.of(f.toFile())); - // Dragboard db = row.startDragAndDrop(TransferMode.COPY); - // db.setContent(cc); - // } catch (IOException e) { - // throw new RuntimeException(e); - // } - // } + listEntry.get().onDragExited(event); }); - // - // row.setEventDispatcher((event, chain) -> { - // if (event.getEventType().getName().equals("MOUSE_DRAGGED")) { - // MouseEvent drag = (MouseEvent) event; - // - // if (drag.isDragDetect()) { - // return chain.dispatchEvent(event); - // } - // - // Rectangle area = new Rectangle( - // App.getApp().getStage().getX(), - // App.getApp().getStage().getY(), - // App.getApp().getStage().getWidth(), - // App.getApp().getStage().getHeight() - // ); - // if (!area.intersects(drag.getScreenX(), drag.getScreenY(), 20, 20)) { - // System.out.println("->Drag down"); - // drag.setDragDetect(true); - // } - // } - // - // return chain.dispatchEvent(event); - // }); - row.setOnDragDropped(event -> { - draggedOverDirectory.set(false); - - // Accept drops from outside the app window - if (event.getGestureSource() == null && event.getDragboard().hasFiles()) { - Dragboard db = event.getDragboard(); - var list = db.getFiles().stream().map(File::toPath).toList(); - var target = row.getItem() != null && row.getItem().isDirectory() - ? row.getItem() - : fileList.getModel().getCurrentDirectory(); - fileList.getModel().dropLocalFilesIntoAsync(target, list); - event.setDropCompleted(true); - event.consume(); - } - - // Accept drops from inside the app window - if (event.getGestureSource() != null) { - var files = FileBrowserClipboard.retrieveDrag(event.getDragboard()) - .getEntries(); - var target = row.getItem() != null - ? row.getItem() - : fileList.getModel().getCurrentDirectory(); - fileList.getModel().dropFilesIntoAsync(target, files, false); - event.setDropCompleted(true); - event.consume(); - } + listEntry.get().onDragDrop(event); }); return row; @@ -276,10 +215,10 @@ final class FileListComp extends AnchorPane { new LazyTextFieldComp(text).createStructure().get(); private final ChangeListener listener; - public FilenameCell(Property editing) { + public FilenameCell(Property editing) { editing.addListener((observable, oldValue, newValue) -> { if (getTableRow().getItem() != null - && getTableRow().getItem().getPath().equals(newValue)) { + && getTableRow().getItem().equals(newValue)) { textField.requestFocus(); } }); diff --git a/app/src/main/java/io/xpipe/app/browser/FileListEntry.java b/app/src/main/java/io/xpipe/app/browser/FileListEntry.java new file mode 100644 index 00000000..cc47740c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/browser/FileListEntry.java @@ -0,0 +1,164 @@ +package io.xpipe.app.browser; + +import io.xpipe.app.core.AppResources; +import io.xpipe.core.store.FileSystem; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.input.DragEvent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.MouseEvent; +import javafx.scene.input.TransferMode; +import lombok.Getter; + +import java.io.File; +import java.util.Timer; +import java.util.TimerTask; + +@Getter +public class FileListEntry { + + public static final Timer DROP_TIMER = new Timer("dnd", true); + + private final Node row; + private final FileSystem.FileEntry item; + private final FileListModel model; + + private Point2D lastOver = new Point2D(-1, -1); + private TimerTask activeTask; + + public FileListEntry(Node row, FileSystem.FileEntry item, FileListModel model) { + this.row = row; + this.item = item; + this.model = model; + } + + private boolean acceptsDrop(DragEvent event) { + if (FileBrowserClipboard.currentDragClipboard == null) { + return false; + } + + // Prevent drag and drops of files into the current directory + if (FileBrowserClipboard.currentDragClipboard + .getBaseDirectory().getPath() + .equals(model.getFileSystemModel().getCurrentDirectory().getPath()) && (item == null || !item.isDirectory())) { + return false; + } + + // Prevent dropping items onto themselves + if (item != null && FileBrowserClipboard.currentDragClipboard.getEntries().contains(item)) { + return false; + } + + return true; + } + + public void onDragDrop(DragEvent event) { + model.getDraggedOverEmpty().setValue(false); + model.getDraggedOverDirectory().setValue(null); + + // Accept drops from outside the app window + if (event.getGestureSource() == null && event.getDragboard().hasFiles()) { + Dragboard db = event.getDragboard(); + var list = db.getFiles().stream().map(File::toPath).toList(); + var target = item != null && item.isDirectory() + ? item + : model.getFileSystemModel().getCurrentDirectory(); + model.getFileSystemModel().dropLocalFilesIntoAsync(target, list); + event.setDropCompleted(true); + event.consume(); + } + + // Accept drops from inside the app window + if (event.getGestureSource() != null) { + var files = FileBrowserClipboard.retrieveDrag(event.getDragboard()).getEntries(); + var target = item != null + ? item + : model.getFileSystemModel().getCurrentDirectory(); + model.getFileSystemModel().dropFilesIntoAsync(target, files, false); + event.setDropCompleted(true); + event.consume(); + } + } + + public void onDragExited(DragEvent event) { + if (item != null && item.isDirectory()) { + model.getDraggedOverDirectory().setValue(null); + } else { + model.getDraggedOverEmpty().setValue(false); + } + event.consume(); + } + + public void startDrag(MouseEvent event) { + if (item == null) { + return; + } + + var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, "img/file_drag_icon.png") + .orElseThrow(); + var image = new Image(url.toString(), 80, 80, true, false); + var selected = model.getSelected(); + Dragboard db = row.startDragAndDrop(TransferMode.COPY); + db.setContent(FileBrowserClipboard.startDrag(model.getFileSystemModel().getCurrentDirectory(), selected)); + db.setDragView(image, 30, 60); + event.setDragDetect(true); + event.consume(); + } + + private void acceptDrag(DragEvent event) { + if (item == null || !item.isDirectory()) { + model.getDraggedOverEmpty().setValue(true); + } else { + model.getDraggedOverDirectory().setValue(item); + } + event.acceptTransferModes(TransferMode.COPY_OR_MOVE); + } + + private void handleHoverTimer(DragEvent event) { + + if (item == null || !item.isDirectory()) { + return; + } + + if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) { + return; + } + + lastOver = (new Point2D(event.getX(), event.getY())); + activeTask = new TimerTask() { + @Override + public void run() { + if (activeTask != this) { + return; + } + + if (item != model.getDraggedOverDirectory().getValue()) { + return; + } + + model.getFileSystemModel().cd(item.getPath()); + } + }; + DROP_TIMER.schedule(activeTask, 1000); + } + + public void onDragEntered(DragEvent event) { + event.consume(); + if (!acceptsDrop(event)) { + return; + } + + acceptDrag(event); + } + + public void onDragOver(DragEvent event) { + event.consume(); + if (!acceptsDrop(event)) { + return; + } + + acceptDrag(event); + handleHoverTimer(event); + } +} diff --git a/app/src/main/java/io/xpipe/app/browser/FileListModel.java b/app/src/main/java/io/xpipe/app/browser/FileListModel.java index 8489d350..2c0ec4d0 100644 --- a/app/src/main/java/io/xpipe/app/browser/FileListModel.java +++ b/app/src/main/java/io/xpipe/app/browser/FileListModel.java @@ -8,7 +8,10 @@ import io.xpipe.core.impl.FileNames; import io.xpipe.core.store.FileSystem; import javafx.beans.property.ObjectProperty; import javafx.beans.property.Property; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import lombok.Getter; import java.util.ArrayList; @@ -25,24 +28,28 @@ final class FileListModel { static final Predicate PREDICATE_ANY = path -> true; static final Predicate PREDICATE_NOT_HIDDEN = path -> true; - private final OpenFileSystemModel model; + private final OpenFileSystemModel fileSystemModel; private final Property> comparatorProperty = new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR); private final Property> all = new SimpleObjectProperty<>(List.of()); private final Property> shown = new SimpleObjectProperty<>(List.of()); private final ObjectProperty> predicateProperty = new SimpleObjectProperty<>(path -> true); + private final ObservableList selected = FXCollections.observableArrayList(); - public FileListModel(OpenFileSystemModel model) { - this.model = model; + private final Property draggedOverDirectory = new SimpleObjectProperty(); + private final Property draggedOverEmpty = new SimpleBooleanProperty(); - model.getFilter().addListener((observable, oldValue, newValue) -> { + public FileListModel(OpenFileSystemModel fileSystemModel) { + this.fileSystemModel = fileSystemModel; + + fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> { refreshShown(); }); } public FileBrowserModel.Mode getMode() { - return model.getBrowserModel().getMode(); + return fileSystemModel.getBrowserModel().getMode(); } public void setAll(List newFiles) { @@ -56,9 +63,9 @@ final class FileListModel { } private void refreshShown() { - List filtered = model.getFilter().getValue() != null ? all.getValue().stream().filter(entry -> { + List filtered = fileSystemModel.getFilter().getValue() != null ? all.getValue().stream().filter(entry -> { var name = FileNames.getFileName(entry.getPath()).toLowerCase(Locale.ROOT); - var filterString = model.getFilter().getValue().toLowerCase(Locale.ROOT); + var filterString = fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT); return name.contains(filterString); }).toList() : all.getValue(); @@ -72,11 +79,11 @@ final class FileListModel { } public boolean rename(String filename, String newName) { - var fullPath = FileNames.join(model.getCurrentPath().get(), filename); - var newFullPath = FileNames.join(model.getCurrentPath().get(), newName); + var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), filename); + var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName); try { - model.getFileSystem().move(fullPath, newFullPath); - model.refresh(); + fileSystemModel.getFileSystem().move(fullPath, newFullPath); + fileSystemModel.refresh(); return true; } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); @@ -86,12 +93,12 @@ final class FileListModel { public void onDoubleClick(FileSystem.FileEntry entry) { if (!entry.isDirectory() && getMode().equals(FileBrowserModel.Mode.SINGLE_FILE_CHOOSER)) { - getModel().getBrowserModel().finishChooser(); + getFileSystemModel().getBrowserModel().finishChooser(); return; } if (entry.isDirectory()) { - model.navigate(entry.getPath(), true); + fileSystemModel.navigate(entry.getPath(), true); } else { FileOpener.openInTextEditor(entry); } diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java index caeecd50..43fb4755 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalTerminalType.java @@ -63,6 +63,7 @@ public interface ExternalTerminalType extends PrefsChoiceValue { ApplicationHelper.checkSupport(pc, executable, getDisplayName()); var toExecute = executable + " " + toCommand(name, command); + // In order to fix this bug which also affects us: https://askubuntu.com/questions/1148475/launching-gnome-terminal-from-vscode toExecute = "GNOME_TERMINAL_SCREEN=\"\" nohup " + toExecute + " /dev/null & disown"; pc.executeSimpleCommand(toExecute); } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/browser.css b/app/src/main/resources/io/xpipe/app/resources/style/browser.css index 8bc226d7..95b7e6ab 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/browser.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/browser.css @@ -42,6 +42,15 @@ -fx-opacity: 0.6; } -.browser .table-row-cell:drag { - -fx-background-color: -color-neutral-emphasis; +.browser .table-directory-view .table-view:drag-into-current { +-fx-border-color: -color-success-muted; + -fx-border-width: 2px; +} + +.browser .table-row-cell:drag-over { + -fx-background-color: -color-success-muted; +} + +.browser .table-row-cell:drag { + -fx-background-color: -color-accent-muted; } diff --git a/app/src/main/resources/io/xpipe/app/resources/style/storage-group-list-comp.css b/app/src/main/resources/io/xpipe/app/resources/style/storage-group-list-comp.css index 6013dd45..a2ea6b07 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/storage-group-list-comp.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/storage-group-list-comp.css @@ -55,7 +55,6 @@ .list-cell:empty { -fx-opacity: 0; --fx-pref-height: 1px; } .storage-group-list-comp .list-cell {