From 554f652da6ad2b07e8e8dd34017857d1663f8859 Mon Sep 17 00:00:00 2001 From: Arthur Poiret Date: Sun, 28 Jun 2026 22:21:04 +0200 Subject: [PATCH 1/5] feat: display component icon in the plugin table --- .../core/components/ApplicationDefaults.java | 8 ++---- .../controllers/ComponentInfoController.java | 5 +++- .../controllers/PluginInfoController.java | 2 +- .../controllers/PluginTableController.java | 10 +++---- .../plugin/ui/PluginComponentCellFactory.java | 10 +++---- .../plugin/ui/PluginKindBadgeView.java | 26 ++++++++++++++++++ .../com/owlplug/plugin/ui/PluginTreeCell.java | 7 ++--- .../main/resources/icons/cube-white-16.png | Bin 400 -> 0 bytes 8 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginKindBadgeView.java delete mode 100644 owlplug-client/src/main/resources/icons/cube-white-16.png diff --git a/owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java b/owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java index af63f955..e7fb28bc 100644 --- a/owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java +++ b/owlplug-client/src/main/java/com/owlplug/core/components/ApplicationDefaults.java @@ -20,7 +20,6 @@ import com.owlplug.core.model.OperatingSystem; import com.owlplug.core.model.RuntimePlatform; -import com.owlplug.explore.model.RemotePackage; import com.owlplug.plugin.model.PluginFormat; import com.owlplug.plugin.model.PluginType; import com.owlplug.project.model.DawApplication; @@ -32,7 +31,6 @@ import java.util.List; import java.util.stream.Collectors; import javafx.scene.image.Image; -import org.kordamp.ikonli.javafx.FontIcon; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -64,7 +62,7 @@ public class ApplicationDefaults { public final Image vst3Image = new Image(getClass().getResourceAsStream("/icons/vst3-green-16.png")); public final Image auImage = new Image(getClass().getResourceAsStream("/icons/au-purple-16.png")); public final Image lv2Image = new Image(getClass().getResourceAsStream("/icons/lv2-orange-16.png")); - public final Image pluginComponentImage = new Image(getClass().getResourceAsStream("/icons/cube-white-16.png")); + public final Image defaultFormat = new Image(getClass().getResourceAsStream("/icons/format-white-16.png")); public final Image verifiedSourceImage = new Image(getClass().getResourceAsStream("/icons/doublecheck-grey-16.png")); public final Image suggestedSourceImage = new Image( ApplicationDefaults.class.getResourceAsStream("/icons/check-grey-16.png")); @@ -160,14 +158,14 @@ public RuntimePlatform getRuntimePlatform() { */ public Image getPluginFormatIcon(PluginFormat format) { if (format == null) { - return pluginComponentImage; + return defaultFormat; } return switch (format) { case VST2 -> vst2Image; case VST3 -> vst3Image; case AU -> auImage; case LV2 -> lv2Image; - default -> pluginComponentImage; + default -> defaultFormat; }; } diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/ComponentInfoController.java b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/ComponentInfoController.java index 4d778f94..3939b160 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/ComponentInfoController.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/ComponentInfoController.java @@ -24,6 +24,7 @@ import com.owlplug.plugin.model.PluginComponent; import com.owlplug.plugin.services.PluginService; import com.owlplug.plugin.ui.PluginFormatBadgeView; +import com.owlplug.plugin.ui.PluginKindBadgeView; import com.owlplug.plugin.ui.PluginStateView; import java.util.ArrayList; import java.util.Optional; @@ -82,13 +83,15 @@ public class ComponentInfoController extends BaseController { public void initialize() { componentProperty.addListener(e -> refresh()); pluginScreenshotPane.setEffect(new ColorAdjust(0, 0, -0.6, 0)); + pluginFormatBadgeContainer.setSpacing(5); } public void refresh() { PluginComponent component = componentProperty.get(); pluginFormatBadgeContainer.getChildren().setAll( - new PluginFormatBadgeView(component.getPlugin().getFormat(), this.getApplicationDefaults())); + new PluginKindBadgeView(component, this.getApplicationDefaults()), + new Label(component.asPlugin().getFormat().getName() + " Component")); pluginTitleLabel.setText(component.getName()); pluginNameLabel.setText(Optional.ofNullable(component.getDescriptiveName()).orElse(component.getName())); pluginVersionLabel.setText(Optional.ofNullable(component.getVersion()).orElse("Unknown")); diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java index 828f203a..548b9bf8 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginInfoController.java @@ -157,7 +157,7 @@ public void initialize() { Async.run(() -> pluginService.enablePlugin(plugin)); }); - pluginComponentListView.setCellFactory(new PluginComponentCellFactory(this.getApplicationDefaults())); + pluginComponentListView.setCellFactory(new PluginComponentCellFactory()); lastScanErrorLabel.managedProperty().bind(lastScanErrorLabel.visibleProperty()); lastScanErrorLabel.setVisible(false); diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java index 9cda0e51..60da23a0 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java @@ -29,7 +29,7 @@ import com.owlplug.plugin.model.PluginFormat; import com.owlplug.plugin.model.PluginState; import com.owlplug.plugin.services.PluginService; -import com.owlplug.plugin.ui.PluginFormatBadgeView; +import com.owlplug.plugin.ui.PluginKindBadgeView; import com.owlplug.plugin.ui.PluginStateView; import java.io.File; import com.owlplug.core.utils.Async; @@ -48,7 +48,6 @@ import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.image.ImageView; -import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.kordamp.ikonli.javafx.FontIcon; @@ -114,17 +113,16 @@ private void createColumns() { TableColumn nameColumn = new TableColumn<>("Name"); nameColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName())); TableColumn formatColumn = new TableColumn<>("Format"); - formatColumn.setCellValueFactory(cellData -> new SimpleObjectProperty<>(cellData.getValue().asPlugin().getFormat())); + formatColumn.setCellValueFactory( + cellData -> new SimpleObjectProperty<>(cellData.getValue().asPlugin().getFormat())); formatColumn.setCellFactory(e -> new TableCell<>() { @Override public void updateItem(PluginFormat item, boolean empty) { super.updateItem(item, empty); if (item == null || empty) { - setText(null); setGraphic(null); } else { - setText(null); - setGraphic(new HBox(new PluginFormatBadgeView(item, getApplicationDefaults(), PluginFormatBadgeView.DisplayMode.ICON_ONLY))); + setGraphic(new PluginKindBadgeView(getTableRow().getItem(), getApplicationDefaults())); } } }); diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginComponentCellFactory.java b/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginComponentCellFactory.java index 562d88a5..431f1929 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginComponentCellFactory.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginComponentCellFactory.java @@ -24,14 +24,13 @@ import javafx.scene.control.ListView; import javafx.scene.image.ImageView; import javafx.util.Callback; +import org.kordamp.ikonli.javafx.FontIcon; public class PluginComponentCellFactory implements Callback, ListCell> { - private ApplicationDefaults applicationDefaults; - public PluginComponentCellFactory(ApplicationDefaults applicationDefaults) { + public PluginComponentCellFactory() { - this.applicationDefaults = applicationDefaults; } @@ -46,10 +45,9 @@ public void updateItem(PluginComponent plugin, boolean empty) { setText(null); setGraphic(null); } else { - ImageView imageView = new ImageView(); - imageView.setImage(applicationDefaults.pluginComponentImage); + FontIcon icon = new FontIcon("mdi2t-toy-brick-outline"); setText(plugin.getName()); - setGraphic(imageView); + setGraphic(icon); } } }; diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginKindBadgeView.java b/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginKindBadgeView.java new file mode 100644 index 00000000..f39011ca --- /dev/null +++ b/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginKindBadgeView.java @@ -0,0 +1,26 @@ +package com.owlplug.plugin.ui; + +import com.owlplug.core.components.ApplicationDefaults; +import com.owlplug.plugin.model.IPlugin; +import com.owlplug.plugin.model.PluginComponent; +import javafx.geometry.Pos; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import org.kordamp.ikonli.javafx.FontIcon; + +public class PluginKindBadgeView extends HBox { + + public PluginKindBadgeView(IPlugin plugin, ApplicationDefaults applicationDefaults) { + super(4); + setAlignment(Pos.CENTER_LEFT); + + if (plugin instanceof PluginComponent component) { + getChildren().add(new PluginFormatBadgeView(component.asPlugin().getFormat(), applicationDefaults)); + FontIcon icon = new FontIcon("mdi2t-toy-brick-outline"); + Tooltip.install(icon, new Tooltip("Component")); + getChildren().add(icon); + } else { + getChildren().add(new PluginFormatBadgeView(plugin.asPlugin().getFormat(), applicationDefaults)); + } + } +} \ No newline at end of file diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginTreeCell.java b/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginTreeCell.java index 09dd3e93..8064f9a0 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginTreeCell.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/ui/PluginTreeCell.java @@ -30,7 +30,6 @@ import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.TreeCell; -import javafx.scene.image.ImageView; import javafx.scene.layout.HBox; import javafx.scene.shape.Circle; import javafx.scene.text.Text; @@ -39,8 +38,8 @@ public class PluginTreeCell extends TreeCell { - private PluginService pluginService; - private ApplicationDefaults applicationDefaults; + private final PluginService pluginService; + private final ApplicationDefaults applicationDefaults; public PluginTreeCell(ApplicationDefaults applicationDefaults, PluginService pluginService) { this.applicationDefaults = applicationDefaults; @@ -106,7 +105,7 @@ private void renderPlugin(Plugin plugin) { private void renderComponent(PluginComponent pluginComponent) { Label label = new Label(pluginComponent.getName()); - label.setGraphic(new ImageView(applicationDefaults.pluginComponentImage)); + label.setGraphic(new FontIcon("mdi2t-toy-brick-outline")); setGraphic(label); setText(null); } diff --git a/owlplug-client/src/main/resources/icons/cube-white-16.png b/owlplug-client/src/main/resources/icons/cube-white-16.png deleted file mode 100644 index 82429f2c396f579721b8579bbd6b4c612633853c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 400 zcmV;B0dM|^P)jBg~a4>XH{d%zpe&a!Og zr7X)V&<0+C-n zGnJ(=?i*vCCEfn=6RQmDO1em+I+3)OOb?RIB$XtcT5InMOB{_x7H9!4zz6W2mgm5G uKU3MF&r~+XbS2q5&## Date: Sun, 28 Jun 2026 23:45:43 +0200 Subject: [PATCH 2/5] feat: add plugin side filters --- .../plugin/components/PluginFilterModel.java | 99 ++++++++ .../controllers/PluginFilterController.java | 229 ++++++++++++++++++ .../controllers/PluginTableController.java | 24 +- .../controllers/PluginTreeViewController.java | 61 ++--- .../plugin/controllers/PluginsController.java | 23 ++ .../resources/fxml/plugins/PluginsView.fxml | 19 +- owlplug-client/src/main/resources/owlplug.css | 50 ++++ 7 files changed, 465 insertions(+), 40 deletions(-) create mode 100644 owlplug-client/src/main/java/com/owlplug/plugin/components/PluginFilterModel.java create mode 100644 owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginFilterController.java diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/components/PluginFilterModel.java b/owlplug-client/src/main/java/com/owlplug/plugin/components/PluginFilterModel.java new file mode 100644 index 00000000..5a3d1915 --- /dev/null +++ b/owlplug-client/src/main/java/com/owlplug/plugin/components/PluginFilterModel.java @@ -0,0 +1,99 @@ +/* OwlPlug + * Copyright (C) 2021 Arthur + * + * This file is part of OwlPlug. + * + * OwlPlug is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 + * as published by the Free Software Foundation. + * + * OwlPlug is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OwlPlug. If not, see . + */ + +package com.owlplug.plugin.components; + +import com.owlplug.plugin.model.IPlugin; +import com.owlplug.plugin.model.Plugin; +import com.owlplug.plugin.model.PluginComponent; +import com.owlplug.plugin.model.PluginFormat; +import java.util.function.Predicate; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; +import org.springframework.stereotype.Component; + +@Component +public class PluginFilterModel { + + private final ObservableSet selectedFormats = FXCollections.observableSet(); + private final ObservableSet selectedManufacturers = FXCollections.observableSet(); + private final ObservableSet selectedCategories = FXCollections.observableSet(); + private final ObjectProperty> predicate = new SimpleObjectProperty<>(); + + public PluginFilterModel() { + predicate.bind(Bindings.createObjectBinding(this::buildPredicate, + selectedFormats, selectedManufacturers, selectedCategories)); + } + + private Predicate buildPredicate() { + if (selectedFormats.isEmpty() && selectedManufacturers.isEmpty() && selectedCategories.isEmpty()) { + return null; + } + return plugin -> { + if (plugin instanceof PluginComponent component) { + return matchesFormat(component.asPlugin().getFormat()) + && matchesManufacturer(component.getManufacturerName()) + && matchesCategory(component.getCategory()); + } else { + Plugin p = (Plugin) plugin; + return matchesFormat(p.getFormat()) + && (matchesManufacturer(p.getManufacturerName()) + || p.getComponents().stream().anyMatch(c -> matchesManufacturer(c.getManufacturerName()))) + && (matchesCategory(p.getCategory()) + || p.getComponents().stream().anyMatch(c -> matchesCategory(c.getCategory()))); + } + }; + } + + private boolean matchesFormat(PluginFormat format) { + return selectedFormats.isEmpty() || selectedFormats.contains(format); + } + + private boolean matchesManufacturer(String manufacturer) { + return selectedManufacturers.isEmpty() || selectedManufacturers.contains(manufacturer); + } + + private boolean matchesCategory(String category) { + return selectedCategories.isEmpty() || selectedCategories.contains(category); + } + + public ObservableSet getSelectedFormats() { + return selectedFormats; + } + + public ObservableSet getSelectedManufacturers() { + return selectedManufacturers; + } + + public ObservableSet getSelectedCategories() { + return selectedCategories; + } + + public ObjectProperty> predicateProperty() { + return predicate; + } + + public void clearAll() { + selectedFormats.clear(); + selectedManufacturers.clear(); + selectedCategories.clear(); + } +} diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginFilterController.java b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginFilterController.java new file mode 100644 index 00000000..c9615e46 --- /dev/null +++ b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginFilterController.java @@ -0,0 +1,229 @@ +/* OwlPlug + * Copyright (C) 2021 Arthur + * + * This file is part of OwlPlug. + * + * OwlPlug is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 + * as published by the Free Software Foundation. + * + * OwlPlug is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OwlPlug. If not, see . + */ + +package com.owlplug.plugin.controllers; + +import com.owlplug.core.components.ApplicationDefaults; +import com.owlplug.core.ui.SideBar; +import com.owlplug.plugin.components.PluginFilterModel; +import com.owlplug.plugin.model.Plugin; +import com.owlplug.plugin.model.PluginFormat; +import com.owlplug.plugin.ui.PluginFormatBadgeView; +import jakarta.annotation.PostConstruct; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.CheckBox; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; + +@Controller +public class PluginFilterController { + + @Autowired + private PluginFilterModel filterModel; + @Autowired + private ApplicationDefaults applicationDefaults; + + private static final int PREVIEW_COUNT = 3; + + private SideBar sideBar; + private VBox formatSection; + private VBox manufacturerSection; + private VBox categorySection; + private final Map formatCheckBoxes = new EnumMap<>(PluginFormat.class); + + @PostConstruct + public void setup() { + formatSection = new VBox(4); + manufacturerSection = new VBox(4); + categorySection = new VBox(4); + + for (PluginFormat format : PluginFormat.values()) { + CheckBox cb = new CheckBox(format.getName()); + cb.setGraphic(new PluginFormatBadgeView(format, applicationDefaults, PluginFormatBadgeView.DisplayMode.ICON_ONLY)); + cb.selectedProperty().addListener((obs, old, selected) -> { + if (selected != filterModel.getSelectedFormats().contains(format)) { + if (selected) { + filterModel.getSelectedFormats().add(format); + } else { + filterModel.getSelectedFormats().remove(format); + } + } + }); + filterModel.getSelectedFormats().addListener( + (SetChangeListener) change -> cb.setSelected(filterModel.getSelectedFormats().contains(format))); + formatCheckBoxes.put(format, cb); + formatSection.getChildren().add(cb); + } + + ScrollPane scrollPane = new ScrollPane(buildContent()); + scrollPane.setFitToWidth(true); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.getStyleClass().add("plugin-filter-scroll"); + + sideBar = new SideBar(220, scrollPane); + sideBar.getStyleClass().add("plugin-filter-sidebar"); + sideBar.setVisible(false); + sideBar.setPrefWidth(0); + } + + private VBox buildContent() { + VBox content = new VBox(8); + content.setPadding(new Insets(12)); + + HBox header = new HBox(); + header.setAlignment(Pos.CENTER_LEFT); + Label titleLabel = new Label("Filters"); + titleLabel.getStyleClass().add("plugin-filter-title"); + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + Hyperlink clearLink = new Hyperlink("Clear all"); + clearLink.getStyleClass().add("plugin-filter-clear"); + clearLink.setOnAction(e -> filterModel.clearAll()); + header.getChildren().addAll(titleLabel, spacer, clearLink); + + Label formatLabel = new Label("Format"); + formatLabel.getStyleClass().add("plugin-filter-section-title"); + Label manufacturerLabel = new Label("Manufacturer"); + manufacturerLabel.getStyleClass().add("plugin-filter-section-title"); + Label categoryLabel = new Label("Category"); + categoryLabel.getStyleClass().add("plugin-filter-section-title"); + + content.getChildren().addAll( + header, new Separator(), + formatLabel, formatSection, + new Separator(), manufacturerLabel, manufacturerSection, + new Separator(), categoryLabel, categorySection + ); + return content; + } + + public void refresh(List plugins) { + Map formatCounts = plugins.stream() + .filter(p -> p.getFormat() != null) + .collect(Collectors.groupingBy(Plugin::getFormat, Collectors.counting())); + formatCheckBoxes.forEach((format, cb) -> { + long count = formatCounts.getOrDefault(format, 0L); + cb.setText(format.getName() + " (" + count + ")"); + }); + + Map manufacturerCounts = plugins.stream() + .filter(p -> p.getManufacturerName() != null && !p.getManufacturerName().isBlank()) + .collect(Collectors.groupingBy(Plugin::getManufacturerName, Collectors.counting())); + buildExpandableSection( + manufacturerSection, manufacturerCounts, + value -> toggle(filterModel.getSelectedManufacturers(), value), + filterModel.getSelectedManufacturers() + ); + + Map categoryCounts = plugins.stream() + .filter(p -> p.getCategory() != null && !p.getCategory().isBlank()) + .collect(Collectors.groupingBy(Plugin::getCategory, Collectors.counting())); + buildExpandableSection( + categorySection, categoryCounts, + value -> toggle(filterModel.getSelectedCategories(), value), + filterModel.getSelectedCategories() + ); + } + + private void toggle(ObservableSet set, String value) { + if (set.contains(value)) { + set.remove(value); + } else { + set.add(value); + } + } + + private void buildExpandableSection(VBox container, Map countMap, + Consumer onToggle, ObservableSet selectedSet) { + container.getChildren().clear(); + if (countMap.isEmpty()) { + return; + } + + List values = countMap.entrySet().stream() + .sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()) + .thenComparing(Map.Entry.comparingByKey())) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + + List preview = values.subList(0, Math.min(PREVIEW_COUNT, values.size())); + List remaining = values.size() > PREVIEW_COUNT + ? values.subList(PREVIEW_COUNT, values.size()) + : List.of(); + + VBox previewBox = new VBox(4); + for (String value : preview) { + previewBox.getChildren().add(createCheckBox(value, countMap.get(value), onToggle, selectedSet)); + } + container.getChildren().add(previewBox); + + if (!remaining.isEmpty()) { + VBox fullBox = new VBox(4); + for (String value : remaining) { + fullBox.getChildren().add(createCheckBox(value, countMap.get(value), onToggle, selectedSet)); + } + fullBox.setVisible(false); + fullBox.setManaged(false); + + Hyperlink viewMore = new Hyperlink("View more (" + remaining.size() + ")"); + viewMore.getStyleClass().add("plugin-filter-view-more"); + viewMore.setOnAction(e -> { + boolean expanded = fullBox.isVisible(); + fullBox.setVisible(!expanded); + fullBox.setManaged(!expanded); + viewMore.setText(expanded ? "View more (" + remaining.size() + ")" : "View less"); + }); + container.getChildren().addAll(fullBox, viewMore); + } + } + + private CheckBox createCheckBox(String value, long count, Consumer onToggle, + ObservableSet selectedSet) { + CheckBox cb = new CheckBox(value + " (" + count + ")"); + cb.setSelected(selectedSet.contains(value)); + cb.selectedProperty().addListener((obs, old, selected) -> { + if (selected != selectedSet.contains(value)) { + onToggle.accept(value); + } + }); + selectedSet.addListener( + (SetChangeListener) change -> cb.setSelected(selectedSet.contains(value))); + return cb; + } + + public SideBar getView() { + return sideBar; + } +} diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java index 60da23a0..75620d4f 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTableController.java @@ -23,6 +23,7 @@ import com.owlplug.core.controllers.BaseController; import com.owlplug.core.utils.FileUtils; import com.owlplug.core.utils.PlatformUtils; +import com.owlplug.plugin.components.PluginFilterModel; import com.owlplug.plugin.controllers.dialogs.DisablePluginDialogController; import com.owlplug.plugin.model.IPlugin; import com.owlplug.plugin.model.Plugin; @@ -31,6 +32,7 @@ import com.owlplug.plugin.services.PluginService; import com.owlplug.plugin.ui.PluginKindBadgeView; import com.owlplug.plugin.ui.PluginStateView; +import java.util.function.Predicate; import java.io.File; import com.owlplug.core.utils.Async; import javafx.beans.binding.Bindings; @@ -67,6 +69,7 @@ public class PluginTableController extends BaseController { private PluginService pluginService; private final SimpleStringProperty search = new SimpleStringProperty(); + private final SimpleObjectProperty> filterPredicate = new SimpleObjectProperty<>(); private final TableView tableView; private final ObservableList pluginList; @@ -95,13 +98,20 @@ public PluginTableController() { FilteredList filteredPluginList = new FilteredList<>(pluginList); filteredPluginList.predicateProperty().bind(Bindings.createObjectBinding(() -> { - if (search.getValue() == null || search.getValue().isEmpty()) { + String searchVal = search.getValue(); + boolean hasSearch = searchVal != null && !searchVal.isEmpty(); + Predicate fp = filterPredicate.get(); + if (!hasSearch && fp == null) { return null; } - return (plugin) -> plugin.getName().toLowerCase().contains(search.getValue().toLowerCase()) - || (plugin.getCategory() != null && plugin.getCategory().toLowerCase().contains( - search.getValue().toLowerCase())); - }, search)); + return plugin -> { + boolean searchMatch = !hasSearch + || plugin.getName().toLowerCase().contains(searchVal.toLowerCase()) + || (plugin.getCategory() != null + && plugin.getCategory().toLowerCase().contains(searchVal.toLowerCase())); + return searchMatch && (fp == null || fp.test(plugin)); + }; + }, search, filterPredicate)); SortedList sortedPluginList = new SortedList<>(filteredPluginList); tableView.setItems(sortedPluginList); @@ -189,6 +199,10 @@ public void updateItem(PluginState item, boolean empty) { } + public void bindFilterModel(PluginFilterModel filterModel) { + filterPredicate.bind(filterModel.predicateProperty()); + } + public void setPlugins(Iterable plugins) { pluginList.clear(); plugins.forEach(p -> { diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTreeViewController.java b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTreeViewController.java index 9890f4a1..d89ee960 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTreeViewController.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginTreeViewController.java @@ -20,6 +20,7 @@ import com.owlplug.core.controllers.BaseController; import com.owlplug.core.ui.FilterableTreeItem; +import com.owlplug.plugin.components.PluginFilterModel; import com.owlplug.plugin.model.IDirectory; import com.owlplug.plugin.model.IPlugin; import com.owlplug.plugin.model.Plugin; @@ -33,7 +34,9 @@ import java.util.HashMap; import java.util.List; import java.util.Set; +import java.util.function.Predicate; import javafx.beans.binding.Bindings; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; @@ -51,6 +54,7 @@ public class PluginTreeViewController extends BaseController { private SymlinkRepository symlinkRepository; private final SimpleStringProperty search = new SimpleStringProperty(); + private final SimpleObjectProperty> filterPredicate = new SimpleObjectProperty<>(); private final TreeView pluginTreeView; private final FilterableTreeItem treePluginNode; private final FilterableTreeItem treeFileRootNode; @@ -68,36 +72,13 @@ public PluginTreeViewController() { pluginTreeView.setRoot(treePluginNode); - // Binds search property to plugin tree filter - treePluginNode.predicateProperty().bind(Bindings.createObjectBinding(() -> { - if (search.getValue() == null || search.getValue().isEmpty()) { - return null; - } - return (item) -> { - if (item instanceof IPlugin plugin) { - return plugin.getName().toLowerCase().contains(search.getValue().toLowerCase()) - || (plugin.getCategory() != null && plugin.getCategory().toLowerCase().contains( - search.getValue().toLowerCase())); - } else { - return item.toString().toLowerCase().contains(search.getValue().toLowerCase()); - } - }; - }, search)); + treePluginNode.predicateProperty().bind( + Bindings.createObjectBinding(() -> buildTreePredicate(search.getValue(), filterPredicate.get()), + search, filterPredicate)); - // Binds search property to file tree filter - treeFileRootNode.predicateProperty().bind(Bindings.createObjectBinding(() -> { - if (search.getValue() == null || search.getValue().isEmpty()) { - return null; - } - return (item) -> { - if (item instanceof IPlugin plugin) { - return plugin.getName().toLowerCase().contains(search.getValue().toLowerCase()) - || (plugin.getCategory() != null && plugin.getCategory().toLowerCase().contains(search.getValue().toLowerCase())); - } else { - return item.toString().toLowerCase().contains(search.getValue().toLowerCase()); - } - }; - }, search)); + treeFileRootNode.predicateProperty().bind( + Bindings.createObjectBinding(() -> buildTreePredicate(search.getValue(), filterPredicate.get()), + search, filterPredicate)); } @@ -105,6 +86,28 @@ public SimpleStringProperty searchProperty() { return this.search; } + public void bindFilterModel(PluginFilterModel filterModel) { + filterPredicate.bind(filterModel.predicateProperty()); + } + + private Predicate buildTreePredicate(String searchVal, Predicate fp) { + boolean hasSearch = searchVal != null && !searchVal.isEmpty(); + if (!hasSearch && fp == null) { + return null; + } + return item -> { + if (item instanceof IPlugin plugin) { + boolean searchMatch = !hasSearch + || plugin.getName().toLowerCase().contains(searchVal.toLowerCase()) + || (plugin.getCategory() != null + && plugin.getCategory().toLowerCase().contains(searchVal.toLowerCase())); + return searchMatch && (fp == null || fp.test(plugin)); + } else { + return !hasSearch || item.toString().toLowerCase().contains(searchVal.toLowerCase()); + } + }; + } + public TreeView getTreeView() { return pluginTreeView; } diff --git a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java index 99e41363..dc2bcbb1 100644 --- a/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java +++ b/owlplug-client/src/main/java/com/owlplug/plugin/controllers/PluginsController.java @@ -22,15 +22,18 @@ import com.owlplug.core.components.ApplicationDefaults.Prefs; import com.owlplug.core.controllers.BaseController; import com.owlplug.core.utils.FX; +import com.owlplug.plugin.components.PluginFilterModel; import com.owlplug.plugin.components.PluginTaskFactory; import com.owlplug.plugin.controllers.dialogs.ExportDialogController; import com.owlplug.plugin.controllers.dialogs.NewLinkController; import com.owlplug.plugin.events.PluginRefreshEvent; import com.owlplug.plugin.events.PluginScanCompletedEvent; import com.owlplug.plugin.events.PluginUpdateEvent; +import com.owlplug.plugin.model.Plugin; import com.owlplug.plugin.repositories.PluginRepository; import com.owlplug.plugin.services.PluginService; import com.owlplug.core.utils.Async; +import java.util.List; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.MenuItem; @@ -38,6 +41,7 @@ import javafx.scene.control.Tab; import javafx.scene.control.TabPane; import javafx.scene.control.TextField; +import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.event.EventListener; @@ -62,6 +66,10 @@ public class PluginsController extends BaseController { protected PluginTreeViewController treeViewController; @Autowired protected PluginTableController tableController; + @Autowired + protected PluginFilterController filterController; + @Autowired + protected PluginFilterModel filterModel; @FXML private SplitMenuButton scanMenuButton; @@ -85,9 +93,13 @@ public class PluginsController extends BaseController { @FXML private Button newLinkButton; @FXML + private Button filterToggleButton; + @FXML private VBox pluginInfoPane; @FXML private VBox pluginsContainer; + @FXML + private HBox filterContainer; /** * FXML initialize method. @@ -99,6 +111,11 @@ public void initialize() { newLinkController.show(); }); + // Wire filter sidebar and predicate into sub-controllers + filterContainer.getChildren().add(0, filterController.getView()); + tableController.bindFilterModel(filterModel); + treeViewController.bindFilterModel(filterModel); + // Add Plugin Table and TreeView to the scene graph pluginsContainer.getChildren().add(treeViewController.getTreeView()); pluginsContainer.getChildren().add(tableController.getTableView()); @@ -215,6 +232,7 @@ public void displayPlugins() { .thenAccept(plugins -> FX.run(() -> { treeViewController.setPlugins(plugins); tableController.setPlugins(plugins); + filterController.refresh(plugins); })); } @@ -231,6 +249,11 @@ public void refresh() { tableController.refresh(); } + @FXML + public void toggleFilter() { + filterController.getView().toggle(); + } + public void setSearch(String query) { searchTextField.setText(query); } diff --git a/owlplug-client/src/main/resources/fxml/plugins/PluginsView.fxml b/owlplug-client/src/main/resources/fxml/plugins/PluginsView.fxml index ea80ac0e..9c7eaf72 100644 --- a/owlplug-client/src/main/resources/fxml/plugins/PluginsView.fxml +++ b/owlplug-client/src/main/resources/fxml/plugins/PluginsView.fxml @@ -17,15 +17,20 @@ - - - + + +