Add showcases
This commit is contained in:
parent
11d4e0d199
commit
84c9a23fa9
10
pom.xml
10
pom.xml
@ -99,6 +99,11 @@
|
||||
<artifactId>javafx-swing</artifactId>
|
||||
<version>${openjfx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-media</artifactId>
|
||||
<version>${openjfx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-web</artifactId>
|
||||
@ -114,6 +119,11 @@
|
||||
<artifactId>ikonli-feather-pack</artifactId>
|
||||
<version>${lib.ikonli.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.kordamp.ikonli</groupId>
|
||||
<artifactId>ikonli-material2-pack</artifactId>
|
||||
<version>${lib.ikonli.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>fr.brouillard.oss</groupId>
|
||||
|
@ -46,6 +46,10 @@
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-swing</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-media</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-web</artifactId>
|
||||
@ -58,6 +62,10 @@
|
||||
<groupId>org.kordamp.ikonli</groupId>
|
||||
<artifactId>ikonli-feather-pack</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.kordamp.ikonli</groupId>
|
||||
<artifactId>ikonli-material2-pack</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>fr.brouillard.oss</groupId>
|
||||
<artifactId>cssfx</artifactId>
|
||||
|
@ -21,6 +21,11 @@ public class ApplicationWindow extends BorderPane {
|
||||
|
||||
sidebar.setOnSelect(pageClass -> {
|
||||
try {
|
||||
// reset previous page, e.g. to free resources
|
||||
if (!pageContainer.getChildren().isEmpty() && pageContainer.getChildren().get(0) instanceof Page page) {
|
||||
page.reset();
|
||||
}
|
||||
|
||||
Page page = pageClass.getDeclaredConstructor().newInstance();
|
||||
pageContainer.getChildren().setAll(page.getView());
|
||||
} catch (Exception e) {
|
||||
|
@ -7,6 +7,8 @@ import atlantafx.sampler.page.Page;
|
||||
import atlantafx.sampler.page.components.*;
|
||||
import atlantafx.sampler.page.general.ThemePage;
|
||||
import atlantafx.sampler.page.general.TypographyPage;
|
||||
import atlantafx.sampler.page.showcase.filemanager.FileManagerPage;
|
||||
import atlantafx.sampler.page.showcase.musicplayer.MusicPlayerPage;
|
||||
import atlantafx.sampler.util.Containers;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
@ -67,8 +69,8 @@ public class Sidebar extends VBox {
|
||||
|
||||
var navScroll = new ScrollPane(navContainer);
|
||||
Containers.setScrollConstraints(navScroll,
|
||||
ScrollPane.ScrollBarPolicy.AS_NEEDED, true,
|
||||
ScrollPane.ScrollBarPolicy.AS_NEEDED, true
|
||||
ScrollPane.ScrollBarPolicy.AS_NEEDED, true,
|
||||
ScrollPane.ScrollBarPolicy.AS_NEEDED, true
|
||||
);
|
||||
VBox.setVgrow(navScroll, ALWAYS);
|
||||
|
||||
@ -144,7 +146,10 @@ public class Sidebar extends VBox {
|
||||
navLink(ToolBarPage.NAME, ToolBarPage.class),
|
||||
navLink(TooltipPage.NAME, TooltipPage.class),
|
||||
navLink(TreePage.NAME, TreePage.class),
|
||||
navLink(TreeTablePage.NAME, TreeTablePage.class)
|
||||
navLink(TreeTablePage.NAME, TreeTablePage.class),
|
||||
caption("SHOWCASE"),
|
||||
navLink(FileManagerPage.NAME, FileManagerPage.class),
|
||||
navLink(MusicPlayerPage.NAME, MusicPlayerPage.class)
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -1,22 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase;
|
||||
|
||||
import atlantafx.sampler.page.AbstractPage;
|
||||
|
||||
public class Chat extends AbstractPage {
|
||||
|
||||
public static final String NAME = "Chat";
|
||||
|
||||
public Chat() {
|
||||
super();
|
||||
createView();
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase;
|
||||
|
||||
import atlantafx.sampler.page.AbstractPage;
|
||||
|
||||
public class FileManager extends AbstractPage {
|
||||
|
||||
public static final String NAME = "File Manager";
|
||||
|
||||
public FileManager() {
|
||||
super();
|
||||
createView();
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase;
|
||||
|
||||
import atlantafx.sampler.page.AbstractPage;
|
||||
|
||||
public class MusicPlayer extends AbstractPage {
|
||||
|
||||
public static final String NAME = "Music Player";
|
||||
|
||||
public MusicPlayer() {
|
||||
super();
|
||||
createView();
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package atlantafx.sampler.page.showcase;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.sampler.page.AbstractPage;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.StageStyle;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import static atlantafx.base.theme.Styles.ACCENT;
|
||||
|
||||
public abstract class ShowcasePage extends AbstractPage {
|
||||
|
||||
protected static final PseudoClass SHOWCASE_MODE = PseudoClass.getPseudoClass("showcase-mode");
|
||||
protected final AnchorPane showcase = new AnchorPane();
|
||||
protected final HBox expandBox = new HBox(10);
|
||||
protected final HBox collapseBox = new HBox(10);
|
||||
|
||||
public ShowcasePage() {
|
||||
createShowcaseLayout();
|
||||
}
|
||||
|
||||
protected void createShowcaseLayout() {
|
||||
var expandBtn = new Button("Expand");
|
||||
expandBtn.setGraphic(new FontIcon(Feather.MAXIMIZE_2));
|
||||
expandBtn.getStyleClass().add(ACCENT);
|
||||
expandBtn.setOnAction(e -> {
|
||||
expandBtn.getScene().getRoot().pseudoClassStateChanged(SHOWCASE_MODE, true);
|
||||
VBox.setVgrow(showcase, Priority.ALWAYS);
|
||||
expandBox.setVisible(false);
|
||||
expandBox.setManaged(false);
|
||||
collapseBox.setVisible(true);
|
||||
collapseBox.setManaged(true);
|
||||
});
|
||||
expandBox.getChildren().setAll(new Spacer(), expandBtn, new Spacer());
|
||||
expandBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
var collapseBtn = new Hyperlink("Exit showcase mode");
|
||||
collapseBtn.setOnAction(e -> {
|
||||
expandBtn.getScene().getRoot().pseudoClassStateChanged(SHOWCASE_MODE, false);
|
||||
VBox.setVgrow(showcase, Priority.NEVER);
|
||||
expandBox.setVisible(true);
|
||||
expandBox.setManaged(true);
|
||||
collapseBox.setVisible(false);
|
||||
collapseBox.setManaged(false);
|
||||
});
|
||||
collapseBox.getChildren().setAll(new FontIcon(Feather.MINIMIZE_2), collapseBtn);
|
||||
collapseBox.setAlignment(Pos.CENTER_LEFT);
|
||||
collapseBox.setPadding(new Insets(5));
|
||||
collapseBox.setVisible(false);
|
||||
collapseBox.setManaged(false);
|
||||
|
||||
sourceCodeToggleBtn.setVisible(false);
|
||||
sourceCodeToggleBtn.setManaged(false);
|
||||
|
||||
userContent.getChildren().setAll(showcase, expandBox, collapseBox);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
protected void showWarning(String header, String description) {
|
||||
var alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("Error Dialog");
|
||||
alert.setHeaderText(header);
|
||||
alert.setContentText(description);
|
||||
alert.initOwner(getScene().getWindow());
|
||||
alert.initStyle(StageStyle.DECORATED);
|
||||
alert.showAndWait();
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase;
|
||||
|
||||
import atlantafx.sampler.page.AbstractPage;
|
||||
|
||||
public class TextEditor extends AbstractPage {
|
||||
|
||||
public static final String NAME = "Text Editor";
|
||||
|
||||
public TextEditor() {
|
||||
super();
|
||||
createView();
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return NAME;
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.filemanager;
|
||||
|
||||
import org.kordamp.ikonli.Ikon;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
record Bookmark(String title, Path path, Ikon icon) { }
|
@ -0,0 +1,76 @@
|
||||
package atlantafx.sampler.page.showcase.filemanager;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static atlantafx.sampler.page.showcase.filemanager.Model.USER_HOME;
|
||||
|
||||
final class BookmarkList extends ListView<Bookmark> {
|
||||
|
||||
public BookmarkList(Model model) {
|
||||
getStyleClass().add("bookmark-list");
|
||||
|
||||
// this is Linux specific and only for EN locale
|
||||
getItems().setAll(
|
||||
new Bookmark("Home", USER_HOME, Feather.HOME),
|
||||
new Bookmark("Documents", USER_HOME.resolve("Documents"), Feather.FILE),
|
||||
new Bookmark("Downloads", USER_HOME.resolve("Downloads"), Feather.DOWNLOAD),
|
||||
new Bookmark("Music", USER_HOME.resolve("Music"), Feather.MUSIC),
|
||||
new Bookmark("Pictures", USER_HOME.resolve("Pictures"), Feather.IMAGE),
|
||||
new Bookmark("Videos", USER_HOME.resolve("Videos"), Feather.VIDEO)
|
||||
);
|
||||
|
||||
setCellFactory(param -> {
|
||||
var cell = new BookmarkCell();
|
||||
cell.setOnMousePressed((MouseEvent event) -> {
|
||||
if (cell.isEmpty() || cell.getItem() == null) {
|
||||
event.consume();
|
||||
} else {
|
||||
Path path = cell.getItem().path();
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
var alert = new Alert(Alert.AlertType.WARNING);
|
||||
alert.setTitle("Error Dialog");
|
||||
alert.setHeaderText("Sorry, this is just demo.");
|
||||
alert.setContentText("There's no such directory as \"" + path + "\"");
|
||||
alert.initOwner(getScene().getWindow());
|
||||
alert.showAndWait();
|
||||
return;
|
||||
}
|
||||
|
||||
model.navigate(path, true);
|
||||
}
|
||||
});
|
||||
return cell;
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static class BookmarkCell extends ListCell<Bookmark> {
|
||||
|
||||
public BookmarkCell() {
|
||||
setGraphicTextGap(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Bookmark bookmark, boolean empty) {
|
||||
super.updateItem(bookmark, empty);
|
||||
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
setText(null);
|
||||
} else {
|
||||
setGraphic(new FontIcon(bookmark.icon()));
|
||||
setText(bookmark.title());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.filemanager;
|
||||
|
||||
import javafx.scene.layout.Pane;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
interface DirectoryView {
|
||||
|
||||
Pane getView();
|
||||
|
||||
FileList getFileList();
|
||||
|
||||
void setDirectory(Path path);
|
||||
|
||||
void setOnAction(Consumer<Path> handler);
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.filemanager;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
import javafx.scene.control.TableView;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static atlantafx.sampler.page.showcase.filemanager.Utils.isFileHidden;
|
||||
|
||||
final class FileList {
|
||||
|
||||
static final Comparator<Path> FILE_TYPE_COMPARATOR =
|
||||
Comparator.comparing(path -> !Files.isDirectory(path));
|
||||
static final Predicate<Path> PREDICATE_ANY = path -> true;
|
||||
static final Predicate<Path> PREDICATE_NOT_HIDDEN = path -> !isFileHidden(path);
|
||||
|
||||
private final ObservableList<Path> list = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Predicate<Path>> predicateProperty = new SimpleObjectProperty<>(path -> true);
|
||||
|
||||
public FileList(TableView<Path> table) {
|
||||
var filteredList = new FilteredList<>(list);
|
||||
filteredList.predicateProperty().bind(predicateProperty);
|
||||
|
||||
var sortedList = new SortedList<>(filteredList);
|
||||
sortedList.comparatorProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
Comparator<Path> tableComparator = table.comparatorProperty().get();
|
||||
return tableComparator != null ?
|
||||
FILE_TYPE_COMPARATOR.thenComparing(tableComparator) :
|
||||
FILE_TYPE_COMPARATOR;
|
||||
}, table.comparatorProperty()));
|
||||
table.setItems(sortedList);
|
||||
}
|
||||
|
||||
public void load(Stream<Path> stream) {
|
||||
list.setAll(stream.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
list.clear();
|
||||
}
|
||||
|
||||
public ObjectProperty<Predicate<Path>> predicateProperty() {
|
||||
return predicateProperty;
|
||||
}
|
||||
}
|
168
sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/FileManagerPage.java
Normal file
168
sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/FileManagerPage.java
Normal file
@ -0,0 +1,168 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.filemanager;
|
||||
|
||||
import atlantafx.base.controls.Breadcrumbs;
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import atlantafx.sampler.page.showcase.ShowcasePage;
|
||||
import atlantafx.sampler.util.Containers;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.Callback;
|
||||
import org.kordamp.ikonli.feather.Feather;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static atlantafx.base.theme.Styles.*;
|
||||
import static atlantafx.sampler.page.showcase.filemanager.FileList.PREDICATE_ANY;
|
||||
import static atlantafx.sampler.page.showcase.filemanager.FileList.PREDICATE_NOT_HIDDEN;
|
||||
import static atlantafx.sampler.page.showcase.filemanager.Utils.openFile;
|
||||
|
||||
public class FileManagerPage extends ShowcasePage {
|
||||
|
||||
public static final String NAME = "File Manager";
|
||||
|
||||
private static final String STYLESHEET_URL =
|
||||
Objects.requireNonNull(FileManagerPage.class.getResource("file-manager.css")).toExternalForm();
|
||||
|
||||
@Override
|
||||
public String getName() { return NAME; }
|
||||
|
||||
private final Model model = new Model();
|
||||
|
||||
public FileManagerPage() {
|
||||
super();
|
||||
createView();
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
var topBar = new ToolBar();
|
||||
|
||||
var backBtn = new Button("", new FontIcon(Feather.ARROW_LEFT));
|
||||
backBtn.getStyleClass().addAll(BUTTON_ICON, LEFT_PILL);
|
||||
backBtn.setOnAction(e -> model.back());
|
||||
backBtn.disableProperty().bind(model.getHistory().canGoBackProperty().not());
|
||||
|
||||
var forthBtn = new Button("", new FontIcon(Feather.ARROW_RIGHT));
|
||||
forthBtn.getStyleClass().addAll(BUTTON_ICON, RIGHT_PILL);
|
||||
forthBtn.setOnAction(e -> model.forth());
|
||||
forthBtn.disableProperty().bind(model.getHistory().canGoForthProperty().not());
|
||||
|
||||
var backForthBox = new HBox(backBtn, forthBtn);
|
||||
backForthBox.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
Breadcrumbs<Path> breadcrumbs = breadCrumbs();
|
||||
breadcrumbs.setOnCrumbAction(e -> model.navigate(e.getSelectedCrumb().getValue(), true));
|
||||
|
||||
var toggleHiddenCheck = new CheckMenuItem("Show Hidden Files");
|
||||
toggleHiddenCheck.setSelected(true);
|
||||
|
||||
var menuBtn = new MenuButton();
|
||||
menuBtn.setGraphic(new FontIcon(Feather.MORE_HORIZONTAL));
|
||||
menuBtn.getItems().setAll(
|
||||
toggleHiddenCheck
|
||||
);
|
||||
menuBtn.getStyleClass().add(BUTTON_ICON);
|
||||
|
||||
topBar.getItems().setAll(
|
||||
backForthBox,
|
||||
breadcrumbs,
|
||||
new Spacer(),
|
||||
menuBtn
|
||||
);
|
||||
|
||||
// ~
|
||||
|
||||
BookmarkList bookmarksList = new BookmarkList(model);
|
||||
|
||||
DirectoryView directoryView = new TableDirectoryView();
|
||||
directoryView.setDirectory(model.currentPathProperty().get());
|
||||
directoryView.setOnAction(path -> {
|
||||
if (Files.isDirectory(path)) {
|
||||
model.navigate(path, true);
|
||||
} else {
|
||||
openFile(path);
|
||||
}
|
||||
});
|
||||
|
||||
// ~
|
||||
|
||||
var splitPane = new SplitPane(bookmarksList, directoryView.getView());
|
||||
splitPane.setDividerPositions(0.2, 0.8);
|
||||
|
||||
var root = new BorderPane();
|
||||
root.getStyleClass().add("file-manager-showcase");
|
||||
root.getStylesheets().add(STYLESHEET_URL);
|
||||
root.setTop(topBar);
|
||||
root.setCenter(splitPane);
|
||||
|
||||
toggleHiddenCheck.selectedProperty().addListener((obs, old, val) -> directoryView.getFileList()
|
||||
.predicateProperty()
|
||||
.set(val ? PREDICATE_ANY : PREDICATE_NOT_HIDDEN)
|
||||
);
|
||||
|
||||
model.currentPathProperty().addListener((obs, old, val) -> {
|
||||
if (!Files.isReadable(val)) {
|
||||
showWarning("Access Denied", "You have no permission to enter \"" + val + "\"");
|
||||
return;
|
||||
}
|
||||
|
||||
breadcrumbs.setSelectedCrumb(
|
||||
Breadcrumbs.buildTreeModel(getParentPath(val, 5).toArray(Path[]::new))
|
||||
);
|
||||
directoryView.setDirectory(val);
|
||||
});
|
||||
|
||||
showcase.getChildren().setAll(root);
|
||||
Containers.setAnchors(root, Insets.EMPTY);
|
||||
}
|
||||
|
||||
private Breadcrumbs<Path> breadCrumbs() {
|
||||
Callback<TreeItem<Path>, Button> crumbFactory = crumb -> {
|
||||
var btn = new Button(crumb.getValue().getFileName().toString());
|
||||
btn.getStyleClass().add(Styles.FLAT);
|
||||
btn.setFocusTraversable(false);
|
||||
if (!crumb.getChildren().isEmpty()) {
|
||||
btn.setGraphic(new FontIcon(Feather.CHEVRON_RIGHT));
|
||||
}
|
||||
btn.setContentDisplay(ContentDisplay.RIGHT);
|
||||
btn.setStyle("-fx-padding: 6px 12px 6px 12px;");
|
||||
return btn;
|
||||
};
|
||||
|
||||
var breadcrumbs = new Breadcrumbs<Path>();
|
||||
breadcrumbs.setAutoNavigationEnabled(false);
|
||||
breadcrumbs.setCrumbFactory(crumbFactory);
|
||||
breadcrumbs.setSelectedCrumb(
|
||||
Breadcrumbs.buildTreeModel(getParentPath(model.currentPathProperty().get(), 5).toArray(Path[]::new))
|
||||
);
|
||||
breadcrumbs.setMaxWidth(Region.USE_PREF_SIZE);
|
||||
breadcrumbs.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
|
||||
return breadcrumbs;
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private List<Path> getParentPath(Path path, int limit) {
|
||||
var result = new ArrayList<Path>();
|
||||
|
||||
var cursor = path;
|
||||
while (result.size() < limit && cursor.getParent() != null) {
|
||||
result.add(cursor);
|
||||
cursor = cursor.getParent();
|
||||
}
|
||||
Collections.reverse(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.filemanager;
|
||||
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Objects;
|
||||
|
||||
final class Model {
|
||||
|
||||
public static final Path USER_HOME = Paths.get(System.getProperty("user.home"));
|
||||
|
||||
private final ReadOnlyObjectWrapper<Path> currentPath = new ReadOnlyObjectWrapper<>();
|
||||
private final NavigationHistory history = new NavigationHistory();
|
||||
|
||||
public Model() {
|
||||
currentPath.set(USER_HOME);
|
||||
history.append(USER_HOME);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Properties //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public ReadOnlyObjectProperty<Path> currentPathProperty() { return currentPath.getReadOnlyProperty(); }
|
||||
|
||||
public NavigationHistory getHistory() { return history; }
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Commands //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void back() {
|
||||
history.back().ifPresent(currentPath::set);
|
||||
}
|
||||
|
||||
public void forth() {
|
||||
history.forth().ifPresent(currentPath::set);
|
||||
}
|
||||
|
||||
public void navigate(Path path, boolean saveInHistory) {
|
||||
currentPath.set(Objects.requireNonNullElse(path, USER_HOME));
|
||||
if (saveInHistory) { history.append(path); }
|
||||
}
|
||||
}
|
50
sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/NavigationHistory.java
Normal file
50
sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/NavigationHistory.java
Normal file
@ -0,0 +1,50 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.filemanager;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
final class NavigationHistory {
|
||||
|
||||
private final IntegerProperty cursor = new SimpleIntegerProperty(0);
|
||||
private final List<Path> history = new ArrayList<>();
|
||||
private final BooleanBinding canGoBack = Bindings.createBooleanBinding(
|
||||
() -> cursor.get() > 0 && history.size() > 1, cursor);
|
||||
private final BooleanBinding canGoForth = Bindings.createBooleanBinding(
|
||||
() -> cursor.get() < history.size() - 1, cursor);
|
||||
|
||||
public void append(Path path) {
|
||||
if (path == null) { return; }
|
||||
var lastPath = history.size() > 0 ? history.get(history.size() - 1) : null;
|
||||
if (!Objects.equals(lastPath, path)) { history.add(path); }
|
||||
cursor.set(history.size() - 1);
|
||||
}
|
||||
|
||||
public Optional<Path> back() {
|
||||
if (!canGoBack.get()) { return Optional.empty(); }
|
||||
cursor.set(cursor.get() - 1);
|
||||
return Optional.of(history.get(cursor.get()));
|
||||
}
|
||||
|
||||
public Optional<Path> forth() {
|
||||
if (!canGoForth.get()) { return Optional.empty(); }
|
||||
cursor.set(cursor.get() + 1);
|
||||
return Optional.of(history.get(cursor.get()));
|
||||
}
|
||||
|
||||
public BooleanBinding canGoBackProperty() {
|
||||
return canGoBack;
|
||||
}
|
||||
|
||||
public BooleanBinding canGoForthProperty() {
|
||||
return canGoForth;
|
||||
}
|
||||
}
|
185
sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/TableDirectoryView.java
Normal file
185
sampler/src/main/java/atlantafx/sampler/page/showcase/filemanager/TableDirectoryView.java
Normal file
@ -0,0 +1,185 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.filemanager;
|
||||
|
||||
import atlantafx.sampler.util.Containers;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableRow;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
import org.kordamp.ikonli.material2.Material2AL;
|
||||
import org.kordamp.ikonli.material2.Material2OutlinedAL;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Comparator;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static atlantafx.sampler.page.showcase.filemanager.Utils.*;
|
||||
import static javafx.scene.control.TableColumn.SortType.ASCENDING;
|
||||
|
||||
final class TableDirectoryView extends AnchorPane implements DirectoryView {
|
||||
|
||||
private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden");
|
||||
private static final PseudoClass FOLDER = PseudoClass.getPseudoClass("folder");
|
||||
|
||||
private final FileList fileList;
|
||||
private Consumer<Path> actionHandler;
|
||||
|
||||
public TableDirectoryView() {
|
||||
TableView<Path> table = createTable();
|
||||
fileList = new FileList(table);
|
||||
|
||||
getChildren().setAll(table);
|
||||
getStyleClass().add("table-directory-view");
|
||||
Containers.setAnchors(table, Insets.EMPTY);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private TableView<Path> createTable() {
|
||||
var filenameCol = new TableColumn<Path, String>("Name");
|
||||
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
|
||||
param.getValue() != null ? param.getValue().getFileName().toString() : null
|
||||
));
|
||||
filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
|
||||
filenameCol.setSortType(ASCENDING);
|
||||
filenameCol.setCellFactory(col -> new FilenameCell());
|
||||
|
||||
var sizeCol = new TableColumn<Path, Number>("Size");
|
||||
sizeCol.setCellValueFactory(param -> new SimpleLongProperty(fileSize(param.getValue())));
|
||||
sizeCol.setCellFactory(col -> new FileSizeCell());
|
||||
|
||||
var mtimeCol = new TableColumn<Path, FileTime>("Modified");
|
||||
mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(fileMTime(param.getValue())));
|
||||
mtimeCol.setCellFactory(col -> new FileTimeCell());
|
||||
|
||||
// ~
|
||||
|
||||
var table = new TableView<Path>();
|
||||
table.getColumns().setAll(filenameCol, sizeCol, mtimeCol);
|
||||
table.getSortOrder().add(filenameCol);
|
||||
table.setSortPolicy(param -> true);
|
||||
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
filenameCol.minWidthProperty().bind(table.widthProperty().multiply(0.5));
|
||||
table.setRowFactory(param -> {
|
||||
TableRow<Path> row = new TableRow<>();
|
||||
|
||||
row.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() == 2 && !row.isEmpty() && actionHandler != null) {
|
||||
actionHandler.accept(row.getItem());
|
||||
}
|
||||
});
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pane getView() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDirectory(Path path) {
|
||||
if (path == null) {
|
||||
fileList.clear();
|
||||
} else {
|
||||
try (Stream<Path> stream = Files.list(path)) {
|
||||
fileList.load(stream);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnAction(Consumer<Path> actionHandler) {
|
||||
this.actionHandler = actionHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileList getFileList() {
|
||||
return fileList;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static class FilenameCell extends TableCell<Path, String> {
|
||||
|
||||
private final FontIcon icon = new FontIcon();
|
||||
|
||||
public FilenameCell() {
|
||||
setGraphicTextGap(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(String filename, boolean empty) {
|
||||
super.updateItem(filename, empty);
|
||||
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
|
||||
setGraphic(null);
|
||||
setText(null);
|
||||
} else {
|
||||
boolean isDirectory = Files.isDirectory(getTableRow().getItem());
|
||||
icon.setIconCode(isDirectory ? Material2AL.FOLDER : Material2OutlinedAL.INSERT_DRIVE_FILE);
|
||||
pseudoClassStateChanged(FOLDER, isDirectory);
|
||||
getTableRow().pseudoClassStateChanged(HIDDEN, isFileHidden(getTableRow().getItem()));
|
||||
setGraphic(icon);
|
||||
setText(filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileSizeCell extends TableCell<Path, Number> {
|
||||
|
||||
@Override
|
||||
protected void updateItem(Number fileSize, boolean empty) {
|
||||
super.updateItem(fileSize, empty);
|
||||
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
Path path = getTableRow().getItem();
|
||||
if (Files.isDirectory(path)) {
|
||||
if (Files.isReadable(path)) {
|
||||
try (Stream<Path> stream = Files.list(path)) {
|
||||
setText(stream.count() + " items");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
setText("unknown");
|
||||
}
|
||||
} else {
|
||||
setText(humanReadableByteCount(fileSize.longValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileTimeCell extends TableCell<Path, FileTime> {
|
||||
|
||||
@Override
|
||||
protected void updateItem(FileTime fileTime, boolean empty) {
|
||||
super.updateItem(fileTime, empty);
|
||||
if (empty) {
|
||||
setText(null);
|
||||
} else {
|
||||
setText(DateTimeFormatter.ISO_DATE.format(
|
||||
fileTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.filemanager;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.text.CharacterIterator;
|
||||
import java.text.StringCharacterIterator;
|
||||
|
||||
final class Utils {
|
||||
|
||||
public static long fileSize(Path path) {
|
||||
if (path == null) { return 0; }
|
||||
try {
|
||||
return Files.size(path);
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isFileHidden(Path path) {
|
||||
if (path == null) { return false; }
|
||||
try {
|
||||
return Files.isHidden(path);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static FileTime fileMTime(Path path) {
|
||||
if (path == null) { return null; }
|
||||
try {
|
||||
return Files.getLastModifiedTime(path);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String humanReadableByteCount(long bytes) {
|
||||
if (-1000 < bytes && bytes < 1000) { return bytes + " B"; }
|
||||
CharacterIterator ci = new StringCharacterIterator("kMGTPE");
|
||||
while (bytes <= -999_950 || bytes >= 999_950) {
|
||||
bytes /= 1000;
|
||||
ci.next();
|
||||
}
|
||||
return String.format("%.1f %cB", bytes / 1000.0, ci.current());
|
||||
}
|
||||
|
||||
public static void openFile(Path path) {
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Desktop.getDesktop().open(path.toFile());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
|
||||
.file-manager-showcase .bookmark-list {
|
||||
-fx-border-width: 0 0 1 1;
|
||||
}
|
||||
.file-manager-showcase .table-directory-view .table-view {
|
||||
-fx-border-width: 0 1 1 0;
|
||||
}
|
||||
|
||||
/* setting opacity directly to the .table-cell or .table-row-cell
|
||||
leads to incorrect table row height calculation #javafx-bug */
|
||||
.file-manager-showcase .table-row-cell:hidden >.table-cell > * {
|
||||
-fx-opacity: 0.6;
|
||||
}
|
||||
|
||||
.file-manager-showcase .table-row-cell >.table-cell:folder .ikonli-font-icon {
|
||||
-fx-icon-color: -color-accent-emphasis;
|
||||
-fx-fill: -color-accent-emphasis;
|
||||
}
|
||||
|
||||
.file-manager-showcase .bookmark-list .ikonli-font-icon {
|
||||
-fx-icon-size: 18px;
|
||||
}
|
||||
|
||||
.file-manager-showcase .table-directory-view .ikonli-font-icon {
|
||||
-fx-icon-size: 18px;
|
||||
}
|
@ -0,0 +1,583 @@
|
||||
/*
|
||||
* Java Color Thief
|
||||
* by Sven Woltmann, Fonpit AG
|
||||
* https://androidpit.com
|
||||
* https://androidpit.de
|
||||
*
|
||||
* Creative Commons Attribution 2.5 License:
|
||||
* http://creativecommons.org/licenses/by/2.5/
|
||||
*/
|
||||
package atlantafx.sampler.page.showcase.musicplayer;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferByte;
|
||||
import java.util.*;
|
||||
|
||||
import static java.awt.image.BufferedImage.TYPE_3BYTE_BGR;
|
||||
import static java.awt.image.BufferedImage.TYPE_4BYTE_ABGR;
|
||||
|
||||
final class ColorThief {
|
||||
|
||||
private static final int DEFAULT_QUALITY = 10;
|
||||
private static final boolean DEFAULT_IGNORE_WHITE = true;
|
||||
|
||||
public static int[] getColor(BufferedImage source) {
|
||||
int[][] palette = getPalette(source, 5);
|
||||
if (palette == null) { return null; }
|
||||
return palette[0];
|
||||
}
|
||||
|
||||
public static int[][] getPalette(BufferedImage source, int colorCount) {
|
||||
MMCQ.ColorMap colorMap = getColorMap(source, colorCount);
|
||||
if (colorMap == null) { return null; }
|
||||
return colorMap.palette();
|
||||
}
|
||||
|
||||
public static MMCQ.ColorMap getColorMap(BufferedImage source, int colorCount) {
|
||||
return getColorMap(source, colorCount, DEFAULT_QUALITY, DEFAULT_IGNORE_WHITE);
|
||||
}
|
||||
|
||||
public static MMCQ.ColorMap getColorMap(BufferedImage sourceImage, int colorCount, int quality,
|
||||
boolean ignoreWhite) {
|
||||
if (colorCount < 2 || colorCount > 256) {
|
||||
throw new IllegalArgumentException("Specified colorCount must be between 2 and 256.");
|
||||
}
|
||||
if (quality < 1) {
|
||||
throw new IllegalArgumentException("Specified quality should be greater then 0.");
|
||||
}
|
||||
|
||||
int[][] pixelArray = switch (sourceImage.getType()) {
|
||||
case TYPE_3BYTE_BGR, TYPE_4BYTE_ABGR -> getPixelsFast(sourceImage, quality, ignoreWhite);
|
||||
default -> getPixelsSlow(sourceImage, quality, ignoreWhite);
|
||||
};
|
||||
|
||||
return MMCQ.quantize(pixelArray, colorCount);
|
||||
}
|
||||
|
||||
private static int[][] getPixelsFast(
|
||||
BufferedImage sourceImage,
|
||||
int quality,
|
||||
boolean ignoreWhite) {
|
||||
DataBufferByte imageData = (DataBufferByte) sourceImage.getRaster().getDataBuffer();
|
||||
byte[] pixels = imageData.getData();
|
||||
int pixelCount = sourceImage.getWidth() * sourceImage.getHeight();
|
||||
|
||||
int colorDepth;
|
||||
int type = sourceImage.getType();
|
||||
colorDepth = switch (type) {
|
||||
case TYPE_3BYTE_BGR -> 3;
|
||||
case TYPE_4BYTE_ABGR -> 4;
|
||||
default -> throw new IllegalArgumentException("Unhandled type: " + type);
|
||||
};
|
||||
|
||||
int expectedDataLength = pixelCount * colorDepth;
|
||||
if (expectedDataLength != pixels.length) {
|
||||
throw new IllegalArgumentException(
|
||||
"(expectedDataLength = " + expectedDataLength + ") != (pixels.length = " + pixels.length + ")"
|
||||
);
|
||||
}
|
||||
|
||||
int numRegardedPixels = (pixelCount + quality - 1) / quality;
|
||||
|
||||
int numUsedPixels = 0;
|
||||
int[][] pixelArray = new int[numRegardedPixels][];
|
||||
int offset, r, g, b, a;
|
||||
|
||||
switch (type) {
|
||||
case TYPE_3BYTE_BGR:
|
||||
for (int i = 0; i < pixelCount; i += quality) {
|
||||
offset = i * 3;
|
||||
b = pixels[offset] & 0xFF;
|
||||
g = pixels[offset + 1] & 0xFF;
|
||||
r = pixels[offset + 2] & 0xFF;
|
||||
|
||||
if (!(ignoreWhite && r > 250 && g > 250 && b > 250)) {
|
||||
pixelArray[numUsedPixels] = new int[] { r, g, b };
|
||||
numUsedPixels++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case TYPE_4BYTE_ABGR:
|
||||
for (int i = 0; i < pixelCount; i += quality) {
|
||||
offset = i * 4;
|
||||
a = pixels[offset] & 0xFF;
|
||||
b = pixels[offset + 1] & 0xFF;
|
||||
g = pixels[offset + 2] & 0xFF;
|
||||
r = pixels[offset + 3] & 0xFF;
|
||||
|
||||
if (a >= 125 && !(ignoreWhite && r > 250 && g > 250 && b > 250)) {
|
||||
pixelArray[numUsedPixels] = new int[] { r, g, b };
|
||||
numUsedPixels++;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalArgumentException("Unhandled type: " + type);
|
||||
}
|
||||
|
||||
return Arrays.copyOfRange(pixelArray, 0, numUsedPixels);
|
||||
}
|
||||
|
||||
private static int[][] getPixelsSlow(
|
||||
BufferedImage sourceImage,
|
||||
int quality,
|
||||
boolean ignoreWhite) {
|
||||
int width = sourceImage.getWidth();
|
||||
int height = sourceImage.getHeight();
|
||||
|
||||
int pixelCount = width * height;
|
||||
int numRegardedPixels = (pixelCount + quality - 1) / quality;
|
||||
int numUsedPixels = 0;
|
||||
|
||||
int[][] res = new int[numRegardedPixels][];
|
||||
int r, g, b;
|
||||
|
||||
for (int i = 0; i < pixelCount; i += quality) {
|
||||
int row = i / width;
|
||||
int col = i % width;
|
||||
int rgb = sourceImage.getRGB(col, row);
|
||||
|
||||
r = (rgb >> 16) & 0xFF;
|
||||
g = (rgb >> 8) & 0xFF;
|
||||
b = (rgb) & 0xFF;
|
||||
if (!(ignoreWhite && r > 250 && g > 250 && b > 250)) {
|
||||
res[numUsedPixels] = new int[] { r, g, b };
|
||||
numUsedPixels++;
|
||||
}
|
||||
}
|
||||
|
||||
return Arrays.copyOfRange(res, 0, numUsedPixels);
|
||||
}
|
||||
|
||||
private static class MMCQ {
|
||||
|
||||
private static final int SIGBITS = 5;
|
||||
private static final int RSHIFT = 8 - SIGBITS;
|
||||
private static final int MULT = 1 << RSHIFT;
|
||||
private static final int HISTOSIZE = 1 << (3 * SIGBITS);
|
||||
private static final int VBOX_LENGTH = 1 << SIGBITS;
|
||||
private static final double FRACT_BY_POPULATION = 0.75;
|
||||
private static final int MAX_ITERATIONS = 1000;
|
||||
|
||||
static int getColorIndex(int r, int g, int b) {
|
||||
return (r << (2 * SIGBITS)) + (g << SIGBITS) + b;
|
||||
}
|
||||
|
||||
public static class VBox {
|
||||
int r1;
|
||||
int r2;
|
||||
int g1;
|
||||
int g2;
|
||||
int b1;
|
||||
int b2;
|
||||
|
||||
private final int[] histo;
|
||||
|
||||
private int[] _avg;
|
||||
private Integer _volume;
|
||||
private Integer _count;
|
||||
|
||||
public VBox(int r1, int r2, int g1, int g2, int b1, int b2, int[] histo) {
|
||||
this.r1 = r1;
|
||||
this.r2 = r2;
|
||||
this.g1 = g1;
|
||||
this.g2 = g2;
|
||||
this.b1 = b1;
|
||||
this.b2 = b2;
|
||||
this.histo = histo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "r1: " + r1 + " / r2: " + r2 + " / g1: " + g1 + " / g2: " + g2 + " / b1: " + b1 + " / b2: " + b2;
|
||||
}
|
||||
|
||||
public int volume(boolean force) {
|
||||
if (_volume == null || force) {
|
||||
_volume = ((r2 - r1 + 1) * (g2 - g1 + 1) * (b2 - b1 + 1));
|
||||
}
|
||||
return _volume;
|
||||
}
|
||||
|
||||
public int count(boolean force) {
|
||||
if (_count == null || force) {
|
||||
int npix = 0;
|
||||
int i, j, k, index;
|
||||
|
||||
for (i = r1; i <= r2; i++) {
|
||||
for (j = g1; j <= g2; j++) {
|
||||
for (k = b1; k <= b2; k++) {
|
||||
index = getColorIndex(i, j, k);
|
||||
npix += histo[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_count = npix;
|
||||
}
|
||||
|
||||
return _count;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("MethodDoesntCallSuperMethod")
|
||||
public VBox clone() {
|
||||
return new VBox(r1, r2, g1, g2, b1, b2, histo);
|
||||
}
|
||||
|
||||
public int[] avg(boolean force) {
|
||||
if (_avg == null || force) {
|
||||
int ntot = 0;
|
||||
int rsum = 0;
|
||||
int gsum = 0;
|
||||
int bsum = 0;
|
||||
int hval, i, j, k, histoindex;
|
||||
|
||||
for (i = r1; i <= r2; i++) {
|
||||
for (j = g1; j <= g2; j++) {
|
||||
for (k = b1; k <= b2; k++) {
|
||||
histoindex = getColorIndex(i, j, k);
|
||||
hval = histo[histoindex];
|
||||
ntot += hval;
|
||||
rsum += (hval * (i + 0.5) * MULT);
|
||||
gsum += (hval * (j + 0.5) * MULT);
|
||||
bsum += (hval * (k + 0.5) * MULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ntot > 0) {
|
||||
_avg = new int[] { (rsum / ntot), (gsum / ntot), (bsum / ntot) };
|
||||
} else {
|
||||
_avg = new int[] { (MULT * (r1 + r2 + 1) / 2), (MULT * (g1 + g2 + 1) / 2), (MULT * (b1 + b2 + 1) / 2) };
|
||||
}
|
||||
}
|
||||
|
||||
return _avg;
|
||||
}
|
||||
|
||||
public boolean contains(int[] pixel) {
|
||||
int rval = pixel[0] >> RSHIFT;
|
||||
int gval = pixel[1] >> RSHIFT;
|
||||
int bval = pixel[2] >> RSHIFT;
|
||||
return (rval >= r1 && rval <= r2 && gval >= g1 && gval <= g2 && bval >= b1 && bval <= b2);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ColorMap {
|
||||
|
||||
public final ArrayList<VBox> vboxes = new ArrayList<>();
|
||||
|
||||
public void push(VBox box) {
|
||||
vboxes.add(box);
|
||||
}
|
||||
|
||||
public int[][] palette() {
|
||||
int numVBoxes = vboxes.size();
|
||||
int[][] palette = new int[numVBoxes][];
|
||||
for (int i = 0; i < numVBoxes; i++) {
|
||||
palette[i] = vboxes.get(i).avg(false);
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return vboxes.size();
|
||||
}
|
||||
|
||||
public int[] map(int[] color) {
|
||||
int numVBoxes = vboxes.size();
|
||||
for (VBox vbox : vboxes) {
|
||||
if (vbox.contains(color)) {
|
||||
return vbox.avg(false);
|
||||
}
|
||||
}
|
||||
return nearest(color);
|
||||
}
|
||||
|
||||
public int[] nearest(int[] color) {
|
||||
double d1 = Double.MAX_VALUE;
|
||||
double d2;
|
||||
int[] pColor = null;
|
||||
|
||||
int numVBoxes = vboxes.size();
|
||||
for (VBox vbox : vboxes) {
|
||||
int[] vbColor = vbox.avg(false);
|
||||
d2 = Math.sqrt(Math.pow(color[0] - vbColor[0], 2) +
|
||||
Math.pow(color[1] - vbColor[1], 2) +
|
||||
Math.pow(color[2] - vbColor[2], 2)
|
||||
);
|
||||
if (d2 < d1) {
|
||||
d1 = d2;
|
||||
pColor = vbColor;
|
||||
}
|
||||
}
|
||||
return pColor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static int[] getHisto(int[][] pixels) {
|
||||
int[] histo = new int[HISTOSIZE];
|
||||
int index, rval, gval, bval;
|
||||
int numPixels = pixels.length;
|
||||
|
||||
for (int[] pixel : pixels) {
|
||||
rval = pixel[0] >> RSHIFT;
|
||||
gval = pixel[1] >> RSHIFT;
|
||||
bval = pixel[2] >> RSHIFT;
|
||||
index = getColorIndex(rval, gval, bval);
|
||||
histo[index]++;
|
||||
}
|
||||
return histo;
|
||||
}
|
||||
|
||||
private static VBox vboxFromPixels(int[][] pixels, int[] histo) {
|
||||
int rmin = 1000000, rmax = 0;
|
||||
int gmin = 1000000, gmax = 0;
|
||||
int bmin = 1000000, bmax = 0;
|
||||
|
||||
int rval, gval, bval;
|
||||
|
||||
int numPixels = pixels.length;
|
||||
for (int[] pixel : pixels) {
|
||||
rval = pixel[0] >> RSHIFT;
|
||||
gval = pixel[1] >> RSHIFT;
|
||||
bval = pixel[2] >> RSHIFT;
|
||||
|
||||
if (rval < rmin) {
|
||||
rmin = rval;
|
||||
} else if (rval > rmax) {
|
||||
rmax = rval;
|
||||
}
|
||||
|
||||
if (gval < gmin) {
|
||||
gmin = gval;
|
||||
} else if (gval > gmax) {
|
||||
gmax = gval;
|
||||
}
|
||||
|
||||
if (bval < bmin) {
|
||||
bmin = bval;
|
||||
} else if (bval > bmax) {
|
||||
bmax = bval;
|
||||
}
|
||||
}
|
||||
|
||||
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
|
||||
}
|
||||
|
||||
private static VBox[] medianCutApply(int[] histo, VBox vbox) {
|
||||
if (vbox.count(false) == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (vbox.count(false) == 1) {
|
||||
return new VBox[] { vbox.clone(), null };
|
||||
}
|
||||
|
||||
int rw = vbox.r2 - vbox.r1 + 1;
|
||||
int gw = vbox.g2 - vbox.g1 + 1;
|
||||
int bw = vbox.b2 - vbox.b1 + 1;
|
||||
int maxw = Math.max(Math.max(rw, gw), bw);
|
||||
|
||||
int total = 0;
|
||||
int[] partialSum = new int[VBOX_LENGTH];
|
||||
Arrays.fill(partialSum, -1);
|
||||
int[] lookAheadSum = new int[VBOX_LENGTH];
|
||||
Arrays.fill(lookAheadSum, -1);
|
||||
int i, j, k, sum, index;
|
||||
|
||||
if (maxw == rw) {
|
||||
for (i = vbox.r1; i <= vbox.r2; i++) {
|
||||
sum = 0;
|
||||
for (j = vbox.g1; j <= vbox.g2; j++) {
|
||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
||||
index = getColorIndex(i, j, k);
|
||||
sum += histo[index];
|
||||
}
|
||||
}
|
||||
total += sum;
|
||||
partialSum[i] = total;
|
||||
}
|
||||
} else if (maxw == gw) {
|
||||
for (i = vbox.g1; i <= vbox.g2; i++) {
|
||||
sum = 0;
|
||||
for (j = vbox.r1; j <= vbox.r2; j++) {
|
||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
||||
index = getColorIndex(j, i, k);
|
||||
sum += histo[index];
|
||||
}
|
||||
}
|
||||
total += sum;
|
||||
partialSum[i] = total;
|
||||
}
|
||||
} else {
|
||||
for (i = vbox.b1; i <= vbox.b2; i++) {
|
||||
sum = 0;
|
||||
for (j = vbox.r1; j <= vbox.r2; j++) {
|
||||
for (k = vbox.g1; k <= vbox.g2; k++) {
|
||||
index = getColorIndex(j, k, i);
|
||||
sum += histo[index];
|
||||
}
|
||||
}
|
||||
total += sum;
|
||||
partialSum[i] = total;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 0; i < VBOX_LENGTH; i++) {
|
||||
if (partialSum[i] != -1) {
|
||||
lookAheadSum[i] = total - partialSum[i];
|
||||
}
|
||||
}
|
||||
|
||||
return maxw == rw ? doCut('r', vbox, partialSum, lookAheadSum, total)
|
||||
: maxw == gw ? doCut('g', vbox, partialSum, lookAheadSum, total)
|
||||
: doCut('b', vbox, partialSum, lookAheadSum, total);
|
||||
}
|
||||
|
||||
private static VBox[] doCut(
|
||||
char color,
|
||||
VBox vbox,
|
||||
int[] partialSum,
|
||||
int[] lookAheadSum,
|
||||
int total
|
||||
) {
|
||||
int vbox_dim1;
|
||||
int vbox_dim2;
|
||||
|
||||
if (color == 'r') {
|
||||
vbox_dim1 = vbox.r1;
|
||||
vbox_dim2 = vbox.r2;
|
||||
} else if (color == 'g') {
|
||||
vbox_dim1 = vbox.g1;
|
||||
vbox_dim2 = vbox.g2;
|
||||
} else {
|
||||
vbox_dim1 = vbox.b1;
|
||||
vbox_dim2 = vbox.b2;
|
||||
}
|
||||
|
||||
int left, right;
|
||||
VBox vbox1, vbox2;
|
||||
int d2, count2;
|
||||
|
||||
for (int i = vbox_dim1; i <= vbox_dim2; i++) {
|
||||
if (partialSum[i] > total / 2) {
|
||||
vbox1 = vbox.clone();
|
||||
vbox2 = vbox.clone();
|
||||
|
||||
left = i - vbox_dim1;
|
||||
right = vbox_dim2 - i;
|
||||
|
||||
if (left <= right) {
|
||||
d2 = Math.min(vbox_dim2 - 1, (i + right / 2));
|
||||
} else {
|
||||
d2 = Math.max(vbox_dim1, ((int) (i - 1 - left / 2.0)));
|
||||
}
|
||||
|
||||
while (d2 < 0 || partialSum[d2] <= 0) {
|
||||
d2++;
|
||||
}
|
||||
count2 = lookAheadSum[d2];
|
||||
while (count2 == 0 && d2 > 0 && partialSum[d2 - 1] > 0) {
|
||||
count2 = lookAheadSum[--d2];
|
||||
}
|
||||
|
||||
if (color == 'r') {
|
||||
vbox1.r2 = d2;
|
||||
vbox2.r1 = d2 + 1;
|
||||
} else if (color == 'g') {
|
||||
vbox1.g2 = d2;
|
||||
vbox2.g1 = d2 + 1;
|
||||
} else {
|
||||
vbox1.b2 = d2;
|
||||
vbox2.b1 = d2 + 1;
|
||||
}
|
||||
|
||||
return new VBox[] { vbox1, vbox2 };
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("VBox can't be cut");
|
||||
}
|
||||
|
||||
public static ColorMap quantize(int[][] pixels, int maxcolors) {
|
||||
if (pixels.length == 0 || maxcolors < 2 || maxcolors > 256) { return null; }
|
||||
|
||||
int[] histo = getHisto(pixels);
|
||||
|
||||
VBox vbox = vboxFromPixels(pixels, histo);
|
||||
ArrayList<VBox> pq = new ArrayList<>();
|
||||
pq.add(vbox);
|
||||
|
||||
int target = (int) Math.ceil(FRACT_BY_POPULATION * maxcolors);
|
||||
|
||||
iter(pq, COMPARATOR_COUNT, target, histo);
|
||||
pq.sort(COMPARATOR_PRODUCT);
|
||||
|
||||
if (maxcolors > pq.size()) {
|
||||
iter(pq, COMPARATOR_PRODUCT, maxcolors, histo);
|
||||
}
|
||||
|
||||
Collections.reverse(pq);
|
||||
|
||||
ColorMap colorMap = new ColorMap();
|
||||
for (VBox vb : pq) {
|
||||
colorMap.push(vb);
|
||||
}
|
||||
|
||||
return colorMap;
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private static void iter(List<VBox> lh, Comparator<VBox> comparator, int target, int[] histo) {
|
||||
int niters = 0;
|
||||
VBox vbox;
|
||||
|
||||
while (niters < MAX_ITERATIONS) {
|
||||
vbox = lh.get(lh.size() - 1);
|
||||
if (vbox.count(false) == 0) {
|
||||
lh.sort(comparator);
|
||||
niters++;
|
||||
continue;
|
||||
}
|
||||
lh.remove(lh.size() - 1);
|
||||
|
||||
VBox[] vboxes = medianCutApply(histo, vbox);
|
||||
VBox vbox1 = vboxes[0];
|
||||
VBox vbox2 = vboxes[1];
|
||||
|
||||
if (vbox1 == null) {
|
||||
throw new RuntimeException("vbox1 not defined; shouldn't happen!");
|
||||
}
|
||||
|
||||
lh.add(vbox1);
|
||||
if (vbox2 != null) {
|
||||
lh.add(vbox2);
|
||||
}
|
||||
lh.sort(comparator);
|
||||
|
||||
if (lh.size() >= target) {
|
||||
return;
|
||||
}
|
||||
if (niters++ > MAX_ITERATIONS) { return; }
|
||||
}
|
||||
}
|
||||
|
||||
private static final Comparator<VBox> COMPARATOR_COUNT = Comparator.comparingInt(a -> a.count(false));
|
||||
|
||||
private static final Comparator<VBox> COMPARATOR_PRODUCT = (a, b) -> {
|
||||
int aCount = a.count(false);
|
||||
int bCount = b.count(false);
|
||||
int aVolume = a.volume(false);
|
||||
int bVolume = b.volume(false);
|
||||
|
||||
if (aCount == bCount) { return aVolume - bVolume; }
|
||||
|
||||
return Long.compare((long) aCount * aVolume, (long) bCount * bVolume);
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.musicplayer;
|
||||
|
||||
import atlantafx.sampler.Resources;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static atlantafx.sampler.page.showcase.musicplayer.MediaFile.Metadata.*;
|
||||
import static atlantafx.sampler.page.showcase.musicplayer.Utils.copyImage;
|
||||
|
||||
@SuppressWarnings("StringOperationCanBeSimplified")
|
||||
record MediaFile(File file) {
|
||||
|
||||
private static final Map<String, Metadata> METADATA_CACHE = new HashMap<>();
|
||||
|
||||
// Sadly JavaFX Media API is not user-friendly. If you want to obtain any
|
||||
// media file metadata you have to load it to media player instance, which
|
||||
// is costly and that instance is not even reusable.
|
||||
public void readMetadata(Consumer<Metadata> callback) {
|
||||
var media = new Media(file.toURI().toString());
|
||||
var mediaPlayer = new MediaPlayer(media);
|
||||
|
||||
// The media information is obtained asynchronously and so not necessarily
|
||||
// available immediately after instantiation of the class. All information
|
||||
// should however be available if the instance has been associated with a
|
||||
// MediaPlayer and that player has transitioned to Status.READY status.
|
||||
mediaPlayer.setOnReady(() -> {
|
||||
Map<String, Object> metadata = media.getMetadata();
|
||||
callback.accept(METADATA_CACHE.computeIfAbsent(file.getAbsolutePath(), k ->
|
||||
// clone everything to make sure media player will be garbage collected
|
||||
new Metadata(
|
||||
new String(getTag(metadata, "title", String.class, NO_TITLE)),
|
||||
copyImage(getTag(metadata, "image", Image.class, NO_IMAGE)),
|
||||
new String(getTag(metadata, "artist", String.class, NO_ARTIST)),
|
||||
new String(getTag(metadata, "album", String.class, NO_ALBUM)),
|
||||
media.getDuration().toMillis()
|
||||
))
|
||||
);
|
||||
|
||||
mediaPlayer.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
public Media createMedia() {
|
||||
return new Media(file.toURI().toString());
|
||||
}
|
||||
|
||||
private <T> T getTag(Map<String, Object> metadata, String key, Class<T> type, T defaultValue) {
|
||||
Object tag = metadata.get(key);
|
||||
return type.isInstance(tag) ? type.cast(tag) : defaultValue;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
record Metadata(String title, Image image, String artist, String album, double duration) {
|
||||
|
||||
static final Image NO_IMAGE = new Image(
|
||||
Resources.getResourceAsStream("images/no-image.png"), 150, 150, true, false
|
||||
);
|
||||
static final String NO_TITLE = "Unknown title";
|
||||
static final String NO_ARTIST = "Unknown artist";
|
||||
static final String NO_ALBUM = "Unknown album";
|
||||
}
|
||||
}
|
@ -0,0 +1,89 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.musicplayer;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
class Model {
|
||||
|
||||
private final ObservableList<MediaFile> playlist = FXCollections.observableArrayList();
|
||||
private final IntegerProperty playlistIndex = new SimpleIntegerProperty();
|
||||
private final ReadOnlyBooleanWrapper canGoBack = new ReadOnlyBooleanWrapper();
|
||||
private final ReadOnlyBooleanWrapper canGoForward = new ReadOnlyBooleanWrapper();
|
||||
private final ReadOnlyObjectWrapper<MediaFile> currentTrack = new ReadOnlyObjectWrapper<>();
|
||||
private final ReadOnlyObjectWrapper<Color> backgroundColor = new ReadOnlyObjectWrapper<>(Color.TRANSPARENT);
|
||||
|
||||
public Model() {
|
||||
canGoBack.bind(Bindings.createBooleanBinding(
|
||||
() -> playlist.size() > 1 && getPlaylistPosition() > 0, currentTrack)
|
||||
);
|
||||
canGoForward.bind(Bindings.createBooleanBinding(
|
||||
() -> playlist.size() > 0 && getPlaylistPosition() < playlist.size() - 1, currentTrack));
|
||||
}
|
||||
|
||||
private int getPlaylistPosition() {
|
||||
if (currentTrack.get() == null) { return -1; }
|
||||
return playlist.indexOf(currentTrack.get());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Properties //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public ObservableList<MediaFile> playlist() { return playlist; }
|
||||
|
||||
public ReadOnlyBooleanProperty canGoBackProperty() { return canGoBack.getReadOnlyProperty(); }
|
||||
|
||||
public ReadOnlyBooleanProperty canGoForwardProperty() { return canGoForward.getReadOnlyProperty(); }
|
||||
|
||||
public ReadOnlyObjectProperty<MediaFile> currentTrackProperty() { return currentTrack.getReadOnlyProperty(); }
|
||||
|
||||
public ReadOnlyObjectProperty<Color> backgroundColorProperty() { return backgroundColor.getReadOnlyProperty(); }
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// Commands //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void play(MediaFile mediaFile) {
|
||||
currentTrack.set(Objects.requireNonNull(mediaFile));
|
||||
}
|
||||
|
||||
public void playPrevious() {
|
||||
if (canGoBack.get()) { currentTrack.set(playlist.get(getPlaylistPosition() - 1)); }
|
||||
}
|
||||
|
||||
public void playNext() {
|
||||
if (canGoForward.get()) { currentTrack.set(playlist.get(getPlaylistPosition() + 1)); }
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
currentTrack.set(null);
|
||||
setBackgroundColor(null);
|
||||
}
|
||||
|
||||
public void setBackgroundColor(Color color) {
|
||||
backgroundColor.set(Objects.requireNonNullElse(color, Color.TRANSPARENT));
|
||||
}
|
||||
|
||||
public void shuffle() {
|
||||
FXCollections.shuffle(playlist);
|
||||
}
|
||||
|
||||
public void addFile(MediaFile mediaFile) {
|
||||
playlist.add(Objects.requireNonNull(mediaFile));
|
||||
}
|
||||
|
||||
public void removeFile(MediaFile mediaFile) {
|
||||
playlist.remove(Objects.requireNonNull(mediaFile));
|
||||
}
|
||||
|
||||
public void removeAll() {
|
||||
reset();
|
||||
playlist().clear();
|
||||
}
|
||||
}
|
78
sampler/src/main/java/atlantafx/sampler/page/showcase/musicplayer/MusicPlayerPage.java
Normal file
78
sampler/src/main/java/atlantafx/sampler/page/showcase/musicplayer/MusicPlayerPage.java
Normal file
@ -0,0 +1,78 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.musicplayer;
|
||||
|
||||
import atlantafx.sampler.Resources;
|
||||
import atlantafx.sampler.page.showcase.ShowcasePage;
|
||||
import atlantafx.sampler.util.Containers;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.SplitPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class MusicPlayerPage extends ShowcasePage {
|
||||
|
||||
public static final String NAME = "Music Player";
|
||||
public static final double BACKGROUND_OPACITY = 0.1;
|
||||
private static final String STYLESHEET_URL = Objects.requireNonNull(
|
||||
MusicPlayerPage.class.getResource("music-player.css")).toExternalForm();
|
||||
private static final Image PLUG_IMAGE = new Image(Resources.getResourceAsStream("images/vinyl.jpg"));
|
||||
|
||||
@Override
|
||||
public String getName() { return NAME; }
|
||||
|
||||
private final Model model = new Model();
|
||||
|
||||
public MusicPlayerPage() {
|
||||
super();
|
||||
createView();
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
var player = new Player(model);
|
||||
player.setVisible(false);
|
||||
|
||||
BackgroundImage backgroundImage = new BackgroundImage(
|
||||
PLUG_IMAGE,
|
||||
BackgroundRepeat.REPEAT,
|
||||
BackgroundRepeat.REPEAT,
|
||||
BackgroundPosition.DEFAULT,
|
||||
BackgroundSize.DEFAULT
|
||||
);
|
||||
var plug = new AnchorPane();
|
||||
plug.setBackground(new Background(backgroundImage));
|
||||
plug.setOpacity(0.5);
|
||||
plug.setMouseTransparent(false);
|
||||
|
||||
var playerStack = new StackPane(player, plug);
|
||||
model.playlist().addListener((ListChangeListener<MediaFile>) c -> {
|
||||
if (model.playlist().size() > 0) {
|
||||
player.setVisible(true);
|
||||
plug.setVisible(false);
|
||||
player.toFront();
|
||||
} else {
|
||||
player.setVisible(false);
|
||||
plug.setVisible(true);
|
||||
plug.toFront();
|
||||
}
|
||||
});
|
||||
|
||||
var playlist = new Playlist(model);
|
||||
|
||||
var root = new SplitPane();
|
||||
root.getStylesheets().add(STYLESHEET_URL);
|
||||
root.getStyleClass().add("music-player-showcase");
|
||||
root.getItems().setAll(playerStack, playlist);
|
||||
|
||||
showcase.getChildren().setAll(root);
|
||||
Containers.setAnchors(root, Insets.EMPTY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
model.reset();
|
||||
}
|
||||
}
|
@ -0,0 +1,262 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.musicplayer;
|
||||
|
||||
import atlantafx.base.controls.Popover;
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.ImagePattern;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.util.Duration;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import static atlantafx.base.controls.Popover.ArrowLocation;
|
||||
import static atlantafx.base.theme.Styles.*;
|
||||
import static atlantafx.sampler.page.showcase.musicplayer.MediaFile.Metadata.*;
|
||||
import static atlantafx.sampler.page.showcase.musicplayer.MusicPlayerPage.BACKGROUND_OPACITY;
|
||||
import static atlantafx.sampler.page.showcase.musicplayer.Utils.formatDuration;
|
||||
import static atlantafx.sampler.page.showcase.musicplayer.Utils.getDominantColor;
|
||||
import static javafx.geometry.Orientation.VERTICAL;
|
||||
import static javafx.geometry.Pos.CENTER;
|
||||
import static org.kordamp.ikonli.material2.Material2AL.CLEAR_ALL;
|
||||
import static org.kordamp.ikonli.material2.Material2AL.EQUALS;
|
||||
import static org.kordamp.ikonli.material2.Material2MZ.*;
|
||||
import static org.kordamp.ikonli.material2.Material2OutlinedAL.FAST_FORWARD;
|
||||
import static org.kordamp.ikonli.material2.Material2OutlinedAL.FAST_REWIND;
|
||||
|
||||
final class Player extends VBox {
|
||||
|
||||
private static final int PANEL_MAX_WIDTH = 220;
|
||||
|
||||
private final ObjectProperty<MediaPlayer> currentPlayer = new SimpleObjectProperty<>();
|
||||
|
||||
public Player(Model model) {
|
||||
Rectangle coverImage = new Rectangle(0, 0, 150, 150);
|
||||
coverImage.setArcWidth(20.0);
|
||||
coverImage.setArcHeight(20.0);
|
||||
coverImage.setFill(new ImagePattern(NO_IMAGE));
|
||||
|
||||
var trackTitle = new Label(NO_TITLE);
|
||||
trackTitle.setAlignment(CENTER);
|
||||
trackTitle.setMaxWidth(Double.MAX_VALUE);
|
||||
trackTitle.getStyleClass().add(TITLE_3);
|
||||
|
||||
var trackArtist = new Label(NO_ARTIST);
|
||||
trackArtist.setAlignment(CENTER);
|
||||
trackArtist.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
var trackAlbum = new Label(NO_ALBUM);
|
||||
trackAlbum.setAlignment(CENTER);
|
||||
trackAlbum.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
// == Media controls ==
|
||||
|
||||
var prevBtn = new Button("", new FontIcon(FAST_REWIND));
|
||||
prevBtn.getStyleClass().addAll(BUTTON_CIRCLE);
|
||||
prevBtn.setShape(new Circle(50));
|
||||
prevBtn.setTooltip(new Tooltip("Previous"));
|
||||
prevBtn.disableProperty().bind(model.canGoBackProperty().not());
|
||||
prevBtn.setOnAction(e -> model.playPrevious());
|
||||
|
||||
var playIcon = new FontIcon(PLAY_ARROW);
|
||||
playIcon.setIconSize(32);
|
||||
|
||||
var playBtn = new Button("", playIcon);
|
||||
playBtn.getStyleClass().addAll(BUTTON_CIRCLE);
|
||||
playBtn.setShape(new Circle(50));
|
||||
|
||||
var nextBtn = new Button("", new FontIcon(FAST_FORWARD));
|
||||
nextBtn.getStyleClass().addAll(BUTTON_CIRCLE);
|
||||
nextBtn.setShape(new Circle(50));
|
||||
nextBtn.disableProperty().bind(model.canGoForwardProperty().not());
|
||||
nextBtn.setOnAction(e -> model.playNext());
|
||||
nextBtn.setTooltip(new Tooltip("Next"));
|
||||
|
||||
var mediaControls = new HBox(20);
|
||||
mediaControls.getChildren().setAll(prevBtn, playBtn, nextBtn);
|
||||
mediaControls.setAlignment(CENTER);
|
||||
|
||||
// == Time controls ==
|
||||
|
||||
var timeSlider = new Slider(0, 1, 0);
|
||||
timeSlider.setMaxWidth(Double.MAX_VALUE);
|
||||
timeSlider.setMinWidth(PANEL_MAX_WIDTH);
|
||||
timeSlider.setMaxWidth(PANEL_MAX_WIDTH);
|
||||
|
||||
var currentTimeLabel = new Label("0.0");
|
||||
currentTimeLabel.getStyleClass().add(TEXT_SMALL);
|
||||
|
||||
var endTimeLabel = new Label("5.0");
|
||||
endTimeLabel.getStyleClass().add(TEXT_SMALL);
|
||||
|
||||
var timeMarkersBox = new HBox(5);
|
||||
timeMarkersBox.getChildren().setAll(currentTimeLabel, new Spacer(), endTimeLabel);
|
||||
timeMarkersBox.setMaxWidth(PANEL_MAX_WIDTH);
|
||||
|
||||
// == Extra controls ==
|
||||
|
||||
var clearPlaylistBtn = new Button("", new FontIcon(CLEAR_ALL));
|
||||
clearPlaylistBtn.getStyleClass().addAll(BUTTON_CIRCLE);
|
||||
clearPlaylistBtn.setShape(new Circle(50));
|
||||
clearPlaylistBtn.setTooltip(new Tooltip("Clear"));
|
||||
clearPlaylistBtn.setOnAction(e -> model.removeAll());
|
||||
|
||||
var shuffleBtn = new Button("", new FontIcon(SHUFFLE));
|
||||
shuffleBtn.getStyleClass().addAll(BUTTON_CIRCLE);
|
||||
shuffleBtn.setShape(new Circle(50));
|
||||
shuffleBtn.setTooltip(new Tooltip("Shuffle"));
|
||||
shuffleBtn.setOnAction(e -> model.shuffle());
|
||||
|
||||
var volumeSlider = new Slider(0, 1, 0.75);
|
||||
volumeSlider.setOrientation(VERTICAL);
|
||||
|
||||
var volumeBar = new VBox(5);
|
||||
volumeBar.getChildren().setAll(new FontIcon(VOLUME_UP), volumeSlider, new FontIcon(VOLUME_OFF));
|
||||
volumeBar.setAlignment(CENTER);
|
||||
|
||||
var volumePopover = new Popover(volumeBar);
|
||||
volumePopover.setHeaderAlwaysVisible(false);
|
||||
volumePopover.setArrowLocation(ArrowLocation.TOP_LEFT);
|
||||
|
||||
var volumeBtn = new Button("", new FontIcon(VOLUME_UP));
|
||||
volumeBtn.getStyleClass().addAll(BUTTON_CIRCLE);
|
||||
volumeBtn.setShape(new Circle(50));
|
||||
volumeBtn.setOnAction(e -> volumePopover.show(volumeBtn));
|
||||
|
||||
var extraControls = new HBox(10);
|
||||
extraControls.getChildren().setAll(clearPlaylistBtn, shuffleBtn, new Spacer(), volumeBtn);
|
||||
extraControls.setMaxWidth(PANEL_MAX_WIDTH);
|
||||
|
||||
// == Root ==
|
||||
|
||||
setSpacing(5);
|
||||
getStyleClass().add("player");
|
||||
setAlignment(CENTER);
|
||||
getChildren().setAll(
|
||||
new Spacer(VERTICAL),
|
||||
new StackPane(coverImage),
|
||||
new Spacer(10, VERTICAL),
|
||||
trackTitle,
|
||||
trackArtist,
|
||||
trackAlbum,
|
||||
new Spacer(20, VERTICAL),
|
||||
mediaControls,
|
||||
new Spacer(10, VERTICAL),
|
||||
timeSlider,
|
||||
timeMarkersBox,
|
||||
new Spacer(10, VERTICAL),
|
||||
extraControls,
|
||||
new Spacer(VERTICAL)
|
||||
);
|
||||
|
||||
// == Play ==
|
||||
|
||||
backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
Color color = model.backgroundColorProperty().get();
|
||||
return new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}, model.backgroundColorProperty()));
|
||||
|
||||
playBtn.setOnAction(e -> {
|
||||
MediaPlayer player = currentPlayer.get();
|
||||
if (player == null) { return; }
|
||||
switch (player.getStatus()) {
|
||||
case READY, PAUSED, STOPPED -> player.play();
|
||||
case PLAYING -> player.pause();
|
||||
default -> { }
|
||||
}
|
||||
});
|
||||
|
||||
InvalidationListener timeChangeListener = obs -> {
|
||||
if (currentPlayer.get() == null) { return; }
|
||||
|
||||
var duration = currentPlayer.get().getCurrentTime();
|
||||
var seconds = duration != null && !duration.equals(Duration.ZERO) ? duration.toSeconds() : 0;
|
||||
|
||||
if (!timeSlider.isValueChanging()) { timeSlider.setValue(seconds); }
|
||||
currentTimeLabel.setText(seconds > 0 ? formatDuration(duration) : "0.0");
|
||||
};
|
||||
|
||||
InvalidationListener sliderChangeListener = obs -> {
|
||||
if (currentPlayer.get() == null) { return; }
|
||||
long max = (long) currentPlayer.get().getMedia().getDuration().toSeconds();
|
||||
long sliderVal = (long) timeSlider.getValue();
|
||||
if (sliderVal <= max && timeSlider.isValueChanging()) {
|
||||
currentPlayer.get().seek(Duration.seconds(sliderVal));
|
||||
}
|
||||
};
|
||||
timeSlider.valueProperty().addListener(sliderChangeListener);
|
||||
|
||||
model.currentTrackProperty().addListener((obs, old, val) -> {
|
||||
if (val == null) {
|
||||
coverImage.setFill(new ImagePattern(NO_IMAGE));
|
||||
trackTitle.setText(NO_TITLE);
|
||||
trackArtist.setText(NO_ARTIST);
|
||||
trackAlbum.setText(NO_ALBUM);
|
||||
timeSlider.setValue(0);
|
||||
currentPlayer.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Media media = val.createMedia();
|
||||
MediaPlayer mediaPlayer = new MediaPlayer(media);
|
||||
mediaPlayer.setOnReady(() -> {
|
||||
Image image = getTag(media, "image", Image.class, NO_IMAGE);
|
||||
coverImage.setFill(new ImagePattern(image));
|
||||
model.setBackgroundColor(image != NO_IMAGE ? getDominantColor(image, BACKGROUND_OPACITY) : null);
|
||||
|
||||
trackTitle.setText(getTag(media, "title", String.class, NO_TITLE));
|
||||
trackArtist.setText(getTag(media, "artist", String.class, NO_ARTIST));
|
||||
trackAlbum.setText(getTag(media, "album", String.class, NO_ALBUM));
|
||||
|
||||
timeSlider.setMax(media.getDuration().toSeconds());
|
||||
endTimeLabel.setText(formatDuration(media.getDuration()));
|
||||
|
||||
playIcon.iconCodeProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (mediaPlayer.statusProperty().get() == null) { return EQUALS; }
|
||||
return switch (mediaPlayer.getStatus()) {
|
||||
case READY, PAUSED, STOPPED -> PLAY_ARROW;
|
||||
case PLAYING -> PAUSE;
|
||||
default -> EQUALS;
|
||||
};
|
||||
}, mediaPlayer.statusProperty()));
|
||||
|
||||
mediaPlayer.volumeProperty().bind(volumeSlider.valueProperty());
|
||||
mediaPlayer.currentTimeProperty().addListener(timeChangeListener);
|
||||
});
|
||||
mediaPlayer.setOnEndOfMedia(model::playNext);
|
||||
|
||||
currentPlayer.set(mediaPlayer);
|
||||
mediaPlayer.play();
|
||||
});
|
||||
|
||||
// remove all listeners and dispose old player
|
||||
currentPlayer.addListener((obs, old, val) -> {
|
||||
if (old != null) {
|
||||
old.stop();
|
||||
old.volumeProperty().unbind();
|
||||
old.currentTimeProperty().removeListener(timeChangeListener);
|
||||
playIcon.iconCodeProperty().unbind();
|
||||
old.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private <T> T getTag(Media media, String key, Class<T> type, T defaultValue) {
|
||||
if (media == null || key == null || type == null) { return defaultValue; }
|
||||
Object tag = media.getMetadata().get(key);
|
||||
return type.isInstance(tag) ? type.cast(tag) : defaultValue;
|
||||
}
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.musicplayer;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.ImagePattern;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static atlantafx.base.theme.Styles.*;
|
||||
import static atlantafx.sampler.page.showcase.musicplayer.Utils.toWebColor;
|
||||
import static java.lang.Double.MAX_VALUE;
|
||||
import static javafx.geometry.Pos.CENTER_LEFT;
|
||||
import static javafx.scene.layout.Priority.ALWAYS;
|
||||
import static javafx.stage.FileChooser.ExtensionFilter;
|
||||
import static org.kordamp.ikonli.material2.Material2AL.ADD;
|
||||
import static org.kordamp.ikonli.material2.Material2MZ.PLAYLIST_PLAY;
|
||||
|
||||
final class Playlist extends VBox {
|
||||
|
||||
public Playlist(Model model) {
|
||||
var headerLabel = new Label("Playlist");
|
||||
headerLabel.getStyleClass().setAll(TEXT_CAPTION);
|
||||
// There's probably some #javafx-bug here. This label uses CSS class that
|
||||
// changes font size & weight. When switching between themes it _sometimes_
|
||||
// ignores new color variables and remains using old fg color. Like it
|
||||
// caches old font or something. The below rule forces it to use proper color.
|
||||
headerLabel.setStyle("-fx-text-fill: -color-fg-default;");
|
||||
|
||||
var sizeLabel = new Label("");
|
||||
sizeLabel.getStyleClass().add(TEXT_SMALL);
|
||||
|
||||
var sizeDescLabel = new Label("empty");
|
||||
sizeDescLabel.getStyleClass().add(TEXT_SMALL);
|
||||
|
||||
var loadProgress = new ProgressBar(1);
|
||||
loadProgress.getStyleClass().add(SMALL);
|
||||
loadProgress.setMaxWidth(MAX_VALUE);
|
||||
loadProgress.setVisible(false);
|
||||
|
||||
var addButton = new Button("Add", new FontIcon(ADD));
|
||||
|
||||
var controlsBox = new HBox();
|
||||
controlsBox.getStyleClass().add("controls");
|
||||
controlsBox.getChildren().setAll(
|
||||
new VBox(5, headerLabel, sizeDescLabel),
|
||||
new Spacer(),
|
||||
addButton
|
||||
);
|
||||
controlsBox.setAlignment(CENTER_LEFT);
|
||||
|
||||
var playlist = new ListView<>(model.playlist());
|
||||
playlist.setCellFactory(param -> new MediaCell(model));
|
||||
playlist.setPlaceholder(new Label("No Content"));
|
||||
|
||||
getStyleClass().add("playlist");
|
||||
setSpacing(10);
|
||||
getChildren().setAll(controlsBox, loadProgress, playlist);
|
||||
|
||||
// ~
|
||||
|
||||
model.currentTrackProperty().addListener((obs, old, val) -> playlist.refresh());
|
||||
|
||||
model.playlist().addListener((ListChangeListener<MediaFile>) c -> {
|
||||
if (model.playlist().size() > 0) {
|
||||
sizeLabel.setText(String.valueOf(model.playlist().size()));
|
||||
sizeDescLabel.setGraphic(sizeLabel);
|
||||
sizeDescLabel.setText("tracks");
|
||||
} else {
|
||||
sizeDescLabel.setGraphic(null);
|
||||
sizeDescLabel.setText("empty");
|
||||
}
|
||||
});
|
||||
|
||||
model.backgroundColorProperty().addListener((obs, old, val) -> {
|
||||
var color = model.backgroundColorProperty().get();
|
||||
playlist.setStyle("-color-cell-bg:" + toWebColor(color) + ";");
|
||||
setBackground(new Background(new BackgroundFill(color, CornerRadii.EMPTY, Insets.EMPTY)));
|
||||
});
|
||||
|
||||
addButton.setOnAction(e -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.getExtensionFilters().addAll(new ExtensionFilter("MP3 files (*.mp3)", "*.mp3"));
|
||||
List<File> files = fileChooser.showOpenMultipleDialog(getScene().getWindow());
|
||||
if (files == null || files.isEmpty()) { return; }
|
||||
|
||||
loadProgress.setVisible(true);
|
||||
final Task<Void> task = new Task<>() {
|
||||
int progress = 0;
|
||||
|
||||
@Override
|
||||
public Void call() throws InterruptedException {
|
||||
for (File file : files) {
|
||||
Thread.sleep(500); // add artificial delay to demonstrate progress bar
|
||||
Platform.runLater(() -> model.addFile(new MediaFile(file)));
|
||||
progress++;
|
||||
updateProgress(progress, files.size());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
task.setOnSucceeded(te -> loadProgress.setVisible(false));
|
||||
loadProgress.progressProperty().bind(task.progressProperty());
|
||||
new Thread(task).start();
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static class MediaCell extends ListCell<MediaFile> {
|
||||
|
||||
private final Model model;
|
||||
private final HBox root;
|
||||
private final Rectangle coverImage;
|
||||
private final Label titleLabel;
|
||||
private final Label artistLabel;
|
||||
private final FontIcon playMark;
|
||||
|
||||
public MediaCell(Model model) {
|
||||
this.model = model;
|
||||
|
||||
coverImage = new Rectangle(0, 0, 32, 32);
|
||||
coverImage.setArcWidth(10.0);
|
||||
coverImage.setArcHeight(10.0);
|
||||
|
||||
titleLabel = new Label();
|
||||
titleLabel.setMaxWidth(MAX_VALUE);
|
||||
titleLabel.getStyleClass().add(TEXT_CAPTION);
|
||||
|
||||
artistLabel = new Label();
|
||||
artistLabel.setMaxWidth(MAX_VALUE);
|
||||
|
||||
var titleBox = new VBox(5, titleLabel, artistLabel);
|
||||
titleBox.setAlignment(CENTER_LEFT);
|
||||
HBox.setHgrow(titleBox, ALWAYS);
|
||||
|
||||
playMark = new FontIcon(PLAYLIST_PLAY);
|
||||
|
||||
root = new HBox(10, coverImage, titleBox, playMark);
|
||||
root.setAlignment(CENTER_LEFT);
|
||||
root.setOnMouseClicked(e -> {
|
||||
if (getItem() != null) { model.play(getItem()); }
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(MediaFile mediaFile, boolean empty) {
|
||||
super.updateItem(mediaFile, empty);
|
||||
|
||||
if (empty || mediaFile == null) {
|
||||
setGraphic(null);
|
||||
coverImage.setFill(null);
|
||||
titleLabel.setText(null);
|
||||
artistLabel.setText(null);
|
||||
} else {
|
||||
setGraphic(root);
|
||||
|
||||
playMark.setVisible(Objects.equals(mediaFile, model.currentTrackProperty().get()));
|
||||
|
||||
mediaFile.readMetadata(metadata -> {
|
||||
coverImage.setFill(new ImagePattern(metadata.image()));
|
||||
titleLabel.setText(metadata.title());
|
||||
artistLabel.setText(metadata.artist());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/* SPDX-License-Identifier: MIT */
|
||||
package atlantafx.sampler.page.showcase.musicplayer;
|
||||
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.util.Duration;
|
||||
|
||||
final class Utils {
|
||||
|
||||
public static WritableImage copyImage(Image source) {
|
||||
int height = (int) source.getHeight();
|
||||
int width = (int) source.getWidth();
|
||||
var reader = source.getPixelReader();
|
||||
var target = new WritableImage(width, height);
|
||||
var writer = target.getPixelWriter();
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
Color color = reader.getColor(x, y);
|
||||
writer.setColor(x, y, color);
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
public static Color getDominantColor(Image image, double opacity) {
|
||||
int[] dominant = ColorThief.getColor(SwingFXUtils.fromFXImage(image, null));
|
||||
if (dominant == null || dominant.length != 3) { return Color.TRANSPARENT; }
|
||||
return Color.rgb(dominant[0], dominant[1], dominant[2], opacity);
|
||||
}
|
||||
|
||||
public static String formatDuration(Duration duration) {
|
||||
long seconds = (long) duration.toSeconds();
|
||||
return seconds < 3600 ?
|
||||
String.format("%02d:%02d", (seconds % 3600) / 60, seconds % 60) :
|
||||
String.format("%d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, seconds % 60);
|
||||
}
|
||||
|
||||
public static String toWebColor(Color color) {
|
||||
int r = ((int) Math.round(color.getRed() * 255)) << 24;
|
||||
int g = ((int) Math.round(color.getGreen() * 255)) << 16;
|
||||
int b = ((int) Math.round(color.getBlue() * 255)) << 8;
|
||||
int a = ((int) Math.round(color.getOpacity() * 255));
|
||||
return String.format("#%08X", (r + g + b + a));
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/** SPDX-License-Identifier: MIT */
|
||||
|
||||
.music-player-showcase {
|
||||
-fx-border-width: 1;
|
||||
-fx-border-color: -color-border-default;
|
||||
}
|
||||
|
||||
.music-player-showcase .playlist {
|
||||
-fx-padding: 10;
|
||||
}
|
||||
|
||||
.music-player-showcase .playlist > .list-view {
|
||||
-fx-border-width: 0;
|
||||
}
|
||||
.music-player-showcase .playlist > .list-view .list-cell {
|
||||
-fx-cell-size: 4em;
|
||||
-fx-background-color: transparent;
|
||||
-fx-background-radius: 6px;
|
||||
}
|
||||
.music-player-showcase .playlist > .list-view:focused>.virtual-flow>.clipped-container>.sheet>.list-cell:filled:selected {
|
||||
-fx-background-color: transparent;
|
||||
}
|
||||
.music-player-showcase .playlist > .list-view:focused>.virtual-flow>.clipped-container>.sheet>.list-cell:selected:hover,
|
||||
.music-player-showcase .playlist > .list-view:focused>.virtual-flow>.clipped-container>.sheet>.list-cell:filled:hover {
|
||||
-fx-background-color: derive(-color-cell-bg, -5%);
|
||||
}
|
@ -6,11 +6,13 @@ module atlantafx.sampler {
|
||||
|
||||
requires java.desktop;
|
||||
requires javafx.swing;
|
||||
requires javafx.media;
|
||||
requires javafx.web;
|
||||
|
||||
requires org.kordamp.ikonli.core;
|
||||
requires org.kordamp.ikonli.javafx;
|
||||
requires org.kordamp.ikonli.feather;
|
||||
requires org.kordamp.ikonli.material2;
|
||||
|
||||
requires fr.brouillard.oss.cssfx;
|
||||
requires datafaker;
|
||||
|
@ -3,6 +3,21 @@
|
||||
.root {
|
||||
-fx-font-family: "Inter";
|
||||
}
|
||||
.root:showcase-mode #sidebar,
|
||||
.root:showcase-mode .page > .header {
|
||||
-fx-min-width: 0;
|
||||
-fx-pref-width: 0;
|
||||
-fx-max-width: 0;
|
||||
-fx-min-height: 0;
|
||||
-fx-pref-height: 0;
|
||||
-fx-max-height: 0;
|
||||
visibility: false;
|
||||
}
|
||||
.root:showcase-mode .page > .stack > .scroll-pane > .viewport > * > .wrapper > .user-content {
|
||||
-fx-max-width: 4096px;
|
||||
-fx-padding: 0;
|
||||
-fx-spacing: 0;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
-fx-padding: 0 0 12px 0;
|
||||
|
BIN
sampler/src/main/resources/images/no-image.png
Normal file
BIN
sampler/src/main/resources/images/no-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
sampler/src/main/resources/images/vinyl.jpg
Normal file
BIN
sampler/src/main/resources/images/vinyl.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
@ -15,7 +15,16 @@ $tree-cell-indent: 1em !default;
|
||||
|
||||
@mixin _base() {
|
||||
|
||||
-fx-border-color: -color-border-default;
|
||||
// draft (only supported by list view for now)
|
||||
-color-cell-bg: -color-bg-default;
|
||||
-color-cell-bg-selected: -color-accent-subtle;
|
||||
-color-cell-bg-odd: -color-bg-inset;
|
||||
-color-cell-fg: -color-fg-default;
|
||||
-color-cell-fg-odd: -color-fg-default;
|
||||
-color-cell-fg-selected: -color-fg-default;
|
||||
-color-cell-border: -color-border-default;
|
||||
|
||||
-fx-border-color: -color-cell-border;
|
||||
-fx-border-width: cfg.$border-width;
|
||||
-fx-border-radius: 0;
|
||||
|
||||
@ -104,11 +113,12 @@ $tree-cell-indent: 1em !default;
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
.list-view {
|
||||
|
||||
@include _base();
|
||||
|
||||
.list-cell {
|
||||
-fx-background-color: -color-bg-default;
|
||||
-fx-text-fill: -color-fg-default;
|
||||
-fx-background-color: -color-cell-bg;
|
||||
-fx-text-fill: -color-cell-fg;
|
||||
-fx-padding: 0 $cell-padding-x 0 $cell-padding-x;
|
||||
-fx-cell-size: $cell-size-normal;
|
||||
|
||||
@ -120,7 +130,7 @@ $tree-cell-indent: 1em !default;
|
||||
|
||||
&.bordered {
|
||||
.list-cell {
|
||||
-fx-border-color: -color-border-default;
|
||||
-fx-border-color: -color-cell-border;
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +142,7 @@ $tree-cell-indent: 1em !default;
|
||||
|
||||
&.striped {
|
||||
.list-cell:odd {
|
||||
-fx-background-color: -color-bg-inset;
|
||||
-fx-background-color: -color-cell-bg-odd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ $separator-width: 0.75px !default;
|
||||
|
||||
#{cfg.$font-icon-selector} {
|
||||
-fx-icon-color: -color-button-fg-hover;
|
||||
-fx-fill: -color-button-fg-hover;
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,6 +81,7 @@ $separator-width: 0.75px !default;
|
||||
|
||||
#{cfg.$font-icon-selector} {
|
||||
-fx-icon-color: -color-button-fg-focused;
|
||||
-fx-fill: -color-button-fg-focused;
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,6 +100,7 @@ $separator-width: 0.75px !default;
|
||||
|
||||
#{cfg.$font-icon-selector} {
|
||||
-fx-icon-color: -color-button-fg-pressed;
|
||||
-fx-fill: -color-button-fg-pressed;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,6 +89,7 @@
|
||||
// font icons
|
||||
.ikonli-font-icon {
|
||||
-fx-icon-color: -color-fg-default;
|
||||
-fx-fill: -color-fg-default;
|
||||
-fx-icon-size: cfg.$icon-size;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user