├── src ├── test │ ├── resources │ │ └── com │ │ │ └── chainstaysoftware │ │ │ └── filechooser │ │ │ ├── aaa │ │ │ ├── dir1 │ │ │ └── aaa │ │ │ ├── dir2 │ │ │ └── aaa │ │ │ ├── empty.txt │ │ │ ├── empty.xml │ │ │ └── bbb │ └── java │ │ └── com │ │ └── chainstaysoftware │ │ └── filechooser │ │ ├── preview │ │ └── PreviewPaneQueryTest.java │ │ ├── FileChooserDemo.java │ │ └── FileMetaDataComparatorTest.java └── main │ ├── resources │ ├── icons8 │ │ ├── 64x64 │ │ │ ├── AAC-64.png │ │ │ ├── AVI-64.png │ │ │ ├── CD-64.png │ │ │ ├── CSS-64.png │ │ │ ├── EXE-64.png │ │ │ ├── GIF-64.png │ │ │ ├── HDD-64.png │ │ │ ├── JPG-64.png │ │ │ ├── MOV-64.png │ │ │ ├── MP3-64.png │ │ │ ├── PDF-64.png │ │ │ ├── PNG-64.png │ │ │ ├── PS-64.png │ │ │ ├── TXT-64.png │ │ │ ├── WMA-64.png │ │ │ ├── XLS-64.png │ │ │ ├── XML-64.png │ │ │ ├── ZIP-64.png │ │ │ ├── Back-64.png │ │ │ ├── File-64.png │ │ │ ├── Folder-64.png │ │ │ ├── HTML-64.png │ │ │ ├── List-64.png │ │ │ ├── Save-64.png │ │ │ ├── USB 2-64.png │ │ │ ├── Word-64.png │ │ │ ├── Computer-64.png │ │ │ ├── Console-64.png │ │ │ ├── Network-64.png │ │ │ ├── Open Folder-64.png │ │ │ ├── PowerPoint-64.png │ │ │ ├── Small Icons-64.png │ │ │ ├── Star Filled-64.png │ │ │ └── User Folder-64.png │ │ └── 24x24 │ │ │ ├── Back-24.png │ │ │ ├── List-24.png │ │ │ ├── Columns-24.png │ │ │ └── Small Icons-24.png │ ├── filechooser.properties │ └── com │ │ └── chainstaysoftware │ │ └── filechooser │ │ └── filechooser.css │ └── java │ └── com │ └── chainstaysoftware │ └── filechooser │ ├── ViewType.java │ ├── HelpCallback.java │ ├── OrderDirection.java │ ├── os │ ├── PlaceType.java │ ├── PlacesProvider.java │ ├── Place.java │ ├── OsInfo.java │ ├── def │ │ └── DefaultPlacesProvider.java │ ├── Places.java │ ├── linux │ │ ├── MountInfo.java │ │ ├── LinuxFileSystem.java │ │ └── LinuxPlacesProvider.java │ └── osx │ │ └── OsxPlacesProvider.java │ ├── FileBrowserCss.java │ ├── FileChooserCallback.java │ ├── OrderBy.java │ ├── IconGridCellContextMenuFactory.java │ ├── FavoritesCallback.java │ ├── PopulateTreeItemRunnableFactory.java │ ├── preview │ ├── PreviewPane.java │ ├── PreviewPaneQuery.java │ ├── ImagePreviewPane.java │ └── HeadPreviewPane.java │ ├── DirectoryListItem.java │ ├── DirListItemComparator.java │ ├── FilesView.java │ ├── ShowHiddenFilesPredicate.java │ ├── PreviewPaneFactory.java │ ├── icons │ ├── Icons.java │ └── IconsImpl.java │ ├── PlacesTreeItem.java │ ├── FilesViewCallback.java │ ├── DirectoryWatcherTask.java │ ├── FilterHiddenFromDirList.java │ ├── FilterHiddenFromDirTree.java │ ├── DirectoryTreeItem.java │ ├── IconGridCell.java │ ├── FileMetaDataComparator.java │ ├── DirListNameColumnCellFactory.java │ ├── DirOrWildcardFilter.java │ ├── AbstractFilesView.java │ ├── UpdateDirectoryList.java │ ├── UpdateDirectoryTree.java │ ├── DirectoryChooserFx.java │ ├── PlacesView.java │ ├── FileChooserFx.java │ ├── DirectoryChooserFxImpl.java │ ├── PropertiesPreviewPane.java │ ├── PlacesTreeItemCellFactory.java │ ├── ListFilesWithPreviewView.java │ └── IconsFilesView.java ├── licenses ├── controlsfx-license.txt ├── icons8.txt ├── commons-io-license.txt └── commons-lang-license.txt ├── .gitignore ├── LICENSE.TXT ├── README.md └── pom.xml /src/test/resources/com/chainstaysoftware/filechooser/aaa: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/com/chainstaysoftware/filechooser/dir1/aaa: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/com/chainstaysoftware/filechooser/dir2/aaa: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/com/chainstaysoftware/filechooser/empty.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/com/chainstaysoftware/filechooser/empty.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/test/resources/com/chainstaysoftware/filechooser/bbb: -------------------------------------------------------------------------------- 1 | the quick brown fox jumps over the lazy dog. 2 | -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/AAC-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/AAC-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/AVI-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/AVI-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/CD-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/CD-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/CSS-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/CSS-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/EXE-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/EXE-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/GIF-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/GIF-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/HDD-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/HDD-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/JPG-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/JPG-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/MOV-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/MOV-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/MP3-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/MP3-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/PDF-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/PDF-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/PNG-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/PNG-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/PS-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/PS-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/TXT-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/TXT-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/WMA-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/WMA-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/XLS-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/XLS-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/XML-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/XML-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/ZIP-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/ZIP-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/24x24/Back-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/24x24/Back-24.png -------------------------------------------------------------------------------- /src/main/resources/icons8/24x24/List-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/24x24/List-24.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Back-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Back-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/File-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/File-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Folder-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Folder-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/HTML-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/HTML-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/List-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/List-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Save-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Save-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/USB 2-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/USB 2-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Word-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Word-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/24x24/Columns-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/24x24/Columns-24.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Computer-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Computer-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Console-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Console-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Network-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Network-64.png -------------------------------------------------------------------------------- /licenses/controlsfx-license.txt: -------------------------------------------------------------------------------- 1 | ControlsFx is licensed under the 3-Clause BSD License. 2 | 3 | http://www.opensource.org/licenses/bsd-license.php 4 | 5 | -------------------------------------------------------------------------------- /src/main/resources/icons8/24x24/Small Icons-24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/24x24/Small Icons-24.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Open Folder-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Open Folder-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/PowerPoint-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/PowerPoint-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Small Icons-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Small Icons-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/Star Filled-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/Star Filled-64.png -------------------------------------------------------------------------------- /src/main/resources/icons8/64x64/User Folder-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ricemery/FileChooserFx/HEAD/src/main/resources/icons8/64x64/User Folder-64.png -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/ViewType.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | public enum ViewType { 4 | List, ListWithPreview, Icon 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/HelpCallback.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | @FunctionalInterface 4 | public interface HelpCallback { 5 | void invoke(); 6 | } 7 | -------------------------------------------------------------------------------- /licenses/icons8.txt: -------------------------------------------------------------------------------- 1 | Icons8 icons are licensed under the Creative Commons Attribution-NoDerivs 3.0 2 | Unported license - https://creativecommons.org/licenses/by-nd/3.0/ 3 | 4 | https://icons8.com/license 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/OrderDirection.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | /** 4 | * Sort order for files. 5 | */ 6 | public enum OrderDirection { 7 | Ascending, 8 | Descending 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/PlaceType.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os; 2 | 3 | public enum PlaceType { 4 | HardDisk, 5 | FloppyDisk, 6 | Cd, 7 | Dvd, 8 | Network, 9 | Usb 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/FileBrowserCss.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | public class FileBrowserCss { 4 | public String getUrl() { 5 | return getClass().getResource("filechooser.css").toExternalForm(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Mobile Tools for Java (J2ME) 4 | .mtj.tmp/ 5 | 6 | # Package Files # 7 | *.jar 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | 14 | #IntelliJ 15 | .idea/ 16 | *.iml 17 | 18 | target/ 19 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/FileChooserCallback.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import java.io.File; 4 | import java.util.Optional; 5 | 6 | @FunctionalInterface 7 | public interface FileChooserCallback { 8 | void fileChosen(Optional fileOptional); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/OrderBy.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | /** 4 | * Sort field for files. Note that not all views support all the OrderBy values. 5 | */ 6 | public enum OrderBy { 7 | Name, 8 | ModificationDate, 9 | Type, 10 | Size 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/PlacesProvider.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os; 2 | 3 | import java.util.List; 4 | 5 | @FunctionalInterface 6 | public interface PlacesProvider { 7 | /** 8 | * Return the default list of {@link Place} for the supporting OS instance. 9 | */ 10 | List getDefaultPlaces(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/IconGridCellContextMenuFactory.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.scene.control.ContextMenu; 4 | 5 | /** 6 | * Build {@link ContextMenu} instance for a single {@link IconGridCell}. 7 | */ 8 | @FunctionalInterface 9 | public interface IconGridCellContextMenuFactory { 10 | ContextMenu create(final DirectoryListItem item); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/FavoritesCallback.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * Called when user Adds or Removes a favorite directory location. The calling code is responsible 7 | * for persisting the favorites. FileChooserFx does NOT handle persistence of favorites. 8 | */ 9 | @FunctionalInterface 10 | public interface FavoritesCallback { 11 | void invoke(File directory); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/PopulateTreeItemRunnableFactory.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.scene.control.TreeItem; 4 | 5 | import java.io.File; 6 | import java.nio.file.DirectoryStream; 7 | import java.nio.file.Path; 8 | 9 | @FunctionalInterface 10 | public interface PopulateTreeItemRunnableFactory { 11 | Runnable create(DirectoryStream directoryStream, 12 | DirectoryStream unfilteredDirectoryStream, 13 | TreeItem parentItem); 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | Copyright 2015-2021 Chainstay Software 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/preview/PreviewPane.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.preview; 2 | 3 | 4 | import javafx.scene.layout.Pane; 5 | 6 | import java.io.File; 7 | 8 | /** 9 | * Show preview of passed in {@link File}. Note that it is possible that the 10 | * implementations will read the entire file will memory. So, there is a 11 | * potential for OutOfMemoryException for large files (and untuned JVMs). 12 | */ 13 | public interface PreviewPane { 14 | /** 15 | * Sets the file to display within the Pane. 16 | */ 17 | void setFile(File file); 18 | 19 | Pane getPane(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/Place.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os; 2 | 3 | import java.io.File; 4 | 5 | public class Place { 6 | private final PlaceType type; 7 | private final File path; 8 | 9 | public Place(PlaceType type, File path) { 10 | this.type = type; 11 | this.path = path; 12 | } 13 | 14 | public PlaceType getType() { 15 | return type; 16 | } 17 | 18 | public File getPath() { 19 | return path; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "Place{" + 25 | "type=" + type + 26 | ", path=" + path + 27 | '}'; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/OsInfo.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os; 2 | 3 | import java.util.Locale; 4 | 5 | /** 6 | * Util to determine the OS that the JVM is running within. 7 | */ 8 | public final class OsInfo { 9 | private static final String osName = System.getProperty("os.name").toLowerCase(Locale.getDefault()); 10 | 11 | private OsInfo() {} 12 | 13 | public static boolean isWindows() { 14 | return osName.contains("windows"); 15 | } 16 | 17 | public static boolean isLinux() { 18 | return osName.contains("linux"); 19 | } 20 | 21 | public static boolean isMac() { 22 | return osName.contains("mac os"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/DirectoryListItem.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * Wraps a File instance to be rendered in a 7 | * Grid, List, or TableView. 8 | */ 9 | class DirectoryListItem { 10 | private final File file; 11 | 12 | // Cache isDir to minimize calls to OS. 13 | private Boolean isDirectory; 14 | 15 | public DirectoryListItem(final File file) { 16 | this.file = file; 17 | } 18 | 19 | public File getFile() { 20 | return file; 21 | } 22 | 23 | public boolean isDirectory() { 24 | if (isDirectory == null) { 25 | isDirectory = file.isDirectory(); 26 | } 27 | 28 | return isDirectory; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/DirListItemComparator.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import java.io.Serializable; 4 | import java.util.Comparator; 5 | 6 | public class DirListItemComparator implements Comparator, Serializable { 7 | private static final long serialVersionUID = 6642373165691519041L; 8 | 9 | private final FileMetaDataComparator fileMetaDataComparator; 10 | 11 | public DirListItemComparator(final OrderBy orderBy, final OrderDirection direction) { 12 | this.fileMetaDataComparator = new FileMetaDataComparator(orderBy, direction); 13 | } 14 | 15 | @Override 16 | public int compare(final DirectoryListItem o1, final DirectoryListItem o2) { 17 | return fileMetaDataComparator.compare(o1.getFile(), o2.getFile()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/FilesView.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.scene.Node; 4 | 5 | import java.nio.file.DirectoryStream; 6 | import java.nio.file.Path; 7 | 8 | interface FilesView { 9 | Node getNode(); 10 | 11 | /** 12 | * sets the files on the view. 13 | * @param directoryStream Filtered stream of files to show in view. 14 | * @param remainingDirectoryStream This stream contains the files not to 15 | * show in the view. But, may include 16 | * directories that should be shown in the 17 | * view but were filtered out in the directoryStream. 18 | */ 19 | void setFiles(DirectoryStream directoryStream, 20 | DirectoryStream remainingDirectoryStream); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/ShowHiddenFilesPredicate.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.beans.property.BooleanProperty; 4 | 5 | import java.io.File; 6 | import java.util.function.Predicate; 7 | 8 | class ShowHiddenFilesPredicate implements Predicate { 9 | private final BooleanProperty showHiddenFiles; 10 | private final BooleanProperty shouldHideFiles; 11 | 12 | ShowHiddenFilesPredicate(final BooleanProperty showHiddenFiles, 13 | final BooleanProperty shouldHideFiles) { 14 | this.showHiddenFiles = showHiddenFiles; 15 | this.shouldHideFiles = shouldHideFiles; 16 | } 17 | 18 | @Override 19 | public boolean test(final File file) { 20 | final boolean filterHidden = !showHiddenFiles.get(); 21 | return !(filterHidden && file.isHidden()) && (!shouldHideFiles.get() || file.isDirectory()); 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/def/DefaultPlacesProvider.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os.def; 2 | 3 | import com.chainstaysoftware.filechooser.os.Place; 4 | import com.chainstaysoftware.filechooser.os.PlaceType; 5 | import com.chainstaysoftware.filechooser.os.PlacesProvider; 6 | 7 | import java.io.File; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.stream.Collectors; 11 | 12 | public class DefaultPlacesProvider implements PlacesProvider { 13 | /** 14 | * Return the default list of {@link Place} for the supporting OS instance. 15 | * Only returns Roots and assumes all are {@link PlaceType#HardDisk} 16 | */ 17 | @Override 18 | public List getDefaultPlaces() { 19 | final List roots = Arrays.asList(File.listRoots()); 20 | return roots.stream() 21 | .map(r -> new Place(PlaceType.HardDisk, r)) 22 | .collect(Collectors.toList()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/PreviewPaneFactory.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.preview.PreviewPane; 4 | 5 | import java.util.Optional; 6 | import java.util.logging.Level; 7 | import java.util.logging.Logger; 8 | 9 | final class PreviewPaneFactory { 10 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.PreviewPaneFactory"); 11 | 12 | private PreviewPaneFactory() {} 13 | 14 | /** 15 | * Creates a {@link PreviewPane} instance from the passed in class. If there is an error, then an 16 | * empty Optional is returned. 17 | * @param previewPaneClass Class to create instance for. 18 | * @return {@link Optional} that contains the created {@link PreviewPane} instance. Or, empty on error. 19 | */ 20 | static Optional create(final Class previewPaneClass) { 21 | try { 22 | return Optional.of(previewPaneClass.newInstance()); 23 | } catch (InstantiationException | IllegalAccessException e) { 24 | logger.log(Level.SEVERE, "Error creating PreviewPane", e); 25 | return Optional.empty(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/icons/Icons.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.icons; 2 | 3 | import javafx.beans.property.MapProperty; 4 | import javafx.scene.image.Image; 5 | 6 | import java.io.File; 7 | import java.util.Map; 8 | 9 | /** 10 | * Icon loading. 11 | */ 12 | public interface Icons { 13 | /** 14 | * Load an icon from the resourceName location in the classpath. 15 | * @param resourceName Must be a filename in the classpath. 16 | * @return {@link Image} 17 | * @throws IllegalArgumentException If the resource is not found. 18 | */ 19 | Image getIcon(String resourceName); 20 | 21 | /** 22 | * Load an icon for the passed in file. 23 | * @param file to load an icon for. 24 | * @return {@link Image} 25 | * @throws IllegalArgumentException If the resource is not found. 26 | */ 27 | Image getIconForFile(File file); 28 | 29 | /** 30 | * Mapping of file extensions to icon images. 31 | */ 32 | MapProperty fileTypeIconsProperty(); 33 | 34 | /** 35 | * Sets the mapping of file extensions to icon images. 36 | */ 37 | void setFileTypeIcons(Map map); 38 | 39 | /** 40 | * Mapping of file extensions to icon images. 41 | */ 42 | Map getFileTypeIcons(); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/Places.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os; 2 | 3 | import com.chainstaysoftware.filechooser.os.def.DefaultPlacesProvider; 4 | import com.chainstaysoftware.filechooser.os.linux.LinuxPlacesProvider; 5 | import com.chainstaysoftware.filechooser.os.osx.OsxPlacesProvider; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Get the default list of {@link Place} to show in the Places list. 11 | */ 12 | public class Places { 13 | private final PlacesProvider defaultPlacesProvider = new DefaultPlacesProvider(); 14 | private final PlacesProvider linuxPlacesProvider = new LinuxPlacesProvider(); 15 | private final PlacesProvider osxPlacesProvider = new OsxPlacesProvider(); 16 | 17 | /** 18 | * Get the default list of {@link Place} to show in the Places list. 19 | */ 20 | public List getDefaultPlaces(final boolean showMountPoints) { 21 | if (!showMountPoints) { 22 | return defaultPlacesProvider.getDefaultPlaces(); 23 | } 24 | 25 | if (OsInfo.isLinux()) { 26 | return linuxPlacesProvider.getDefaultPlaces(); 27 | } 28 | 29 | if (OsInfo.isMac()) { 30 | return osxPlacesProvider.getDefaultPlaces(); 31 | } 32 | 33 | return defaultPlacesProvider.getDefaultPlaces(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/PlacesTreeItem.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | 4 | import javafx.scene.image.Image; 5 | 6 | import java.io.File; 7 | import java.util.Optional; 8 | 9 | /** 10 | * Wrapper for items being placed in the Places tree. 11 | */ 12 | class PlacesTreeItem { 13 | private final Optional text; 14 | private final Optional file; 15 | private final Image icon; 16 | private final boolean isFavorite; 17 | 18 | public PlacesTreeItem(final Optional text, 19 | final Optional file, 20 | final Image icon, 21 | final boolean isFavorite) { 22 | this.text = text; 23 | this.file = file; 24 | this.icon = icon; 25 | this.isFavorite = isFavorite; 26 | } 27 | 28 | public Optional getText() { 29 | return text; 30 | } 31 | 32 | public Optional getFile() { 33 | return file; 34 | } 35 | 36 | public Image getIcon() { 37 | return icon; 38 | } 39 | 40 | public boolean isFavorite() { 41 | return isFavorite; 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return "PlacesTreeItem{" + 47 | "text=" + text + 48 | ", file=" + file + 49 | ", icon=" + icon + 50 | ", isFavorite=" + isFavorite + 51 | '}'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/resources/filechooser.properties: -------------------------------------------------------------------------------- 1 | # File Browser Strings 2 | backbutton.tooltip=Back 3 | iconview.tooltip=Icon View 4 | listview.tooltip=List View 5 | listwithpreviewview.tooltip=List with Preview 6 | addfavoritebutton.txt=Add 7 | addfavoritebutton.tooltip.txt=Add selected directory as favorite 8 | removefavoritebutton.txt=Remove 9 | removefavoritebutton.tooltip.txt=Remove directory from favorites 10 | newfolderbutton.text=New Folder 11 | helpbutton.text=Help 12 | cancelbutton.text=Cancel 13 | openbutton.text=Open 14 | savebutton.text=Save 15 | namelabel.text=Name 16 | filterdropdown.allfiles=All Files (*.*) 17 | 18 | # Places Strings 19 | placeslist.text=Places 20 | computer.text=Computer 21 | favorites.text=Favorites 22 | 23 | # Create Folder Dialog Strings 24 | createfolder.title=Create Folder 25 | createfolder.text=Folder Name 26 | 27 | # ListFilesView Headings 28 | listfilesview.name=Name 29 | listfilesview.datemodified=Date Modified 30 | listfilesview.size=Size 31 | 32 | # Properties Preview Strings 33 | propertiespreview.create=Created 34 | propertiespreview.modified=Modified 35 | propertiespreview.lastopened=Last Opened 36 | propertiespreview.size=Size 37 | 38 | # Icons View Context Menu Strings 39 | iconsview.context.arrangeby=Arrange By 40 | iconsview.context.name=Name 41 | iconsview.context.size=Size 42 | iconsview.context.type=Type 43 | iconsview.context.modificationdate=Modification Date 44 | iconsview.context.reverseorder=Reverse Order 45 | 46 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/preview/PreviewPaneQuery.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.preview; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.util.Map; 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | /** 11 | * Determine the {@link PreviewPane} to utilize for a specific file. 12 | */ 13 | public final class PreviewPaneQuery { 14 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.preview.PreviewPaneQuery"); 15 | 16 | private PreviewPaneQuery() {} 17 | 18 | /** 19 | * Determine the {@link PreviewPane} to utilize. 20 | * @param previewHandlers Map of configured {@link PreviewPane} keyed on MimeType. 21 | * @param file File to return a {@link PreviewPane} for. 22 | * @return {@link PreviewPane} class to use for the file. Or, null if none found. 23 | */ 24 | public static Class query(final Map> previewHandlers, 25 | final File file) { 26 | try { 27 | if (file.isDirectory()) { 28 | return null; 29 | } 30 | 31 | final String mimeType = Files.probeContentType(file.toPath()); 32 | return previewHandlers.get(mimeType); 33 | } catch (IOException e) { 34 | logger.log(Level.WARNING, "could not determine mimetype for - " + file, e); 35 | return null; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/FilesViewCallback.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.beans.property.BooleanProperty; 4 | import javafx.beans.property.ObjectProperty; 5 | import javafx.collections.ObservableList; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.DirectoryStream; 10 | import java.nio.file.Path; 11 | 12 | /** 13 | * Callbacks from the {@link FilesView} implementations back into the 14 | * {@link FileChooserFx}. 15 | */ 16 | public interface FilesViewCallback { 17 | /** 18 | * Request that the directory is changed, and update the 19 | * view to include the directory contents. 20 | */ 21 | void requestChangeDirectory(File directory); 22 | 23 | DirectoryStream getDirectoryStream(File directory) throws IOException; 24 | 25 | DirectoryStream unfilteredDirectoryStream(File directory) throws IOException; 26 | 27 | /** 28 | * Update the currently selected file. 29 | */ 30 | void setCurrentSelection(File file); 31 | 32 | File getCurrentSelection(); 33 | 34 | void fireDoneButton(); 35 | 36 | void disableAddFavoriteButton(boolean disable); 37 | 38 | void disableRemoveFavoritieButton(boolean disable); 39 | 40 | /** 41 | * Update all the files in the view. 42 | */ 43 | void updateFiles(); 44 | 45 | ObjectProperty orderByProperty(); 46 | 47 | ObjectProperty orderDirectionProperty(); 48 | 49 | ObservableList favoriteDirsProperty(); 50 | 51 | BooleanProperty showMountPointsProperty(); 52 | 53 | BooleanProperty showHiddenFilesProperty(); 54 | 55 | BooleanProperty shouldHideFilesProperty(); 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/linux/MountInfo.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os.linux; 2 | 3 | /** 4 | * Adapted from - https://gist.github.com/ikonst/3394662 5 | */ 6 | public class MountInfo { 7 | /** 8 | * The mounted device (can be "none" or any arbitrary string for virtual 9 | * file systems). 10 | */ 11 | private final String device; 12 | /** 13 | * The path where the file system is mounted. 14 | */ 15 | private final String mountpoint; 16 | /** 17 | * The file system. 18 | */ 19 | private final String fs; 20 | /** 21 | * The mount options. 22 | */ 23 | private final String options; 24 | /** 25 | * The dumping frequency for dump(8); see fstab(5). 26 | */ 27 | private final int dump; 28 | /** 29 | * The order in which file system checks are done at reboot time; see 30 | * fstab(5). 31 | */ 32 | private final int pass; 33 | 34 | public MountInfo(final String device, 35 | final String mountpoint, 36 | final String fs, 37 | final String options, 38 | final int dump, 39 | final int pass) { 40 | this.device = device; 41 | this.mountpoint = mountpoint; 42 | this.fs = fs; 43 | this.options = options; 44 | this.dump = dump; 45 | this.pass = pass; 46 | } 47 | 48 | public String getDevice() { 49 | return device; 50 | } 51 | 52 | public String getMountpoint() { 53 | return mountpoint; 54 | } 55 | 56 | public String getFs() { 57 | return fs; 58 | } 59 | 60 | public String getOptions() { 61 | return options; 62 | } 63 | 64 | public int getDump() { 65 | return dump; 66 | } 67 | 68 | public int getPass() { 69 | return pass; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/preview/ImagePreviewPane.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.preview; 2 | 3 | import javafx.geometry.Pos; 4 | import javafx.scene.image.Image; 5 | import javafx.scene.image.ImageView; 6 | import javafx.scene.layout.HBox; 7 | import javafx.scene.layout.Pane; 8 | 9 | import java.io.File; 10 | import java.io.FileInputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | 16 | /** 17 | * {@link PreviewPane} implementation for displaying image file types. 18 | */ 19 | public class ImagePreviewPane implements PreviewPane { 20 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.ImagePreviewPane"); 21 | 22 | private final HBox hBox; 23 | private final ImageView imageView; 24 | 25 | public ImagePreviewPane() { 26 | imageView = new ImageView(); 27 | imageView.setId("imagePreviewImageView"); 28 | 29 | hBox = new HBox(); 30 | hBox.setId("imagePreviewHbox"); 31 | hBox.getChildren().add(imageView); 32 | hBox.setAlignment(Pos.CENTER); 33 | hBox.setMinSize(0, 0); 34 | 35 | imageView.fitWidthProperty().bind(hBox.widthProperty()); 36 | imageView.fitHeightProperty().bind(hBox.heightProperty()); 37 | imageView.setPreserveRatio(true); 38 | } 39 | 40 | /** 41 | * Sets the file to display within the Pane. 42 | */ 43 | @Override 44 | public void setFile(final File file) { 45 | try (final InputStream is = new FileInputStream(file)){ 46 | final Image image = new Image(is); 47 | imageView.setImage(image); 48 | } catch (IOException e) { 49 | logger.log(Level.WARNING, "Error opening - " + file, e); 50 | } 51 | } 52 | 53 | @Override 54 | public Pane getPane() { 55 | return hBox; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/resources/com/chainstaysoftware/filechooser/filechooser.css: -------------------------------------------------------------------------------- 1 | .toolbarbutton { 2 | -fx-background-color: transparent; 3 | } 4 | 5 | .toolbarbutton:hover { 6 | -fx-color: -fx-pressed-base; 7 | -fx-background-color: -fx-body-color; 8 | } 9 | 10 | .toolbartogglebutton { 11 | -fx-background-color: transparent; 12 | } 13 | 14 | .toolbartogglebutton:hover { 15 | -fx-color: -fx-pressed-base; 16 | -fx-background-color: -fx-body-color; 17 | } 18 | 19 | .toolbartogglebutton:selected { 20 | -fx-background-color: 21 | linear-gradient(to bottom, derive(-fx-outer-border, -20%), -fx-outer-border), 22 | linear-gradient(to bottom, 23 | derive(-fx-text-box-border, -10%) 0%, 24 | derive(-fx-text-box-border, -2%) 20%, 25 | derive(-fx-text-box-border, 5%) 50%); 26 | -fx-background-insets: 0, 1; 27 | } 28 | 29 | 30 | .extensionspane { 31 | -fx-padding: 5 10 2 10; 32 | } 33 | 34 | .extensionspane-buttonshbox { 35 | -fx-spacing: 5; 36 | } 37 | 38 | .buttonbar { 39 | -fx-padding: 3 10 7 10; 40 | } 41 | 42 | .propertiespreview-grid { 43 | -fx-padding: 0; 44 | -fx-hgap: 10; 45 | -fx-vgap: 2; 46 | } 47 | 48 | .propertiespreview-vbox { 49 | -fx-spacing: 5; 50 | -fx-background-color: 51 | linear-gradient(from 0px 0px to 0px 4px, derive(-fx-control-inner-background, -8%), -fx-control-inner-background); 52 | } 53 | 54 | .propertiespreview-label { 55 | -fx-font-weight: normal; 56 | } 57 | 58 | .propertiespreview-valuelabel { 59 | -fx-font-weight: bolder; 60 | } 61 | 62 | .propertiespreview-namevaluelabel { 63 | -fx-font-weight: bolder; 64 | -fx-font-size: 1.166667em; /* 14pt - 2 more than the default font */ 65 | } 66 | 67 | .tree-cell { 68 | -fx-padding: 1 0 1 0; 69 | -fx-indent: 2; 70 | } 71 | 72 | .places-droptarget { 73 | -fx-background-color: #FFFF66; 74 | } 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/preview/HeadPreviewPane.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.preview; 2 | 3 | import javafx.scene.control.TextArea; 4 | import javafx.scene.layout.BorderPane; 5 | import javafx.scene.layout.Pane; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.charset.Charset; 10 | import java.nio.file.Files; 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | import java.util.stream.Stream; 14 | 15 | /** 16 | * {@link PreviewPane} implementation for displaying N number of lines from the 17 | * head of text file types. 18 | */ 19 | public class HeadPreviewPane implements PreviewPane { 20 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.HeadPreviewWindow"); 21 | 22 | private static final Charset encoding = Charset.defaultCharset(); 23 | private static final int maxLines = 1000; 24 | 25 | private final BorderPane borderPane; 26 | private final TextArea textArea; 27 | 28 | public HeadPreviewPane() { 29 | this.textArea = new TextArea(); 30 | 31 | textArea.setId("headPreviewTextArea"); 32 | textArea.setEditable(false); 33 | textArea.setMinSize(0, 0); 34 | textArea.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE); 35 | 36 | borderPane = new BorderPane(); 37 | borderPane.setId("headPreviewPane"); 38 | borderPane.setCenter(textArea); 39 | } 40 | 41 | /** 42 | * Sets the file to display within the Pane. 43 | * 44 | * @param file 45 | */ 46 | @Override 47 | public void setFile(final File file) { 48 | try (Stream stream = Files.lines(file.toPath(), encoding)) { 49 | textArea.setText(stream.limit(maxLines).reduce((t, u) -> t + "\r\n" + u).orElse("")); 50 | } catch (IOException e) { 51 | logger.log(Level.WARNING, "Error reading file - " + file, e); 52 | // Error reading file. 53 | } 54 | } 55 | 56 | @Override 57 | public Pane getPane() { 58 | return borderPane; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/chainstaysoftware/filechooser/preview/PreviewPaneQueryTest.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.preview; 2 | 3 | 4 | import javafx.scene.layout.Pane; 5 | import org.assertj.core.api.Assertions; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import java.io.File; 9 | import java.util.AbstractMap; 10 | import java.util.Collections; 11 | import java.util.Map; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | 15 | class PreviewPaneQueryTest { 16 | private final Map> handlersMap 17 | = Collections.unmodifiableMap(Stream.of( 18 | new AbstractMap.SimpleEntry<>("text/plain", TxtPreviewPane.class)) 19 | .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, 20 | AbstractMap.SimpleEntry::getValue))); 21 | 22 | @Test 23 | void testQuery() { 24 | final Class txtPaneClass 25 | = PreviewPaneQuery.query(handlersMap, new File("./src/test/resources/com/chainstaysoftware/filechooser/empty.txt")); 26 | Assertions.assertThat(txtPaneClass) 27 | .describedAs("Txt pane should extend TxtPreviewPane") 28 | .isEqualTo(TxtPreviewPane.class); 29 | 30 | final Class unknownPaneClass 31 | = PreviewPaneQuery.query(handlersMap, new File("./src/test/resources/com/chainstaysoftware/filechooser/foo.bar")); 32 | Assertions.assertThat(unknownPaneClass) 33 | .describedAs("Unknown should return null pane") 34 | .isNull(); 35 | 36 | final Class dirPaneClass 37 | = PreviewPaneQuery.query(handlersMap, new File("./src/test/resources/com/chainstaysoftware/filechooser/dir1")); 38 | Assertions.assertThat(dirPaneClass) 39 | .describedAs("Unknown should return null pane") 40 | .isNull(); 41 | } 42 | 43 | private class TxtPreviewPane implements PreviewPane { 44 | @Override 45 | public void setFile(File file) { 46 | } 47 | 48 | @Override 49 | public Pane getPane() { 50 | return null; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FileChooserFx 2 | JavaFx based FileChooser and DirectoryChooser 3 | 4 | The FileChooser and DirectoryChooser implementations in JavaFx call out to OS native implementations. 5 | Native implementations are great, unless customization is needed. This project implements a FileChooser 6 | and DirectoryChooser completely in JavaFx code so that the implementations can be customized as needed. 7 | 8 | ## Limitations 9 | * The FileChooser does not support multifile select. 10 | * There are file previews implemented for jpg, png and text files. Other file previews can be plugged in. 11 | * Linux and OSX mount points can optionally be shown in the "Places" list. The code to determine the list of mount points is "experimental". Java does not include a good way of determining all the mount points and the types. The names and types may show differently than a native file browser. 12 | * Localization hooks are provided, but only English text is provided. 13 | 14 | ## Dependencies 15 | * Apache Commmons Lang and IO 16 | * ControlsFx - http://fxexperience.com/controlsfx/ 17 | * Icons from Icons8 - https://icons8.com 18 | 19 | ## Usage 20 | * See src/test/java/com/chainstaysoftware/filebrowser/FileChooserDemo.java for sample usage. 21 | * "--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED" should be passed on the Java command line to avoid an exception when Icon View is displayed. 22 | 23 | ## File Preview 24 | The preview code to execute for a file is determined from the file's mimetype. 25 | FileChooserFx uses the Java Files.probeContentType(Path path) method to determine 26 | the mimetype. 27 | 28 | The Sun Java OS-X implementation of probeContentType is problematic. The 29 | implementation will always return null to the probeContentType 30 | call if there is no ~/.mime.types file found. And, the ~/.mime.types file is 31 | not installed within OS-X by default. 32 | 33 | To work around the problem a ~/mime.types file can be created. One source of the 34 | file content can be found at 35 | http://svn.apache.org/viewvc/httpd/httpd/branches/2.2.x/docs/conf/mime.types?revision=1576707&view=co 36 | 37 | Alternately, a custom FileTypeDetector implementation could be installed. 38 | For an example look at https://odoepner.wordpress.com/2013/07/29/transparently-improve-java-7-mime-type-recognition-with-apache-tika/ 39 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/DirectoryWatcherTask.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.application.Platform; 4 | import javafx.concurrent.Task; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.nio.file.FileSystem; 9 | import java.nio.file.StandardWatchEventKinds; 10 | import java.nio.file.WatchKey; 11 | import java.nio.file.WatchService; 12 | import java.util.logging.Level; 13 | import java.util.logging.Logger; 14 | 15 | /** 16 | * Task to watch for file system changes. 17 | */ 18 | class DirectoryWatcherTask extends Task { 19 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.DirectoryWatcherTask"); 20 | 21 | private final File directory; 22 | private final FilesViewCallback callback; 23 | 24 | public DirectoryWatcherTask(final File directory, 25 | final FilesViewCallback callback) { 26 | this.directory = directory; 27 | this.callback = callback; 28 | } 29 | 30 | @Override 31 | protected Void call() { 32 | try (FileSystem fileSystem = directory.toPath().getFileSystem(); 33 | WatchService watcher = fileSystem.newWatchService()) { 34 | directory.toPath().register(watcher, 35 | StandardWatchEventKinds.ENTRY_CREATE, 36 | StandardWatchEventKinds.ENTRY_DELETE, 37 | StandardWatchEventKinds.ENTRY_MODIFY); 38 | while (!isCancelled()) { 39 | WatchKey key = watcher.take(); 40 | 41 | if (key.pollEvents().isEmpty()) { 42 | continue; 43 | } 44 | 45 | logger.log(Level.FINE, "Directory contents changed. Updating view."); 46 | Platform.runLater(callback::updateFiles); 47 | 48 | boolean valid = key.reset(); 49 | if (!valid) { 50 | break; 51 | } 52 | } 53 | } catch (InterruptedException e) { 54 | if (!isCancelled()) { 55 | logger.log(Level.FINE, "interrupted while waiting for key: " + e.getMessage()); 56 | } 57 | } catch (IOException e) { 58 | logger.log(Level.FINE, "watching " + directory + " failed: " + e.getMessage()); 59 | } 60 | 61 | logger.log(Level.FINE, "exiting watcher task"); 62 | return null; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/linux/LinuxFileSystem.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os.linux; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.nio.charset.Charset; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.Optional; 12 | import java.util.logging.Level; 13 | import java.util.logging.Logger; 14 | 15 | /** 16 | * Adapted from - https://gist.github.com/ikonst/3394662 17 | */ 18 | public class LinuxFileSystem { 19 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.os.linux.LinuxFileSystem"); 20 | 21 | /** 22 | * Retrieves the {@link MountInfo} for each entry in /proc/mounts. 23 | * An empty list is returned if /proc/mounts cannot be loaded. 24 | */ 25 | public List getMounts() { 26 | try (final InputStreamReader reader = new InputStreamReader(new FileInputStream("/proc/mounts"), 27 | Charset.defaultCharset())) { 28 | 29 | final List mounts = new ArrayList<>(); 30 | 31 | try (BufferedReader bufferedReader = new BufferedReader(reader)) { 32 | do { 33 | final String line = bufferedReader.readLine(); 34 | if (line == null) { 35 | break; 36 | } 37 | 38 | getMountInfo(line).ifPresent(mounts::add); 39 | } while (true); 40 | 41 | return mounts; 42 | } 43 | } catch (IOException e) { 44 | logger.warning("Unable to open /proc/mounts to get mountpoint info"); 45 | return Collections.emptyList(); 46 | } 47 | } 48 | 49 | private Optional getMountInfo(final String line) { 50 | String[] parts = line.split(" "); 51 | if (parts.length < 6) { 52 | return Optional.empty(); 53 | } 54 | 55 | try { 56 | final String device = parts[0]; 57 | final String mountpoint = parts[1].replace("\\040", " "); 58 | final String fs = parts[2]; 59 | final String options = parts[3]; 60 | final int fs_freq = Integer.parseInt(parts[4]); 61 | final int fs_passno = Integer.parseInt(parts[5]); 62 | 63 | return Optional.of(new MountInfo(device, mountpoint, fs, options, fs_freq, fs_passno)); 64 | } catch (NumberFormatException e) { 65 | logger.log(Level.WARNING, "Error parsing fs_freq or fs_passno", e); 66 | return Optional.empty(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/FilterHiddenFromDirList.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.application.Platform; 4 | import javafx.concurrent.Service; 5 | import javafx.concurrent.Task; 6 | 7 | import java.io.File; 8 | import java.util.LinkedList; 9 | import java.util.List; 10 | import java.util.concurrent.CountDownLatch; 11 | import java.util.function.Predicate; 12 | 13 | /** 14 | * JavaFx Service to possibly filter out hidden files from 15 | * a {@link List} of {@link DirectoryListItem}. This code is 16 | * not run on the main JavaFx thread to avoid calling the slow 17 | * File.isHidden method. 18 | */ 19 | class FilterHiddenFromDirList extends Service { 20 | private final List itemList; 21 | private final Predicate shouldHideFile; 22 | private final CountDownLatch latch = new CountDownLatch(1); 23 | 24 | FilterHiddenFromDirList(final List itemList, 25 | final Predicate shouldHideFile) { 26 | this.itemList = itemList; 27 | this.shouldHideFile = shouldHideFile; 28 | } 29 | 30 | protected Task createTask() { 31 | return new UpdateListTask(); 32 | } 33 | 34 | private class UpdateListTask extends Task { 35 | @Override 36 | protected Void call() throws Exception { 37 | filterHidden(); 38 | 39 | latch.await(); 40 | 41 | return null; 42 | } 43 | 44 | // Move filter and icon update to another thread. 45 | private void filterHidden() { 46 | final List filterItems = new LinkedList<>(); 47 | 48 | for (DirectoryListItem item : itemList) { 49 | if (isCancelled()) { 50 | return; 51 | } 52 | 53 | if (!shouldHideFile.test(item.getFile())) { 54 | filterItems.add(item); 55 | 56 | if (shouldSchedule(filterItems)) { 57 | scheduleJavaFx(filterItems); 58 | filterItems.clear(); 59 | } 60 | } 61 | } 62 | 63 | scheduleJavaFx(filterItems); 64 | Platform.runLater(latch::countDown); 65 | } 66 | 67 | private boolean shouldSchedule(final List items) { 68 | return items.size() % 100 == 0; 69 | } 70 | 71 | private void scheduleJavaFx(final List items) { 72 | final List temp = new LinkedList<>(); 73 | temp.addAll(items); 74 | 75 | Platform.runLater(() -> temp.forEach(itemList::remove)); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/FilterHiddenFromDirTree.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.application.Platform; 4 | import javafx.concurrent.Service; 5 | import javafx.concurrent.Task; 6 | import javafx.scene.control.TreeItem; 7 | 8 | import java.io.File; 9 | import java.util.LinkedList; 10 | import java.util.List; 11 | import java.util.concurrent.CountDownLatch; 12 | import java.util.function.Predicate; 13 | 14 | /** 15 | * JavaFx Service to possibly filter out hidden files from 16 | * a {@link List} of {@link TreeItem}. This code is 17 | * not run on the main JavaFx thread to avoid calling the slow 18 | * File.isHidden method. 19 | */ 20 | class FilterHiddenFromDirTree extends Service { 21 | private final List> itemList; 22 | private final Predicate shouldHideFile; 23 | 24 | private final CountDownLatch latch = new CountDownLatch(1); 25 | 26 | FilterHiddenFromDirTree(final List> itemList, 27 | final Predicate shouldHideFile) { 28 | this.itemList = itemList; 29 | this.shouldHideFile = shouldHideFile; 30 | } 31 | 32 | protected Task createTask() { 33 | return new UpdateListTask(); 34 | } 35 | 36 | private class UpdateListTask extends Task { 37 | @Override 38 | protected Void call() throws Exception { 39 | filterHidden(); 40 | 41 | latch.await(); 42 | 43 | return null; 44 | } 45 | 46 | // Move filter and icon update to another thread. 47 | private void filterHidden() { 48 | final List> filterItems = new LinkedList<>(); 49 | 50 | for (TreeItem item : itemList) { 51 | if (isCancelled()) { 52 | return; 53 | } 54 | 55 | if (!shouldHideFile.test(item.getValue())) { 56 | filterItems.add(item); 57 | 58 | if (shouldSchedule(filterItems)) { 59 | scheduleJavaFx(filterItems); 60 | filterItems.clear(); 61 | } 62 | } 63 | } 64 | 65 | scheduleJavaFx(filterItems); 66 | Platform.runLater(latch::countDown); 67 | } 68 | 69 | private boolean shouldSchedule(final List> items) { 70 | return items.size() % 100 == 0; 71 | } 72 | 73 | private void scheduleJavaFx(final List> items) { 74 | final List> temp = new LinkedList<>(); 75 | temp.addAll(items); 76 | 77 | Platform.runLater(() -> temp.forEach(itemList::remove)); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/osx/OsxPlacesProvider.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os.osx; 2 | 3 | import com.chainstaysoftware.filechooser.os.Place; 4 | import com.chainstaysoftware.filechooser.os.PlaceType; 5 | import com.chainstaysoftware.filechooser.os.PlacesProvider; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.Files; 10 | import java.util.Arrays; 11 | import java.util.LinkedList; 12 | import java.util.List; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | 16 | /** 17 | * {@link PlacesProvider} for OS X. NOTE - This may not be the best way to 18 | * get this info from OS X. Java does not make it easy.. 19 | */ 20 | public class OsxPlacesProvider implements PlacesProvider { 21 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.os.osx.OsxPlacesProvider"); 22 | 23 | private static final List networkFsTypes = Arrays.asList("afpfs", "cifs", "ncpfs", "nfs", "smb", "smbfs"); 24 | private static final List cdFsTypes = Arrays.asList("cd9660"); 25 | 26 | /** 27 | * Return the default list of {@link Place} for the supporting OS instance. 28 | */ 29 | @Override 30 | public List getDefaultPlaces() { 31 | final List places = new LinkedList<>(); 32 | 33 | final File volumes = new File("/Volumes"); 34 | final String[] children = volumes.list(); 35 | 36 | Arrays.stream(children) 37 | .forEach(s -> { 38 | try { 39 | File path = new File(volumes, s); 40 | if (Files.isSymbolicLink(path.toPath())) { 41 | path = Files.readSymbolicLink(path.toPath()).toFile(); 42 | } 43 | 44 | places.add(new Place(toPlaceType(path), path)); 45 | } catch (IOException e) { 46 | logger.log(Level.WARNING, "Error finding OS X mounts", e); 47 | } 48 | }); 49 | 50 | return places; 51 | } 52 | 53 | private PlaceType toPlaceType(final File path) 54 | throws IOException { 55 | final String fsType = Files.getFileStore(path.toPath()).type(); 56 | if (cdFsTypes.contains(fsType)) { 57 | return PlaceType.Cd; 58 | } 59 | 60 | if (networkFsTypes.contains(fsType)) { 61 | return PlaceType.Network; 62 | } 63 | 64 | // Code is assuming that "msdos" is a Usb drive.. This probably 65 | // isn't 100% correct. Haven't been able to find a better solution. 66 | if ("msdos".equals(fsType)) { 67 | return PlaceType.Usb; 68 | } 69 | 70 | return PlaceType.HardDisk; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/DirectoryTreeItem.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.collections.FXCollections; 4 | import javafx.collections.ObservableList; 5 | import javafx.scene.control.TreeItem; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.DirectoryStream; 10 | import java.nio.file.Path; 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | class DirectoryTreeItem extends TreeItem { 15 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.DirectoryTreeItem"); 16 | 17 | private final FilesViewCallback callback; 18 | private final PopulateTreeItemRunnableFactory factory; 19 | 20 | // Cache isLeaf, lastModified and length so that sorting by either field is performant on slow filesystems. 21 | private Boolean isLeaf; 22 | private Long lastModified; 23 | private Long length; 24 | 25 | private boolean directoryListLoaded = false; 26 | 27 | DirectoryTreeItem(final File value, 28 | final FilesViewCallback callback, 29 | final PopulateTreeItemRunnableFactory factory) { 30 | super(value, null); 31 | 32 | this.callback = callback; 33 | this.factory = factory; 34 | } 35 | 36 | @Override 37 | public boolean isLeaf() { 38 | if (isLeaf == null) { 39 | isLeaf = !getValue().isDirectory(); 40 | } 41 | 42 | return isLeaf; 43 | } 44 | 45 | @Override 46 | public ObservableList> getChildren() { 47 | if (isLeaf()) { 48 | return FXCollections.observableArrayList(); 49 | } 50 | 51 | if (!directoryListLoaded) { 52 | loadChildren(); 53 | } 54 | 55 | return super.getChildren(); 56 | } 57 | 58 | private void loadChildren() { 59 | directoryListLoaded = true; 60 | 61 | try { 62 | final DirectoryStream filteredStream = callback.getDirectoryStream(getValue()); 63 | final DirectoryStream unfilteredStream = callback.unfilteredDirectoryStream(getValue()); 64 | final Runnable runnable = factory.create(filteredStream, unfilteredStream, this); 65 | runnable.run(); 66 | 67 | } catch (IOException e) { 68 | logger.log(Level.WARNING, "Error loading directory children.", e); 69 | } 70 | } 71 | 72 | long length() { 73 | if (length == null) { 74 | length = getValue().length(); 75 | } 76 | 77 | return length; 78 | } 79 | 80 | long lastModified() { 81 | if (lastModified == null) { 82 | lastModified = getValue().lastModified(); 83 | } 84 | 85 | return lastModified; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/IconGridCell.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.icons.Icons; 4 | import com.chainstaysoftware.filechooser.icons.IconsImpl; 5 | import javafx.geometry.Pos; 6 | import javafx.scene.control.ContentDisplay; 7 | import javafx.scene.control.OverrunStyle; 8 | import javafx.scene.image.Image; 9 | import javafx.scene.image.ImageView; 10 | import org.controlsfx.control.GridCell; 11 | 12 | class IconGridCell extends GridCell { 13 | private final ImageView imageView; 14 | private final boolean preserveImageProperties; 15 | private final IconGridCellContextMenuFactory contextMenuFactory; 16 | private final Icons icons; 17 | 18 | IconGridCell(final boolean preserveImageProperties, 19 | final IconGridCellContextMenuFactory contextMenuFactory, 20 | final Icons icons) { 21 | getStyleClass().add("image-grid-cell"); 22 | 23 | this.preserveImageProperties = preserveImageProperties; 24 | this.icons = icons; 25 | 26 | imageView = new ImageView(); 27 | imageView.fitHeightProperty().bind(heightProperty().subtract(40)); 28 | imageView.fitWidthProperty().bind(widthProperty()); 29 | 30 | setContentDisplay(ContentDisplay.TOP); 31 | setWrapText(true); 32 | setAlignment(Pos.TOP_CENTER); 33 | 34 | this.contextMenuFactory = contextMenuFactory; 35 | } 36 | 37 | @Override 38 | protected void updateItem(final DirectoryListItem item, 39 | final boolean empty) { 40 | super.updateItem(item, empty); 41 | 42 | setContextMenu(null); 43 | 44 | if (empty) { 45 | setGraphic(null); 46 | setText(null); 47 | setUserData(null); 48 | } else { 49 | final String text = item.getFile().getName(); 50 | 51 | final Image image = item.isDirectory() 52 | ? icons.getIcon(IconsImpl.FOLDER_64) 53 | : icons.getIconForFile(item.getFile()); 54 | 55 | if (preserveImageProperties) { 56 | imageView.setPreserveRatio(true); 57 | imageView.setSmooth(image.isSmooth()); 58 | } 59 | imageView.setImage(image); 60 | 61 | setGraphic(imageView); 62 | setText(text); 63 | setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); 64 | setUserData(item.getFile()); 65 | 66 | setOnContextMenuRequested(event -> { 67 | if (contextMenuFactory != null) { 68 | contextMenuFactory.create(item).show(this, event.getScreenX(), event.getScreenY()); 69 | } 70 | }); 71 | } 72 | } 73 | 74 | @Override 75 | public void updateSelected(final boolean selected) { 76 | setStyle(selected ? "-fx-border-color: -fx-cell-focus-inner-border;" : null); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/FileMetaDataComparator.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import org.apache.commons.io.FilenameUtils; 4 | 5 | import java.io.File; 6 | import java.io.Serializable; 7 | import java.util.Comparator; 8 | 9 | /** 10 | * Comparator for {@link File} that allows specification of the {@link OrderBy} 11 | * to sort on. 12 | */ 13 | public class FileMetaDataComparator implements Comparator, Serializable { 14 | private static final long serialVersionUID = 8867211248432156391L; 15 | 16 | private final OrderBy orderBy; 17 | private final OrderDirection direction; 18 | 19 | public FileMetaDataComparator(final OrderBy orderBy, 20 | final OrderDirection direction) { 21 | this.orderBy = orderBy; 22 | this.direction = direction; 23 | } 24 | 25 | @Override 26 | public int compare(final File o1, final File o2) { 27 | if (OrderBy.ModificationDate.equals(orderBy)) { 28 | return getAnswer(compareByDate(o1, o2)); 29 | } 30 | 31 | if (OrderBy.Size.equals(orderBy)) { 32 | return getAnswer(compareBySize(o1, o2)); 33 | } 34 | 35 | if (OrderBy.Type.equals(orderBy)) { 36 | return getAnswer(compareByType(o1, o2)); 37 | } 38 | 39 | return getAnswer(compareByName(o1, o2)); 40 | } 41 | 42 | private int compareByName(final File o1, final File o2) { 43 | return o1.getName().compareTo(o2.getName()); 44 | } 45 | 46 | private int compareByType(final File o1, final File o2) { 47 | if (o1.isDirectory()) { 48 | if (o2.isDirectory()) { 49 | return o1.compareTo(o2); 50 | } 51 | 52 | return -1; 53 | } else if (o2.isDirectory()) { 54 | return 1; 55 | } 56 | 57 | final String extension1 = FilenameUtils.getExtension(o1.getName()); 58 | final String extension2 = FilenameUtils.getExtension(o2.getName()); 59 | return extension1.compareTo(extension2); 60 | } 61 | 62 | private int compareBySize(final File o1, final File o2) { 63 | // treat directories as zero length. The length method on file does not 64 | // return the total size of the directory contents. 65 | 66 | if (o1.isDirectory()) { 67 | if (o2.isDirectory()) { 68 | return compareByName(o1, o2); 69 | } 70 | 71 | return -1; 72 | } 73 | 74 | if (o2.isDirectory()) { 75 | return 1; 76 | } 77 | 78 | if (o1.length() < o2.length()) { 79 | return -1; 80 | } 81 | 82 | return o1.length() > o2.length() ? 1 : 0; 83 | } 84 | 85 | private int compareByDate(final File o1, final File o2) { 86 | if (o1.lastModified() < o2.lastModified()) { 87 | return -1; 88 | } 89 | 90 | if (o1.lastModified() > o2.lastModified()) { 91 | return 1; 92 | } 93 | 94 | return 0; 95 | } 96 | 97 | private int getAnswer(final int ascending) { 98 | if (OrderDirection.Ascending.equals(direction)) { 99 | return ascending; 100 | } 101 | 102 | return -ascending; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/DirListNameColumnCellFactory.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.icons.Icons; 4 | import com.chainstaysoftware.filechooser.icons.IconsImpl; 5 | import javafx.scene.control.TableCell; 6 | import javafx.scene.control.TableColumn; 7 | import javafx.scene.image.Image; 8 | import javafx.scene.image.ImageView; 9 | import javafx.util.Callback; 10 | 11 | import javax.swing.filechooser.FileSystemView; 12 | import java.io.File; 13 | 14 | /** 15 | * CellFactory for {@link DirectoryListItem#getFile()} value. 16 | */ 17 | class DirListNameColumnCellFactory 18 | implements Callback, TableCell> { 19 | 20 | private final boolean nameOnly; 21 | private final FilesViewCallback callback; 22 | private final Icons icons; 23 | 24 | /** 25 | * Constructor 26 | * @param nameOnly Indicates if the cell text should include the file.getPath() value 27 | * or file.getName() 28 | * @param callback {@link FilesViewCallback} 29 | * @param icons Icon retriever for File objects. 30 | */ 31 | DirListNameColumnCellFactory(final boolean nameOnly, 32 | final FilesViewCallback callback, 33 | final Icons icons) { 34 | this.nameOnly = nameOnly; 35 | this.callback = callback; 36 | this.icons = icons; 37 | } 38 | 39 | @Override 40 | public TableCell call(TableColumn param) { 41 | return new DirListTableCell(); 42 | } 43 | 44 | private class DirListTableCell extends TableCell { 45 | @Override 46 | protected void updateItem(DirectoryListItem item, boolean empty) { 47 | super.updateItem(item, empty); 48 | 49 | if (empty || item == null) { 50 | setText(null); 51 | setGraphic(null); 52 | setOnMouseClicked(null); 53 | } else { 54 | if (nameOnly) { 55 | final String systemDisplayName = FileSystemView.getFileSystemView().getSystemDisplayName(item.getFile()); 56 | setText("".equals(systemDisplayName) ? item.getFile().toString() : systemDisplayName); 57 | } else { 58 | setText(item.getFile().toString()); 59 | } 60 | 61 | final Image image = item.isDirectory() 62 | ? icons.getIcon(IconsImpl.FOLDER_64) 63 | : icons.getIconForFile(item.getFile()); 64 | final ImageView graphic = new ImageView(image); 65 | graphic.setFitHeight(IconsImpl.SMALL_ICON_HEIGHT); 66 | graphic.setFitWidth(IconsImpl.SMALL_ICON_WIDTH); 67 | graphic.setPreserveRatio(true); 68 | setGraphic(graphic); 69 | 70 | setOnMouseClicked(event -> { 71 | final File file = item.getFile(); 72 | if (event.getClickCount() < 2) { 73 | return; 74 | } 75 | 76 | if (file.isDirectory()) { 77 | callback.requestChangeDirectory(file); 78 | } else { 79 | callback.fireDoneButton(); 80 | } 81 | }); 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/os/linux/LinuxPlacesProvider.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.os.linux; 2 | 3 | import com.chainstaysoftware.filechooser.os.Place; 4 | import com.chainstaysoftware.filechooser.os.PlaceType; 5 | import com.chainstaysoftware.filechooser.os.PlacesProvider; 6 | import com.chainstaysoftware.filechooser.os.def.DefaultPlacesProvider; 7 | 8 | import java.io.File; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.function.Predicate; 12 | import java.util.stream.Collectors; 13 | 14 | /** 15 | * {@link PlacesProvider} for Linux. NOTE - This may not be the best way to 16 | * get this info from Linux. Java does not make it easy.. 17 | */ 18 | public class LinuxPlacesProvider implements PlacesProvider { 19 | // List of Linux file systems that we want to show. Any others will be filtered out. 20 | private static final List supportedFsTypes = Arrays.asList("btrfs", "cifs", "exfat", "ext", "ext2", "ext3", "ext4", 21 | "f2fs", "hfs", "hpfs", "iso9660", "jfs", "minix", "msdos", "ncpfs", "nfs", "ntfs", "prl_fs", "reiser4", "reiserFS", 22 | "smb", "smbfs", "umsdos", "vfat", "xfs", "zfs"); 23 | private static final List networkFsTypes = Arrays.asList("cifs", "ncpfs", "nfs", "smb", "smbfs"); 24 | private static final List cdFsTypes = Arrays.asList("iso9660"); 25 | 26 | private final LinuxFileSystem linuxFileSystem = new LinuxFileSystem(); 27 | /** 28 | * Return the default list of {@link Place} for the supporting OS instance. 29 | * Only returns Roots and assumes all are {@link PlaceType#HardDisk} 30 | */ 31 | @Override 32 | public List getDefaultPlaces() { 33 | // File.listRoots() only returns "/" on Linux. So, if possible, get the 34 | // mounted drives from /proc/mounts 35 | final List mountInfos = linuxFileSystem.getMounts(); 36 | if (mountInfos.isEmpty()) { 37 | return new DefaultPlacesProvider().getDefaultPlaces(); 38 | } 39 | 40 | return mountInfos.stream() 41 | .filter(new ShouldInclude()) 42 | .map(mountInfo -> new Place(toPlaceType(mountInfo), new File(mountInfo.getMountpoint()))) 43 | .collect(Collectors.toList()); 44 | } 45 | 46 | /** 47 | * Attempt to map a {@link MountInfo} to a {@link PlaceType} 48 | */ 49 | private PlaceType toPlaceType(final MountInfo mountInfo) { 50 | if (networkFsTypes.contains(mountInfo.getFs())) { 51 | return PlaceType.Network; 52 | } 53 | 54 | if (cdFsTypes.contains(mountInfo.getFs())) { 55 | return PlaceType.Cd; 56 | } 57 | 58 | // Is there a better way to determine if a drive is connected over USB? 59 | // The Swing FileSystemView seems to be unreliable. 60 | if (mountInfo.getDevice().startsWith("/dev") && mountInfo.getMountpoint().startsWith("/media")) { 61 | // Assuming if in media dir, and not a CD or DVD then it must be Usb. 62 | return PlaceType.Usb; 63 | } 64 | 65 | return PlaceType.HardDisk; 66 | } 67 | 68 | /** 69 | * Predicate to determine if a {@link MountInfo} should be included within the Places view. 70 | * This code tries to filter out the "special" mounts such as "proc", "rootfs", etc. 71 | */ 72 | private static class ShouldInclude implements Predicate { 73 | @Override 74 | public boolean test(MountInfo mountInfo) { 75 | return supportedFsTypes.contains(mountInfo.getFs().toLowerCase()) 76 | && !"/boot".equals(mountInfo.getMountpoint()) 77 | && !"/home".equals(mountInfo.getMountpoint()); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/DirOrWildcardFilter.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import org.apache.commons.io.IOCase; 4 | import org.apache.commons.io.filefilter.WildcardFileFilter; 5 | 6 | import java.io.File; 7 | import java.util.List; 8 | 9 | /** 10 | * WildcardFilter that accepts all directory entries. 11 | */ 12 | public class DirOrWildcardFilter extends WildcardFileFilter { 13 | /** 14 | * Construct a new case-sensitive wildcard filter for a single wildcard. 15 | * 16 | * @param wildcard the wildcard to match 17 | * @throws IllegalArgumentException if the pattern is null 18 | */ 19 | public DirOrWildcardFilter(String wildcard) { 20 | super(wildcard); 21 | } 22 | 23 | /** 24 | * Construct a new wildcard filter for a single wildcard specifying case-sensitivity. 25 | * 26 | * @param wildcard the wildcard to match, not null 27 | * @param caseSensitivity how to handle case sensitivity, null means case-sensitive 28 | * @throws IllegalArgumentException if the pattern is null 29 | */ 30 | public DirOrWildcardFilter(String wildcard, IOCase caseSensitivity) { 31 | super(wildcard, caseSensitivity); 32 | } 33 | 34 | /** 35 | * Construct a new case-sensitive wildcard filter for an array of wildcards. 36 | *

37 | * The array is not cloned, so could be changed after constructing the 38 | * instance. This would be inadvisable however. 39 | * 40 | * @param wildcards the array of wildcards to match 41 | * @throws IllegalArgumentException if the pattern array is null 42 | */ 43 | public DirOrWildcardFilter(String[] wildcards) { 44 | super(wildcards); 45 | } 46 | 47 | /** 48 | * Construct a new wildcard filter for an array of wildcards specifying case-sensitivity. 49 | *

50 | * The array is not cloned, so could be changed after constructing the 51 | * instance. This would be inadvisable however. 52 | * 53 | * @param wildcards the array of wildcards to match, not null 54 | * @param caseSensitivity how to handle case sensitivity, null means case-sensitive 55 | * @throws IllegalArgumentException if the pattern array is null 56 | */ 57 | public DirOrWildcardFilter(String[] wildcards, IOCase caseSensitivity) { 58 | super(wildcards, caseSensitivity); 59 | } 60 | 61 | /** 62 | * Construct a new case-sensitive wildcard filter for a list of wildcards. 63 | * 64 | * @param wildcards the list of wildcards to match, not null 65 | * @throws IllegalArgumentException if the pattern list is null 66 | * @throws ClassCastException if the list does not contain Strings 67 | */ 68 | public DirOrWildcardFilter(List wildcards) { 69 | super(wildcards); 70 | } 71 | 72 | /** 73 | * Construct a new wildcard filter for a list of wildcards specifying case-sensitivity. 74 | * 75 | * @param wildcards the list of wildcards to match, not null 76 | * @param caseSensitivity how to handle case sensitivity, null means case-sensitive 77 | * @throws IllegalArgumentException if the pattern list is null 78 | * @throws ClassCastException if the list does not contain Strings 79 | */ 80 | public DirOrWildcardFilter(List wildcards, IOCase caseSensitivity) { 81 | super(wildcards, caseSensitivity); 82 | } 83 | 84 | /** 85 | * Checks to see if the filename matches one of the wildcards. Or, the 86 | * filename refers to a directory. 87 | * 88 | * @param file the file to check 89 | * @return true if the filename matches one of the wildcards 90 | */ 91 | @Override 92 | public boolean accept(File file) { 93 | return super.accept(file) || file.isDirectory(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/AbstractFilesView.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.preview.PreviewPane; 4 | import javafx.application.Platform; 5 | import javafx.scene.Cursor; 6 | import javafx.scene.Node; 7 | import javafx.scene.Scene; 8 | import javafx.scene.input.KeyCode; 9 | import javafx.scene.layout.Pane; 10 | import javafx.stage.Modality; 11 | import javafx.stage.Stage; 12 | import javafx.stage.StageStyle; 13 | import org.apache.commons.lang3.StringUtils; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.util.Optional; 18 | import java.util.logging.Level; 19 | import java.util.logging.Logger; 20 | 21 | abstract class AbstractFilesView implements FilesView { 22 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.AbstractFilesView"); 23 | 24 | private final Stage parent; 25 | 26 | AbstractFilesView(final Stage parent) { 27 | this.parent = parent; 28 | } 29 | 30 | /** 31 | * Create {@link Stage} to display {@link PreviewPane} and show the {@link Stage} 32 | * @param previewPaneClass {@link Node} to display the file within. 33 | * @param file {@link File} to preview. 34 | */ 35 | void showPreview(final Class previewPaneClass, 36 | final File file) { 37 | final Optional previewPaneOpt = PreviewPaneFactory.create(previewPaneClass); 38 | if (!previewPaneOpt.isPresent()) { 39 | logger.log(Level.SEVERE, "No PreviewPane created."); 40 | return; 41 | } 42 | 43 | parent.getScene().setCursor(Cursor.WAIT); 44 | 45 | Platform.runLater(new FilesViewRunnable(previewPaneOpt.orElseThrow(IllegalStateException::new), 46 | file)); 47 | } 48 | 49 | private class FilesViewRunnable implements Runnable { 50 | private final PreviewPane previewPane; 51 | private final File file; 52 | 53 | FilesViewRunnable(final PreviewPane previewPane, 54 | final File file) { 55 | this.previewPane = previewPane; 56 | this.file = file; 57 | } 58 | 59 | @Override 60 | public void run() { 61 | final Stage stage = new Stage(); 62 | final Pane pane = previewPane.getPane(); 63 | pane.prefWidthProperty().bind(stage.widthProperty()); 64 | pane.prefHeightProperty().bind(stage.heightProperty()); 65 | pane.maxWidthProperty().bind(stage.widthProperty()); 66 | pane.maxHeightProperty().bind(stage.heightProperty()); 67 | pane.minWidthProperty().bind(stage.widthProperty()); 68 | pane.minHeightProperty().bind(stage.heightProperty()); 69 | 70 | final Scene scene = new Scene(new Pane(pane)); 71 | scene.getStylesheets().add(new FileBrowserCss().getUrl()); 72 | scene.setOnKeyPressed(event -> { 73 | if (event.getCode() == KeyCode.ESCAPE) { 74 | stage.close(); 75 | } 76 | }); 77 | 78 | previewPane.setFile(file); 79 | 80 | stage.setScene(scene); 81 | stage.initOwner(parent); 82 | stage.setAlwaysOnTop(true); 83 | stage.initStyle(StageStyle.UTILITY); 84 | stage.initModality(Modality.APPLICATION_MODAL); 85 | stage.setTitle(getTitle(file)); 86 | stage.setWidth(1024); 87 | stage.setHeight(768); 88 | stage.setOnShown(windowEvent -> parent.getScene().setCursor(null)); 89 | stage.show(); 90 | } 91 | 92 | /** 93 | * Build a window title for the passed in file. 94 | * @param file 95 | */ 96 | private String getTitle(final File file) { 97 | final int maxLength = 75; 98 | return StringUtils.abbreviateMiddle(file.getPath(), "...", maxLength); 99 | } 100 | } 101 | 102 | boolean compareFilePaths(final File f1, final File f2) { 103 | if (f1 == null || f2 == null) { 104 | return false; 105 | } 106 | 107 | try { 108 | return f1.getCanonicalFile().equals(f2.getCanonicalFile()); 109 | } catch (IOException e) { 110 | logger.log(Level.SEVERE, "Error canonicalizing file", e); 111 | return f1.equals(f2); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/UpdateDirectoryList.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.application.Platform; 4 | import javafx.concurrent.Service; 5 | import javafx.concurrent.Task; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.nio.file.DirectoryStream; 10 | import java.nio.file.Path; 11 | import java.util.LinkedList; 12 | import java.util.List; 13 | import java.util.concurrent.CountDownLatch; 14 | import java.util.logging.Level; 15 | import java.util.logging.Logger; 16 | 17 | /** 18 | * JavaFx Service to update a {@link List} of {@link DirectoryListItem} with 19 | * the files/directories found at the passed in {@link DirectoryStream}. This code 20 | * is not run on the JavaFx thread so that the UI does not block while retreiving the 21 | * list of files from the OS. 22 | */ 23 | class UpdateDirectoryList extends Service { 24 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.UpdateDirectoryList"); 25 | 26 | private final DirectoryStream dirStream; 27 | private final DirectoryStream unfilteredDirStream; 28 | private final List itemList; 29 | private final CountDownLatch latch = new CountDownLatch(1); 30 | 31 | /** 32 | * Constructor 33 | * @param dirStream Stream of all files/directories that matched the used file filter. 34 | * @param unfilteredDirStream Stream of all files/directories that did NOT match the used file 35 | * filter. This is used to find directories that were excluded 36 | * by the filter. 37 | * @param itemList List to update with results. 38 | */ 39 | UpdateDirectoryList(final DirectoryStream dirStream, 40 | final DirectoryStream unfilteredDirStream, 41 | final List itemList) { 42 | this.dirStream = dirStream; 43 | this.unfilteredDirStream = unfilteredDirStream; 44 | this.itemList = itemList; 45 | } 46 | 47 | protected Task createTask() { 48 | return new UpdateListTask(); 49 | } 50 | 51 | private class UpdateListTask extends Task { 52 | @Override 53 | protected Void call() throws Exception { 54 | update(dirStream, false); 55 | if (isCancelled()) { 56 | return null; 57 | } 58 | 59 | update(unfilteredDirStream, true); 60 | 61 | Platform.runLater(latch::countDown); 62 | 63 | latch.await(); 64 | 65 | return null; 66 | } 67 | 68 | private void update(final DirectoryStream directoryStream, 69 | final boolean dirOnly) { 70 | final List itemsToAdd = new LinkedList<>(); 71 | 72 | try { 73 | for (Path path: directoryStream) { 74 | if (isCancelled()) { 75 | return; 76 | } 77 | 78 | if (!dirOnly || path.toFile().isDirectory()) { 79 | final DirectoryListItem dirListItem = getDirListItem(path.toFile()); 80 | itemsToAdd.add(dirListItem); 81 | 82 | if (shouldSchedule(itemsToAdd)) { 83 | scheduleJavaFx(itemsToAdd); 84 | itemsToAdd.clear(); 85 | } 86 | } 87 | } 88 | 89 | scheduleJavaFx(itemsToAdd); 90 | } finally { 91 | closeStream(dirStream); 92 | } 93 | } 94 | 95 | private boolean shouldSchedule(final List itemsToUpdate) { 96 | return itemsToUpdate.size() % 100 == 0; 97 | } 98 | 99 | private void scheduleJavaFx(final List itemsToUpdate) { 100 | final List temp = new LinkedList<>(); 101 | temp.addAll(itemsToUpdate); 102 | 103 | Platform.runLater(() -> itemList.addAll(temp)); 104 | } 105 | 106 | private DirectoryListItem getDirListItem(final File file) { 107 | return new DirectoryListItem(file); 108 | } 109 | 110 | private void closeStream(final DirectoryStream directoryStream) { 111 | try { 112 | directoryStream.close(); 113 | } catch (IOException e) { 114 | logger.log(Level.WARNING, "Error closing directory stream", e); 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/UpdateDirectoryTree.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.application.Platform; 4 | import javafx.concurrent.Service; 5 | import javafx.concurrent.Task; 6 | import javafx.scene.control.TreeItem; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.nio.file.DirectoryStream; 11 | import java.nio.file.Path; 12 | import java.util.LinkedList; 13 | import java.util.List; 14 | import java.util.concurrent.CountDownLatch; 15 | import java.util.logging.Level; 16 | import java.util.logging.Logger; 17 | 18 | /** 19 | * JavaFx Service to update a {@link List} of {@link TreeItem} with 20 | * the files/directories found at the passed in {@link DirectoryStream}. This code 21 | * is not run on the JavaFx thread so that the UI does not block while retreiving the 22 | * list of files from the OS. 23 | */ 24 | class UpdateDirectoryTree extends Service { 25 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.UpdateDirectoryTree"); 26 | 27 | private final DirectoryStream dirStream; 28 | private final DirectoryStream unfilteredDirStream; 29 | private final List> itemList; 30 | private final FilesViewCallback callback; 31 | private final PopulateTreeItemRunnableFactory populateFactory; 32 | 33 | private final CountDownLatch latch = new CountDownLatch(1); 34 | 35 | /** 36 | * Constructor 37 | * @param dirStream Stream of all files/directories that matched the used file filter. 38 | * @param unfilteredDirStream Stream of all files/directories that did NOT match the used file 39 | * filter. This is used to find directories that were excluded 40 | * by the filter. 41 | * @param itemList List to update with results. 42 | * @param callback {@link FilesViewCallback} impl. 43 | * @param populateFactory factory for creating {@link com.chainstaysoftware.filechooser.ListFilesView.PopulateTreeItemRunnable} 44 | * instances. 45 | */ 46 | UpdateDirectoryTree(final DirectoryStream dirStream, 47 | final DirectoryStream unfilteredDirStream, 48 | final List> itemList, 49 | final FilesViewCallback callback, 50 | final PopulateTreeItemRunnableFactory populateFactory) { 51 | this.dirStream = dirStream; 52 | this.unfilteredDirStream = unfilteredDirStream; 53 | this.itemList = itemList; 54 | this.callback = callback; 55 | this.populateFactory = populateFactory; 56 | } 57 | 58 | protected Task createTask() { 59 | return new UpdateListTask(); 60 | } 61 | 62 | private class UpdateListTask extends Task { 63 | @Override 64 | protected Void call() throws Exception { 65 | update(dirStream, false); 66 | if (isCancelled()) { 67 | return null; 68 | } 69 | 70 | update(unfilteredDirStream, true); 71 | 72 | Platform.runLater(latch::countDown); 73 | 74 | latch.await(); 75 | 76 | return null; 77 | } 78 | 79 | private void update(final DirectoryStream directoryStream, 80 | final boolean dirOnly) { 81 | final List> itemsToAdd = new LinkedList<>(); 82 | 83 | try { 84 | for (Path path: directoryStream) { 85 | if (isCancelled()) { 86 | return; 87 | } 88 | 89 | if (!dirOnly || path.toFile().isDirectory()) { 90 | final TreeItem treeItem = getTreeItem(path.toFile()); 91 | itemsToAdd.add(treeItem); 92 | 93 | if (shouldSchedule(itemsToAdd)) { 94 | scheduleJavaFx(itemsToAdd); 95 | itemsToAdd.clear(); 96 | } 97 | } 98 | } 99 | 100 | scheduleJavaFx(itemsToAdd); 101 | } finally { 102 | closeStream(dirStream); 103 | } 104 | } 105 | 106 | private boolean shouldSchedule(final List> items) { 107 | return items.size() % 100 == 0; 108 | } 109 | 110 | private void scheduleJavaFx(final List> items) { 111 | final List> temp = new LinkedList<>(); 112 | temp.addAll(items); 113 | 114 | Platform.runLater(() -> itemList.addAll(temp)); 115 | } 116 | 117 | private TreeItem getTreeItem(final File file) { 118 | return new DirectoryTreeItem(file, callback, populateFactory); 119 | } 120 | 121 | private void closeStream(final DirectoryStream directoryStream) { 122 | try { 123 | directoryStream.close(); 124 | } catch (IOException e) { 125 | logger.log(Level.WARNING, "Error closing directory stream", e); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/DirectoryChooserFx.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.beans.property.BooleanProperty; 4 | import javafx.beans.property.ObjectProperty; 5 | import javafx.beans.property.ReadOnlyDoubleProperty; 6 | import javafx.beans.property.StringProperty; 7 | import javafx.collections.ObservableList; 8 | import javafx.scene.Node; 9 | import javafx.stage.Window; 10 | 11 | import java.io.File; 12 | 13 | public interface DirectoryChooserFx { 14 | /** 15 | * Property representing the height of the FileChooser. 16 | */ 17 | ReadOnlyDoubleProperty heightProperty(); 18 | 19 | /** 20 | * Get the height of the FileChooser. 21 | */ 22 | double getHeight(); 23 | 24 | /** 25 | * Set the height of the FileChooser. 26 | */ 27 | void setHeight(double height); 28 | 29 | /** 30 | * Property representing the width of the FileChooser. 31 | */ 32 | ReadOnlyDoubleProperty widthProperty(); 33 | 34 | /** 35 | * Get the width of the FileChooser. 36 | */ 37 | double getWidth(); 38 | 39 | /** 40 | * Set the width of the FileChooser. 41 | */ 42 | void setWidth(double width); 43 | 44 | /** 45 | * Sets the position of the dividers. Should be called before "show" is invoked. 46 | * 47 | * @param placesDivider the position of the divider that separates the places view from 48 | * the directories, between 0.0 and 1.0 (inclusive). 49 | */ 50 | void setDividerPosition(double placesDivider); 51 | 52 | /** 53 | * Returns the position of the divider. 54 | */ 55 | double getDividerPosition(); 56 | 57 | void setInitialDirectory(File value); 58 | 59 | File getInitialDirectory(); 60 | 61 | ObjectProperty initialDirectoryProperty(); 62 | 63 | void setTitle(String value); 64 | 65 | String getTitle(); 66 | 67 | StringProperty titleProperty(); 68 | 69 | void setViewType(ViewType viewType); 70 | 71 | ViewType getViewType(); 72 | 73 | ObjectProperty viewTypeProperty(); 74 | 75 | /** 76 | * Set the sort field. 77 | */ 78 | void setOrderBy(OrderBy orderBy); 79 | 80 | /** 81 | * Retrieve the current sort field. Defaults to {@link OrderBy#Name}. 82 | */ 83 | OrderBy getOrderBy(); 84 | 85 | /** 86 | * Retrieve the current sort field. Defaults to {@link OrderBy#Name}. 87 | */ 88 | ObjectProperty orderByProperty(); 89 | 90 | /** 91 | * Set the sort direction. 92 | */ 93 | void setOrderDirection(OrderDirection orderDirection); 94 | 95 | /** 96 | * Retrieve the sort direction. Defaults to {@link OrderDirection#Ascending}. 97 | */ 98 | OrderDirection getOrderDirection(); 99 | 100 | /** 101 | * Retrieve the sort direction. Defaults to {@link OrderDirection#Ascending}. 102 | */ 103 | ObjectProperty orderDirectionProperty(); 104 | 105 | /** 106 | * Disable/enable the display of mount points on Linux/OSX. 107 | */ 108 | void setShowMountPoints(boolean value); 109 | 110 | /** 111 | * Showing/not showing Linux/OSX mount points. 112 | */ 113 | boolean showMountPoints(); 114 | 115 | /** 116 | * Disable/enable the display of mount points on Linux/OSX. 117 | */ 118 | BooleanProperty showMountPointsProperty(); 119 | 120 | /** 121 | * Overrides the default button order for Open, Cancel, Help Buttons on 122 | * the bottom of dialog. See {@link javafx.scene.control.ButtonBar} button order property. Must 123 | * be called before 'show' function is called. 124 | */ 125 | void setOpenCancelHelpBtnOrder(String buttonOrder); 126 | 127 | /** 128 | * List of directories to show in the Favorites list. As favorites are add and removed 129 | * by the user the list is updated. 130 | */ 131 | ObservableList getFavoriteDirs(); 132 | 133 | /** 134 | * Sets callbacks for when user wants to add and/or remove director favorites. 135 | * This method MUST be called with non-null {@link FavoritesCallback} instances 136 | * for the add/remove favorites buttons to be included in the displayed DirectoryChooserFx 137 | * instance. 138 | */ 139 | void setFavoriteDirsCallbacks(FavoritesCallback addFavorite, FavoritesCallback removeFavorite); 140 | 141 | /** 142 | * Sets the callback for the help button. This method MUST be called with a 143 | * non-null {@link HelpCallback} for the Help button to be included in 144 | * the displayed DirectoryChooserFx instance. 145 | */ 146 | void setHelpCallback(HelpCallback helpCallback); 147 | 148 | /** 149 | * Show the directory chooser dialog. 150 | */ 151 | void showDialog(Window ownerWindow, FileChooserCallback fileChooserCallback); 152 | 153 | /** 154 | * Show the directory chooser dialog with user content {@link Node} placed just above 155 | * the bottom row of dialog buttons. 156 | */ 157 | void showDialog(Window ownerWindow, Node userContent, FileChooserCallback fileChooserCallback); 158 | 159 | } 160 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | FileChooserFx 8 | JavaFx based FileChooser and DirectoryChooser 9 | https://github.com/ricemery/FileChooserFx 10 | 11 | com.chainstaysoftware.controls 12 | filechooserfx 13 | 21.0.1-SNAPSHOT 14 | jar 15 | 16 | 17 | UTF-8 18 | 5.14.0 19 | 20 | 21 | 22 | 23 | Apache License, Version 2.0 24 | http://www.apache.org/licenses/LICENSE-2.0.txt 25 | repo 26 | 27 | 28 | 29 | 30 | 31 | org.openjfx 32 | javafx-controls 33 | 21.0.8 34 | 35 | 36 | commons-io 37 | commons-io 38 | 2.20.0 39 | 40 | 41 | org.apache.commons 42 | commons-lang3 43 | 3.19.0 44 | 45 | 46 | org.controlsfx 47 | controlsfx 48 | 11.2.2 49 | 50 | 51 | 52 | org.junit.jupiter 53 | junit-jupiter-api 54 | ${junit.jupiter.version} 55 | test 56 | 57 | 58 | org.junit.jupiter 59 | junit-jupiter-engine 60 | ${junit.jupiter.version} 61 | test 62 | 63 | 64 | org.assertj 65 | assertj-core 66 | 3.27.6 67 | test 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-compiler-plugin 77 | 3.14.1 78 | 79 | 21 80 | 21 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-enforcer-plugin 86 | 3.5.0 87 | 88 | 89 | enforce-versions 90 | 91 | enforce 92 | 93 | 94 | 95 | 96 | 3.6.3 97 | 98 | 99 | 21 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | org.apache.maven.plugins 109 | maven-surefire-plugin 110 | 3.5.0 111 | 112 | 113 | org.apache.maven.plugins 114 | maven-source-plugin 115 | 3.3.1 116 | 117 | 118 | attach-sources 119 | 120 | jar 121 | 122 | 123 | 124 | 125 | 126 | net.nicoulaj.maven.plugins 127 | checksum-maven-plugin 128 | 1.11 129 | 130 | 131 | 132 | artifacts 133 | 134 | 135 | 136 | 137 | 138 | SHA-256 139 | SHA-512 140 | 141 | true 142 | 143 | 144 | 145 | org.apache.maven.plugins 146 | maven-javadoc-plugin 147 | 3.10.0 148 | 149 | 150 | attach-javadocs 151 | 152 | jar 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/PlacesView.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.icons.Icons; 4 | import com.chainstaysoftware.filechooser.icons.IconsImpl; 5 | import com.chainstaysoftware.filechooser.os.Place; 6 | import com.chainstaysoftware.filechooser.os.PlaceType; 7 | import com.chainstaysoftware.filechooser.os.Places; 8 | import javafx.beans.Observable; 9 | import javafx.scene.control.TitledPane; 10 | import javafx.scene.control.TreeItem; 11 | import javafx.scene.control.TreeView; 12 | import javafx.scene.image.Image; 13 | 14 | import java.io.File; 15 | import java.util.EnumMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Optional; 19 | import java.util.ResourceBundle; 20 | 21 | 22 | /** 23 | * Places - JavaFx node creation and logic. 24 | */ 25 | class PlacesView { 26 | private static final Map placeToIcon = new EnumMap<>(PlaceType.class); 27 | static { 28 | placeToIcon.put(PlaceType.Cd, IconsImpl.CD_64); 29 | placeToIcon.put(PlaceType.Dvd, IconsImpl.DVD_64); 30 | placeToIcon.put(PlaceType.FloppyDisk, IconsImpl.FLOPPY_64); 31 | placeToIcon.put(PlaceType.HardDisk, IconsImpl.HARDDISK_64); 32 | placeToIcon.put(PlaceType.Network, IconsImpl.NETWORK_64); 33 | placeToIcon.put(PlaceType.Usb, IconsImpl.USB_64); 34 | } 35 | 36 | private final ResourceBundle resourceBundle = ResourceBundle.getBundle("filechooser"); 37 | 38 | private final FilesViewCallback callback; 39 | private final Icons icons; 40 | 41 | private final TitledPane placesPane; 42 | private final TreeView placesTreeView; 43 | private final TreeItem defaultPlacesNode; 44 | private final TreeItem favoritesPlacesNode; 45 | 46 | PlacesView(final FilesViewCallback callback, 47 | final Icons icons) { 48 | this.callback = callback; 49 | this.icons = icons; 50 | 51 | defaultPlacesNode = createDefaultPlacesItem(); 52 | favoritesPlacesNode = createFavoritePlacesItem(); 53 | placesTreeView = createPlacesView(); 54 | placesPane = createPlacesPane(placesTreeView); 55 | 56 | this.callback.favoriteDirsProperty().addListener((Observable o) -> updatePlaces()); 57 | } 58 | 59 | TitledPane toPane() { 60 | return placesPane; 61 | } 62 | 63 | /** 64 | * Return the selected directory from the Places View (if any). 65 | */ 66 | Optional getSelectedItem() { 67 | final TreeItem item = placesTreeView.getSelectionModel().getSelectedItem(); 68 | if (item == null || item.getValue() == null || !item.getValue().getFile().isPresent()) { 69 | return Optional.empty(); 70 | } 71 | 72 | return Optional.of(item.getValue().getFile().get()); 73 | } 74 | 75 | /** 76 | * Create the {@link TitledPane} to hold the Places Tree. This pane is used 77 | * to put a title bar on the Places.. 78 | */ 79 | private TitledPane createPlacesPane(final TreeView treeView) { 80 | final TitledPane titledPane = new TitledPane(resourceBundle.getString("placeslist.text"), treeView); 81 | titledPane.setId("placesPane"); 82 | titledPane.setCollapsible(false); 83 | return titledPane; 84 | } 85 | 86 | private TreeItem createDefaultPlacesItem() { 87 | final TreeItem item = new TreeItem<>(); 88 | item.setValue(new PlacesTreeItem(Optional.of(resourceBundle.getString("computer.text")), 89 | Optional.empty(), icons.getIcon(IconsImpl.COMPUTER_64), false)); 90 | item.setExpanded(true); 91 | 92 | return item; 93 | } 94 | 95 | private TreeItem createFavoritePlacesItem() { 96 | final TreeItem item = new TreeItem<>(); 97 | item.setValue(new PlacesTreeItem(Optional.of(resourceBundle.getString("favorites.text")), 98 | Optional.empty(), icons.getIcon(IconsImpl.STAR_64), true)); 99 | item.setExpanded(true); 100 | return item; 101 | } 102 | 103 | /** 104 | * Create the Places {@link TreeView}. 105 | */ 106 | private TreeView createPlacesView() { 107 | final TreeView view = new TreeView<>(); 108 | view.setId("placesView"); 109 | view.setShowRoot(false); 110 | view.setCellFactory(new PlacesTreeItemCellFactory(callback.favoriteDirsProperty())); 111 | 112 | final TreeItem root = new TreeItem<>(); 113 | view.setRoot(root); 114 | 115 | view.setOnMouseClicked(event -> { 116 | final TreeItem selectedItem = view.getSelectionModel().getSelectedItem(); 117 | if (selectedItem == null || !selectedItem.getValue().getFile().isPresent()) { 118 | callback.disableRemoveFavoritieButton(true); 119 | return; 120 | } 121 | 122 | final File currentDir = selectedItem.getValue().getFile().get(); 123 | callback.requestChangeDirectory(currentDir); 124 | 125 | callback.disableRemoveFavoritieButton(!callback.favoriteDirsProperty().contains(currentDir)); 126 | }); 127 | 128 | return view; 129 | } 130 | 131 | /** 132 | * Update the Places View with drives, home dir and favorites. 133 | */ 134 | void updatePlaces() { 135 | final TreeItem rootNode = placesTreeView.getRoot(); 136 | rootNode.getChildren().clear(); 137 | rootNode.getChildren().add(defaultPlacesNode); 138 | 139 | defaultPlacesNode.getChildren().clear(); 140 | favoritesPlacesNode.getChildren().clear(); 141 | 142 | final List defaultPlaces = new Places().getDefaultPlaces(callback.showMountPointsProperty().get()); 143 | defaultPlaces.forEach(place -> 144 | defaultPlacesNode.getChildren().add(new TreeItem<>(new PlacesTreeItem(Optional.empty(), 145 | Optional.of(place.getPath()), toIcon(place), false), null))); 146 | 147 | final String homeDirStr = System.getProperty("user.home"); 148 | if (homeDirStr != null) { 149 | defaultPlacesNode.getChildren().add(new TreeItem<>(new PlacesTreeItem(Optional.empty(), 150 | Optional.of(new File(homeDirStr)), icons.getIcon(IconsImpl.USER_HOME_64), false), null)); 151 | } 152 | 153 | if (!callback.favoriteDirsProperty().isEmpty()) { 154 | rootNode.getChildren().add(favoritesPlacesNode); 155 | 156 | callback.favoriteDirsProperty().forEach(file -> 157 | favoritesPlacesNode.getChildren().add(new TreeItem<>(new PlacesTreeItem(Optional.empty(), 158 | Optional.of(file), icons.getIcon(IconsImpl.FOLDER_64), true), null))); 159 | } 160 | 161 | callback.disableAddFavoriteButton(true); 162 | callback.disableRemoveFavoritieButton(true); 163 | } 164 | 165 | /** 166 | * Get the icon for the passed in place. 167 | */ 168 | private Image toIcon(final Place place) { 169 | String filename = placeToIcon.get(place.getType()); 170 | if (filename == null) { 171 | filename = IconsImpl.HARDDISK_64; 172 | } 173 | 174 | return icons.getIcon(filename); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/FileChooserFx.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.icons.Icons; 4 | import com.chainstaysoftware.filechooser.preview.PreviewPane; 5 | import javafx.beans.property.BooleanProperty; 6 | import javafx.beans.property.ObjectProperty; 7 | import javafx.beans.property.ReadOnlyDoubleProperty; 8 | import javafx.beans.property.StringProperty; 9 | import javafx.collections.ObservableList; 10 | import javafx.collections.ObservableMap; 11 | import javafx.scene.Node; 12 | import javafx.stage.FileChooser; 13 | import javafx.stage.Window; 14 | 15 | import java.io.File; 16 | 17 | public interface FileChooserFx { 18 | /** 19 | * Property representing the height of the FileChooser. 20 | */ 21 | ReadOnlyDoubleProperty heightProperty(); 22 | 23 | /** 24 | * Get the height of the FileChooser. 25 | */ 26 | double getHeight(); 27 | 28 | /** 29 | * Set the height of the FileChooser. 30 | */ 31 | void setHeight(double height); 32 | 33 | /** 34 | * Property representing the width of the FileChooser. 35 | */ 36 | ReadOnlyDoubleProperty widthProperty(); 37 | 38 | /** 39 | * Get the width of the FileChooser. 40 | */ 41 | double getWidth(); 42 | 43 | /** 44 | * Set the width of the FileChooser. 45 | */ 46 | void setWidth(double width); 47 | 48 | /** 49 | * Sets the position of the dividers. Should be called before "show" is invoked. 50 | * 51 | * @param placesDivider the position of the divider that separates the places view from 52 | * the file/directories, between 0.0 and 1.0 (inclusive). 53 | * @param previewDivider the position of the divider that separates the preview pane from 54 | * the list of files/directories, between 0.0 and 1.0 (inclusive). 55 | * The value is relative to the placesDivider. 0.0 indicates right ontop 56 | * of the places divider. And, 1.0 indicates thr far right of the window. 57 | */ 58 | void setDividerPositions(double placesDivider, double previewDivider); 59 | 60 | 61 | /** 62 | * Returns the position of the dividers. The returned array contain 2 elements. 63 | * The first element is the position of the places divider. The second element 64 | * is the position of the divider between the file list and the preview pane. 65 | */ 66 | double[] getDividerPositions(); 67 | 68 | ObservableList getExtensionFilters(); 69 | 70 | ObjectProperty selectedExtensionFilterProperty(); 71 | 72 | void setSelectedExtensionFilter(FileChooser.ExtensionFilter filter); 73 | 74 | FileChooser.ExtensionFilter getSelectedExtensionFilter(); 75 | 76 | ObservableMap> getPreviewHandlers(); 77 | 78 | void setInitialDirectory(File value); 79 | 80 | File getInitialDirectory(); 81 | 82 | ObjectProperty initialDirectoryProperty(); 83 | 84 | void setFileName(String value); 85 | 86 | String getFileName(); 87 | 88 | ObjectProperty fileNameProperty(); 89 | 90 | void setTitle(String value); 91 | 92 | String getTitle(); 93 | 94 | StringProperty titleProperty(); 95 | 96 | void setViewType(ViewType viewType); 97 | 98 | ViewType getViewType(); 99 | 100 | ObjectProperty viewTypeProperty(); 101 | 102 | void setShowHiddenFiles(boolean value); 103 | 104 | boolean showHiddenFiles(); 105 | 106 | BooleanProperty showHiddenFilesProperty(); 107 | 108 | void setShouldHideFiles(boolean value); 109 | 110 | boolean shouldHideFiles(); 111 | 112 | BooleanProperty shouldHideFilesProperty(); 113 | 114 | /** 115 | * Set the sort field. 116 | */ 117 | void setOrderBy(OrderBy orderBy); 118 | 119 | /** 120 | * Retrieve the current sort field. Defaults to {@link OrderBy#Name}. 121 | */ 122 | OrderBy getOrderBy(); 123 | 124 | /** 125 | * Retrieve the current sort field. Defaults to {@link OrderBy#Name}. 126 | */ 127 | ObjectProperty orderByProperty(); 128 | 129 | /** 130 | * Set the sort direction. 131 | */ 132 | void setOrderDirection(OrderDirection orderDirection); 133 | 134 | /** 135 | * Retrieve the sort direction. Defaults to {@link OrderDirection#Ascending}. 136 | */ 137 | OrderDirection getOrderDirection(); 138 | 139 | /** 140 | * Retrieve the sort direction. Defaults to {@link OrderDirection#Ascending}. 141 | */ 142 | ObjectProperty orderDirectionProperty(); 143 | 144 | /** 145 | * Disable/enable the display of mount points on Linux/OSX. 146 | */ 147 | void setShowMountPoints(boolean value); 148 | 149 | /** 150 | * Showing/not showing Linux/OSX mount points. 151 | */ 152 | boolean showMountPoints(); 153 | 154 | /** 155 | * Disable/enable the display of mount points on Linux/OSX. 156 | */ 157 | BooleanProperty showMountPointsProperty(); 158 | 159 | /** 160 | * List of directories to show in the Favorites list. As favorites are add and removed 161 | * by the user, the list is updated. 162 | */ 163 | ObservableList favoriteDirsProperty(); 164 | 165 | /** 166 | * Overrides the default button order for Open, Cancel, Help Buttons on 167 | * the bottom of dialog. See {@link javafx.scene.control.ButtonBar} button order property. Must 168 | * be called before 'show' function is called. 169 | */ 170 | void setOpenCancelHelpBtnOrder(String buttonOrder); 171 | 172 | /** 173 | * Sets callbacks for when user wants to add and/or remove director favorites. 174 | * This method MUST be called with non-null {@link FavoritesCallback} instances 175 | * for the add/remove favorites buttons to be included in the displayed FileChooserFx 176 | * instance. 177 | */ 178 | void setFavoriteDirsCallbacks(FavoritesCallback addFavorite, FavoritesCallback removeFavorite); 179 | 180 | /** 181 | * Sets the callback for the help button. This method MUST be called with a 182 | * non-null {@link HelpCallback} for the Help button to be included in 183 | * the displayed FileChooserFx instance. 184 | */ 185 | void setHelpCallback(HelpCallback helpCallback); 186 | 187 | /** 188 | * Set the implementation to use for Icon handling. This does not need 189 | * to be called unless there is a desire to override the default Icon set. 190 | */ 191 | void setIcons(Icons icons); 192 | 193 | /** 194 | * Show the file open dialog. 195 | */ 196 | void showOpenDialog(Window ownerWindow, FileChooserCallback fileChooserCallback); 197 | 198 | /** 199 | * Show the file open dialog with user content {@link Node} placed just above 200 | * the bottom row of dialog buttons. 201 | */ 202 | void showOpenDialog(Window ownerWindow, Node userContent, 203 | FileChooserCallback fileChooserCallback); 204 | 205 | /** 206 | * Show the file save dialog. 207 | */ 208 | void showSaveDialog(Window ownerWindow, FileChooserCallback fileChooserCallback); 209 | 210 | /** 211 | * Show the file save dialog with user content {@link Node} placed just above 212 | * the bottom row of dialog buttons. 213 | */ 214 | void showSaveDialog(Window ownerWindow, Node userContent, 215 | FileChooserCallback fileChooserCallback); 216 | } 217 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/icons/IconsImpl.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser.icons; 2 | 3 | import javafx.beans.property.MapProperty; 4 | import javafx.beans.property.SimpleMapProperty; 5 | import javafx.collections.FXCollections; 6 | import javafx.scene.image.Image; 7 | import org.apache.commons.io.FilenameUtils; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.io.InputStream; 12 | import java.util.HashMap; 13 | import java.util.Locale; 14 | import java.util.Map; 15 | 16 | /** 17 | * Icon loading. 18 | */ 19 | public class IconsImpl implements Icons { 20 | public static final int SMALL_ICON_WIDTH = 25; 21 | public static final int SMALL_ICON_HEIGHT = 25; 22 | 23 | public static final String BACK_ARROW_24 = "/icons8/24x24/Back-24.png"; 24 | public static final String ICON_VIEW_24 = "/icons8/24x24/Small Icons-24.png"; 25 | public static final String LIST_VIEW_24 = "/icons8/24x24/List-24.png"; 26 | public static final String LIST_WITH_PREVIEW_VIEW_24 = "/icons8/24x24/Columns-24.png"; 27 | 28 | public static final String COMPUTER_64 = "/icons8/64x64/Computer-64.png"; 29 | public static final String STAR_64 = "/icons8/64x64/Star Filled-64.png"; 30 | public static final String FOLDER_64 = "/icons8/64x64/Folder-64.png"; 31 | public static final String HARDDISK_64 = "/icons8/64x64/HDD-64.png"; 32 | public static final String CD_64 = "/icons8/64x64/CD-64.png"; 33 | public static final String DVD_64 = CD_64; 34 | public static final String FLOPPY_64 = "/icons8/64x64/Save-64.png"; 35 | public static final String NETWORK_64 = "/icons8/64x64/Network-64.png"; 36 | public static final String USB_64 = "/icons8/64x64/USB 2-64.png"; 37 | public static final String OPEN_FOLDER_64 = "/icons8/64x64/Open Folder-64.png"; 38 | public static final String USER_HOME_64 = "/icons8/64x64/User Folder-64.png"; 39 | 40 | public static final String GENERIC_FILE_64 = "/icons8/64x64/File-64.png"; 41 | public static final String AAC_64 = "/icons8/64x64/AAC-64.png"; 42 | public static final String AVI_64 = "/icons8/64x64/AVI-64.png"; 43 | public static final String CONSOLE_64 = "/icons8/64x64/Console-64.png"; 44 | public static final String CSS_64 = "/icons8/64x64/CSS-64.png"; 45 | public static final String EXE_64 = "/icons8/64x64/EXE-64.png"; 46 | public static final String GIF_64 = "/icons8/64x64/GIF-64.png"; 47 | public static final String HTML_64 = "/icons8/64x64/HTML-64.png"; 48 | public static final String JPG_64 = "/icons8/64x64/JPG-64.png"; 49 | public static final String MOV_64 = "/icons8/64x64/MOV-64.png"; 50 | public static final String MP3_64 = "/icons8/64x64/MP3-64.png"; 51 | public static final String PDF_64 = "/icons8/64x64/PDF-64.png"; 52 | public static final String PNG_64 = "/icons8/64x64/PNG-64.png"; 53 | public static final String POWERPOINT_64 = "/icons8/64x64/PowerPoint-64.png"; 54 | public static final String PS_64 = "/icons8/64x64/PS-64.png"; 55 | public static final String TXT_64 = "/icons8/64x64/TXT-64.png"; 56 | public static final String WMA_64 = "/icons8/64x64/WMA-64.png"; 57 | public static final String WORD_64 = "/icons8/64x64/Word-64.png"; 58 | public static final String XLS_64 = "/icons8/64x64/XLS-64.png"; 59 | public static final String XML_64 = "/icons8/64x64/XML-64.png"; 60 | public static final String ZIP_64 = "/icons8/64x64/ZIP-64.png"; 61 | 62 | private final MapProperty fileTypeIconsProperty = new SimpleMapProperty<>(); 63 | 64 | public IconsImpl() { 65 | final Map fileTypeIcons = new HashMap<>(); 66 | fileTypeIcons.put("aac", getIcon(AAC_64)); 67 | fileTypeIcons.put("avi", getIcon(AVI_64)); 68 | fileTypeIcons.put("cmd", getIcon(CONSOLE_64)); 69 | fileTypeIcons.put("css", getIcon(CSS_64)); 70 | fileTypeIcons.put("doc", getIcon(WORD_64)); 71 | fileTypeIcons.put("docx", getIcon(WORD_64)); 72 | fileTypeIcons.put("exe", getIcon(EXE_64)); 73 | fileTypeIcons.put("gif", getIcon(GIF_64)); 74 | fileTypeIcons.put("html", getIcon(HTML_64)); 75 | fileTypeIcons.put("jpg", getIcon(JPG_64)); 76 | fileTypeIcons.put("mov", getIcon(MOV_64)); 77 | fileTypeIcons.put("mp3", getIcon(MP3_64)); 78 | fileTypeIcons.put("pdf", getIcon(PDF_64)); 79 | fileTypeIcons.put("png", getIcon(PNG_64)); 80 | fileTypeIcons.put("ppt", getIcon(POWERPOINT_64)); 81 | fileTypeIcons.put("pptm", getIcon(POWERPOINT_64)); 82 | fileTypeIcons.put("pptx", getIcon(POWERPOINT_64)); 83 | fileTypeIcons.put("ps", getIcon(PS_64)); 84 | fileTypeIcons.put("sh", getIcon(CONSOLE_64)); 85 | fileTypeIcons.put("txt", getIcon(TXT_64)); 86 | fileTypeIcons.put("wma", getIcon(WMA_64)); 87 | fileTypeIcons.put("xls", getIcon(XLS_64)); 88 | fileTypeIcons.put("xlsx", getIcon(XLS_64)); 89 | fileTypeIcons.put("xlt", getIcon(XLS_64)); 90 | fileTypeIcons.put("xml", getIcon(XML_64)); 91 | fileTypeIcons.put("zip", getIcon(ZIP_64)); 92 | 93 | fileTypeIconsProperty.setValue(FXCollections.observableMap(fileTypeIcons)); 94 | } 95 | 96 | /** 97 | * Load an icon from the CURSOR_PATH location in the class path. 98 | * @param resourceName Must be a filename of an icon located in the 99 | * CURSOR_PATH location of the classpath. 100 | * @return {@link Image} 101 | * @throws IllegalArgumentException If the resource is not found. 102 | */ 103 | @Override 104 | public Image getIcon(final String resourceName) { 105 | if (resourceName == null) { 106 | throw new IllegalArgumentException("resourceName must not be null"); 107 | } 108 | 109 | try (final InputStream inputStream = IconsImpl.class.getResourceAsStream(resourceName)) { 110 | if (inputStream == null) { 111 | throw new IllegalArgumentException(resourceName + " is not a valid resource"); 112 | } 113 | 114 | return new Image(inputStream); 115 | } catch (IOException e) { 116 | throw new IllegalArgumentException(resourceName + " is not a valid resource", e); 117 | } 118 | } 119 | 120 | /** 121 | * Load an icon for the passed in file. 122 | * @param file to load an icon for. 123 | * @return {@link Image} 124 | * @throws IllegalArgumentException If the resource is not found. 125 | */ 126 | @Override 127 | public Image getIconForFile(final File file) { 128 | final String extension = FilenameUtils.getExtension(file.getName()).toLowerCase(Locale.ENGLISH); 129 | final Image image = fileTypeIconsProperty.getValue().get(extension); 130 | return image == null 131 | ? getIcon(IconsImpl.GENERIC_FILE_64) 132 | : image; 133 | } 134 | 135 | /** 136 | * Mapping of file extensions to icon images. 137 | */ 138 | @Override 139 | public MapProperty fileTypeIconsProperty() { 140 | return fileTypeIconsProperty; 141 | } 142 | 143 | /** 144 | * Sets the mapping of file extensions to icon images. 145 | */ 146 | @Override 147 | public void setFileTypeIcons(final Map map) { 148 | fileTypeIconsProperty().setValue(FXCollections.observableMap(map)); 149 | } 150 | 151 | /** 152 | * Mapping of file extensions to icon images. 153 | */ 154 | @Override 155 | public final Map getFileTypeIcons() { 156 | return fileTypeIconsProperty().get(); 157 | } 158 | } 159 | 160 | -------------------------------------------------------------------------------- /src/test/java/com/chainstaysoftware/filechooser/FileChooserDemo.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.preview.HeadPreviewPane; 4 | import com.chainstaysoftware.filechooser.preview.ImagePreviewPane; 5 | import com.chainstaysoftware.filechooser.preview.PreviewPane; 6 | import javafx.application.Application; 7 | import javafx.scene.Node; 8 | import javafx.scene.Scene; 9 | import javafx.scene.control.Button; 10 | import javafx.scene.control.ToolBar; 11 | import javafx.scene.layout.BorderPane; 12 | import javafx.scene.layout.HBox; 13 | import javafx.scene.text.Text; 14 | import javafx.scene.text.TextFlow; 15 | import javafx.stage.FileChooser; 16 | import javafx.stage.Stage; 17 | 18 | import java.util.Arrays; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | public class FileChooserDemo extends Application { 23 | @Override 24 | public void start(Stage primaryStage) { 25 | final HashMap> previewHandlers = new HashMap<>(); 26 | previewHandlers.put("image/png", ImagePreviewPane.class); 27 | previewHandlers.put("image/jpg", ImagePreviewPane.class); 28 | previewHandlers.put("text/plain", HeadPreviewPane.class); 29 | 30 | final TextFlow textFlow = new TextFlow(); 31 | 32 | final Button fileChooserButton = createFileChooserButton(primaryStage, previewHandlers, textFlow); 33 | final Button fileSaveButton = createFileSaveButton(primaryStage, previewHandlers, textFlow, null); 34 | 35 | final Button userButton = new Button("User action"); 36 | final HBox userHbox = new HBox(userButton); 37 | final Button fileSaveWithUserButton 38 | = createFileSaveButton(primaryStage, previewHandlers, textFlow, userHbox); 39 | 40 | final Button dirChooserButton = createDirChooserButton(primaryStage, textFlow); 41 | 42 | final ToolBar toolBar = new ToolBar(); 43 | toolBar.getItems().addAll(fileChooserButton, fileSaveButton, 44 | fileSaveWithUserButton, dirChooserButton); 45 | 46 | final BorderPane borderPane = new BorderPane(); 47 | borderPane.setTop(toolBar); 48 | borderPane.setCenter(textFlow); 49 | 50 | primaryStage.setScene(new Scene(borderPane, 800, 600)); 51 | primaryStage.show(); 52 | } 53 | 54 | private Button createFileChooserButton(final Stage primaryStage, 55 | final Map> previewHandlers, 56 | final TextFlow textFlow) { 57 | final Button fileChooserButton = new Button("Choose File"); 58 | fileChooserButton.setOnAction(event -> { 59 | final FileChooserFx fileChooser = new FileChooserFxImpl(); 60 | fileChooser.setTitle("File Chooser"); 61 | fileChooser.setShowHiddenFiles(false); 62 | fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("Text files (txt)", "*.txt"), 63 | new FileChooser.ExtensionFilter("XML files", "*.xml"), 64 | new FileChooser.ExtensionFilter("All files", "*.*")); 65 | fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("XML files (xml)", "*.xml")); 66 | fileChooser.getPreviewHandlers().putAll(previewHandlers); 67 | fileChooser.setHelpCallback(() -> System.out.println("Help invoked")); 68 | fileChooser.setShowMountPoints(true); 69 | fileChooser.setFavoriteDirsCallbacks(directory -> System.out.println("Add favorite - " + directory), 70 | directory -> System.out.println("Remove favorite - " + directory)); 71 | fileChooser.setViewType(ViewType.Icon); 72 | fileChooser.setWidth(1024); 73 | fileChooser.setHeight(768); 74 | fileChooser.setDividerPositions(.15, .30); 75 | 76 | fileChooser.showOpenDialog(primaryStage, 77 | fileOptional -> textFlow.getChildren() 78 | .add(new Text("File to open - " + fileOptional.toString() + "\r\n" 79 | + "Selected Extension Filter - " + fileChooser.getSelectedExtensionFilter().getDescription() + "\r\n" 80 | + "View Type - " + fileChooser.getViewType() + "\r\n" 81 | + "Sort - " + fileChooser.getOrderBy() + " " + fileChooser.getOrderDirection() + "\r\n" 82 | + "Favorites - " + fileChooser.favoriteDirsProperty() + "\r\n" 83 | + "Window size - " + fileChooser.getWidth() + " " + fileChooser.getHeight() + "\r\n" 84 | + "Divider positions - " + Arrays.toString(fileChooser.getDividerPositions()) + "\r\n"))); 85 | }); 86 | return fileChooserButton; 87 | } 88 | 89 | private Button createFileSaveButton(final Stage primaryStage, 90 | final Map> previewHandlers, 91 | final TextFlow textFlow, 92 | final Node userContent) { 93 | final Button fileSaveButton = new Button(); 94 | if (userContent == null) { 95 | fileSaveButton.setText("Save File"); 96 | } else { 97 | fileSaveButton.setText("Save File with user content"); 98 | } 99 | 100 | fileSaveButton.setOnAction(event -> { 101 | final FileChooserFx fileChooser = new FileChooserFxImpl(); 102 | fileChooser.setShowHiddenFiles(false); 103 | fileChooser.getPreviewHandlers().putAll(previewHandlers); 104 | fileChooser.setHelpCallback(() -> System.out.println("Help invoked")); 105 | //fileChooser.setInitialFileName("foo.txt"); 106 | 107 | if (userContent == null) { 108 | fileChooser.setTitle("File Save"); 109 | fileChooser.showSaveDialog(primaryStage, 110 | fileOptional -> textFlow.getChildren() 111 | .add(new Text("File to save - " + fileOptional.toString() + "\r\n"))); 112 | } else { 113 | fileChooser.setTitle("File Save with user content"); 114 | fileChooser.showSaveDialog(primaryStage, 115 | userContent, 116 | fileOptional -> textFlow.getChildren() 117 | .add(new Text("File to save - " + fileOptional.toString() + "\r\n"))); 118 | } 119 | }); 120 | return fileSaveButton; 121 | } 122 | 123 | private Button createDirChooserButton(final Stage primaryStage, 124 | final TextFlow textFlow) { 125 | final Button dirChooserButton = new Button("Choose Directory"); 126 | dirChooserButton.setOnAction(event -> { 127 | final DirectoryChooserFx dirChooser = new DirectoryChooserFxImpl(); 128 | dirChooser.setTitle("Directory Chooser"); 129 | dirChooser.setHelpCallback(() -> System.out.println("Help invoked")); 130 | dirChooser.setViewType(ViewType.ListWithPreview); 131 | dirChooser.setShowMountPoints(true); 132 | dirChooser.setDividerPosition(.15); 133 | 134 | dirChooser.showDialog(primaryStage, 135 | fileOptional -> textFlow.getChildren() 136 | .add(new Text("Directory to open - " + fileOptional.toString() + "\r\n" 137 | + "View Type - " + dirChooser.getViewType() + "\r\n" 138 | + "Window size - " + dirChooser.getWidth() + " " + dirChooser.getHeight() + "\r\n" 139 | + "Divider position - " + dirChooser.getDividerPosition() + "\r\n"))); 140 | }); 141 | return dirChooserButton; 142 | } 143 | 144 | public static void main(String[] args) { 145 | Application.launch(args); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/DirectoryChooserFxImpl.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import javafx.beans.property.BooleanProperty; 4 | import javafx.beans.property.ObjectProperty; 5 | import javafx.beans.property.ReadOnlyDoubleProperty; 6 | import javafx.beans.property.StringProperty; 7 | import javafx.collections.ObservableList; 8 | import javafx.scene.Node; 9 | import javafx.stage.Window; 10 | 11 | import java.io.File; 12 | 13 | public class DirectoryChooserFxImpl implements DirectoryChooserFx { 14 | private final FileChooserFxImpl fileChooser = new FileChooserFxImpl(); 15 | 16 | /** 17 | * Property representing the height of the FileChooser. 18 | */ 19 | @Override 20 | public ReadOnlyDoubleProperty heightProperty() { 21 | return fileChooser.heightProperty(); 22 | } 23 | 24 | /** 25 | * Get the height of the FileChooser. 26 | */ 27 | @Override 28 | public double getHeight() { 29 | return fileChooser.getHeight(); 30 | } 31 | 32 | /** 33 | * Set the height of the FileChooser. 34 | * 35 | * @param height 36 | */ 37 | @Override 38 | public void setHeight(final double height) { 39 | fileChooser.setHeight(height); 40 | } 41 | 42 | /** 43 | * Property representing the width of the FileChooser. 44 | */ 45 | @Override 46 | public ReadOnlyDoubleProperty widthProperty() { 47 | return fileChooser.widthProperty(); 48 | } 49 | 50 | /** 51 | * Get the width of the FileChooser. 52 | */ 53 | @Override 54 | public double getWidth() { 55 | return fileChooser.getWidth(); 56 | } 57 | 58 | /** 59 | * Set the width of the FileChooser. 60 | */ 61 | @Override 62 | public void setWidth(final double width) { 63 | fileChooser.setWidth(width); 64 | } 65 | 66 | /** 67 | * Sets the position of the dividers. Should be called before "show" is invoked. 68 | * 69 | * @param placesDivider the position of the divider that separates the places view from 70 | * the directories, between 0.0 and 1.0 (inclusive). 71 | */ 72 | @Override 73 | public void setDividerPosition(final double placesDivider) { 74 | fileChooser.setDividerPositions(placesDivider, 0); 75 | } 76 | 77 | /** 78 | * Returns the position of the divider. 79 | */ 80 | @Override 81 | public double getDividerPosition() { 82 | return fileChooser.getDividerPositions()[0]; 83 | } 84 | 85 | @Override 86 | public void setInitialDirectory(final File value) { 87 | fileChooser.setInitialDirectory(value); 88 | } 89 | 90 | @Override 91 | public File getInitialDirectory() { 92 | return fileChooser.getInitialDirectory(); 93 | } 94 | 95 | @Override 96 | public ObjectProperty initialDirectoryProperty() { 97 | return fileChooser.initialDirectoryProperty(); 98 | } 99 | 100 | @Override 101 | public void setTitle(final String value) { 102 | fileChooser.setTitle(value); 103 | } 104 | 105 | @Override 106 | public String getTitle() { 107 | return fileChooser.getTitle(); 108 | } 109 | 110 | @Override 111 | public StringProperty titleProperty() { 112 | return fileChooser.titleProperty(); 113 | } 114 | 115 | /** 116 | * List of directories to show in the Favorites list. As favorites are add and removed 117 | * by the user, the list is updated. 118 | */ 119 | @Override 120 | public ObservableList getFavoriteDirs() { 121 | return fileChooser.favoriteDirsProperty(); 122 | } 123 | 124 | /** 125 | * Sets callbacks for when user wants to add and/or remove director favorites. 126 | * This method MUST be called with non-null {@link FavoritesCallback} instances 127 | * for the add/remove favorites buttons to be included in the displayed DirectoryChooserFx 128 | * instance. 129 | */ 130 | @Override 131 | public void setFavoriteDirsCallbacks(final FavoritesCallback addFavorite, 132 | final FavoritesCallback removeFavorite) { 133 | fileChooser.setFavoriteDirsCallbacks(addFavorite, removeFavorite); 134 | } 135 | 136 | /** 137 | * Sets the callback for the help button. This method MUST be called with a 138 | * non-null {@link HelpCallback} for the Help button to be included in 139 | * the displayed DirectoryChooserFx instance. 140 | */ 141 | @Override 142 | public void setViewType(final ViewType viewType) { 143 | fileChooser.setViewType(viewType); 144 | } 145 | 146 | @Override 147 | public ViewType getViewType() { 148 | return fileChooser.getViewType(); 149 | } 150 | 151 | @Override 152 | public ObjectProperty viewTypeProperty() { 153 | return fileChooser.viewTypeProperty(); 154 | } 155 | 156 | /** 157 | * Set the sort field. 158 | */ 159 | @Override 160 | public void setOrderBy(final OrderBy orderBy) { 161 | fileChooser.setOrderBy(orderBy); 162 | } 163 | 164 | /** 165 | * Retrieve the current sort field. Defaults to {@link OrderBy#Name}. 166 | */ 167 | @Override 168 | public OrderBy getOrderBy() { 169 | return fileChooser.getOrderBy(); 170 | } 171 | 172 | /** 173 | * Retrieve the current sort field. Defaults to {@link OrderBy#Name}. 174 | */ 175 | @Override 176 | public ObjectProperty orderByProperty() { 177 | return fileChooser.orderByProperty(); 178 | } 179 | 180 | /** 181 | * Set the sort direction. 182 | */ 183 | @Override 184 | public void setOrderDirection(final OrderDirection orderDirection) { 185 | fileChooser.setOrderDirection(orderDirection); 186 | } 187 | 188 | /** 189 | * Retrieve the sort direction. Defaults to {@link OrderDirection#Ascending}. 190 | */ 191 | @Override 192 | public OrderDirection getOrderDirection() { 193 | return fileChooser.getOrderDirection(); 194 | } 195 | 196 | /** 197 | * Retrieve the sort direction. Defaults to {@link OrderDirection#Ascending}. 198 | */ 199 | @Override 200 | public ObjectProperty orderDirectionProperty() { 201 | return fileChooser.orderDirectionProperty(); 202 | } 203 | 204 | /** 205 | * Disable/enable the display of mount points on Linux/OSX. 206 | */ 207 | @Override 208 | public void setShowMountPoints(boolean value) { 209 | fileChooser.setShowMountPoints(value); 210 | } 211 | 212 | /** 213 | * Showing/not showing Linux/OSX mount points. 214 | */ 215 | @Override 216 | public boolean showMountPoints() { 217 | return fileChooser.showMountPoints(); 218 | } 219 | 220 | /** 221 | * Disable/enable the display of mount points on Linux/OSX. 222 | */ 223 | @Override 224 | public BooleanProperty showMountPointsProperty() { 225 | return fileChooser.showMountPointsProperty(); 226 | } 227 | 228 | /** 229 | * Overrides the default button order for Open, Cancel, Help Buttons on 230 | * the bottom of dialog. See {@link javafx.scene.control.ButtonBar} button order property. Must 231 | * be called before 'show' function is called. 232 | */ 233 | @Override 234 | public void setOpenCancelHelpBtnOrder(final String buttonOrder) { 235 | fileChooser.setOpenCancelHelpBtnOrder(buttonOrder); 236 | } 237 | 238 | @Override 239 | public void setHelpCallback(final HelpCallback helpCallback) { 240 | fileChooser.setHelpCallback(helpCallback); 241 | } 242 | 243 | @Override 244 | public void showDialog(final Window ownerWindow, 245 | final FileChooserCallback fileChooserCallback) { 246 | showDialog(ownerWindow, null, fileChooserCallback); 247 | } 248 | 249 | @Override 250 | public void showDialog(final Window ownerWindow, 251 | final Node userContent, 252 | final FileChooserCallback fileChooserCallback) { 253 | fileChooser.showOpenDialog(ownerWindow, userContent, fileChooserCallback, true); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /src/test/java/com/chainstaysoftware/filechooser/FileMetaDataComparatorTest.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import org.assertj.core.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.File; 7 | 8 | class FileMetaDataComparatorTest { 9 | private final File testDataDir = new File("./src/test/resources/com/chainstaysoftware/filechooser"); 10 | private final File aaa = new File(testDataDir, "aaa"); 11 | private final File bbb = new File(testDataDir, "bbb"); 12 | private final File emptyTxt = new File(testDataDir, "empty.txt"); 13 | private final File emptyXml = new File(testDataDir, "empty.xml"); 14 | private final File dir1 = new File(testDataDir, "dir1"); 15 | private final File dir2 = new File(testDataDir, "dir2"); 16 | 17 | @Test 18 | void testOrderByModificationDate() { 19 | final FileMetaDataComparator comparator = new FileMetaDataComparator(OrderBy.ModificationDate, OrderDirection.Ascending); 20 | 21 | bbb.setLastModified(System.currentTimeMillis()); 22 | 23 | Assertions.assertThat(comparator.compare(aaa, aaa)) 24 | .describedAs("Same file mod date should equal") 25 | .isEqualTo(0); 26 | Assertions.assertThat(comparator.compare(aaa, bbb)) 27 | .describedAs("Less than mod date should be less than") 28 | .isLessThan(0); 29 | Assertions.assertThat(comparator.compare(bbb, aaa)) 30 | .describedAs("Greater than mod date should be greater than") 31 | .isGreaterThan(0); 32 | } 33 | 34 | @Test 35 | void testOrderByModificationDate_Descending() { 36 | final FileMetaDataComparator comparator = new FileMetaDataComparator(OrderBy.ModificationDate, OrderDirection.Descending); 37 | 38 | bbb.setLastModified(System.currentTimeMillis()); 39 | 40 | Assertions.assertThat(comparator.compare(aaa, aaa)) 41 | .describedAs("Same file mod date should equal") 42 | .isEqualTo(0); 43 | Assertions.assertThat(comparator.compare(aaa, bbb)) 44 | .describedAs("Less than mod date should be greater than") 45 | .isGreaterThan(0); 46 | Assertions.assertThat(comparator.compare(bbb, aaa)) 47 | .describedAs("Greater than mod date should be less than") 48 | .isLessThan(0); 49 | } 50 | 51 | @Test 52 | void testOrderBySize() { 53 | final FileMetaDataComparator comparator = new FileMetaDataComparator(OrderBy.Size, OrderDirection.Ascending); 54 | 55 | Assertions.assertThat(comparator.compare(aaa, aaa)) 56 | .describedAs("Same file size should equal") 57 | .isEqualTo(0); 58 | Assertions.assertThat(comparator.compare(aaa, bbb)) 59 | .describedAs("Less than size should be less than") 60 | .isLessThan(0); 61 | Assertions.assertThat(comparator.compare(bbb, aaa)) 62 | .describedAs("Greater than size should be greater than") 63 | .isGreaterThan(0); 64 | 65 | Assertions.assertThat(comparator.compare(testDataDir, aaa)) 66 | .describedAs("Directory should be lessthan file") 67 | .isLessThan(0); 68 | Assertions.assertThat(comparator.compare(aaa, testDataDir)) 69 | .describedAs("File should be greatethan dir") 70 | .isGreaterThan(0); 71 | Assertions.assertThat(comparator.compare(new File(testDataDir, "dir1"), new File(testDataDir, "dir2"))) 72 | .describedAs("Directories should fall back to name compare") 73 | .isLessThan(0); 74 | Assertions.assertThat( comparator.compare(new File(testDataDir, "dir1"), new File(testDataDir, "dir1"))) 75 | .describedAs("Directories should fall back to name compare") 76 | .isEqualTo(0); 77 | Assertions.assertThat(comparator.compare(new File(testDataDir, "dir2"), new File(testDataDir, "dir1"))) 78 | .describedAs("Directories should fall back to name compare") 79 | .isGreaterThan(0); 80 | } 81 | 82 | @Test 83 | void testOrderBySize_Descending() { 84 | final FileMetaDataComparator comparator = new FileMetaDataComparator(OrderBy.Size, OrderDirection.Descending); 85 | 86 | Assertions.assertThat(comparator.compare(aaa, aaa)) 87 | .describedAs("Same file size should equal") 88 | .isEqualTo(0); 89 | Assertions.assertThat(comparator.compare(aaa, bbb)) 90 | .describedAs("Less than size should be greater than") 91 | .isGreaterThan(0); 92 | Assertions.assertThat(comparator.compare(bbb, aaa)) 93 | .describedAs("Greater than size should be less than") 94 | .isLessThan(0); 95 | } 96 | 97 | @Test 98 | void testOrderByType() { 99 | final FileMetaDataComparator comparator = new FileMetaDataComparator(OrderBy.Type, OrderDirection.Ascending); 100 | 101 | Assertions.assertThat(comparator.compare(aaa, aaa)) 102 | .describedAs("Same file type date should equal - no extension") 103 | .isEqualTo(0); 104 | Assertions.assertThat(comparator.compare(emptyTxt, emptyTxt)) 105 | .describedAs("Same file type date should equal - with extension") 106 | .isEqualTo(0); 107 | Assertions.assertThat(comparator.compare(emptyTxt, emptyXml)) 108 | .describedAs("Alpha order type - lt") 109 | .isLessThan(0); 110 | Assertions.assertThat(comparator.compare(emptyXml, emptyTxt)) 111 | .describedAs("Alpha order type - gt") 112 | .isGreaterThan(0); 113 | 114 | Assertions.assertThat(comparator.compare(dir1, dir1)) 115 | .describedAs("Same file type - directory - should equal - no extension") 116 | .isEqualTo(0); 117 | Assertions.assertThat(comparator.compare(dir1, dir2)) 118 | .describedAs("Alpha order type - directory - lt") 119 | .isLessThan(0); 120 | Assertions.assertThat(comparator.compare(dir2, dir1)) 121 | .describedAs("Alpha order type - directory - gt") 122 | .isGreaterThan(0); 123 | 124 | Assertions.assertThat(comparator.compare(dir1, aaa)) 125 | .describedAs("Alpha order type - file and directory - lt - dirs sort before files") 126 | .isLessThan(0); 127 | Assertions.assertThat(comparator.compare(aaa, dir1)) 128 | .describedAs("Alpha order type - file and directory - gt - dirs sort before files") 129 | .isGreaterThan(0); 130 | } 131 | 132 | @Test 133 | void testOrderByType_Descending() { 134 | final FileMetaDataComparator comparator = new FileMetaDataComparator(OrderBy.Type, OrderDirection.Descending); 135 | 136 | Assertions.assertThat(comparator.compare(aaa, aaa)) 137 | .describedAs("Same file type date should equal - no extension") 138 | .isEqualTo(0); 139 | Assertions.assertThat(comparator.compare(emptyTxt, emptyTxt)) 140 | .describedAs("Same file type date should equal - with extension") 141 | .isEqualTo(0); 142 | Assertions.assertThat(comparator.compare(emptyTxt, emptyXml)) 143 | .describedAs("Alpha order type - lt") 144 | .isGreaterThan(0); 145 | Assertions.assertThat(comparator.compare(emptyXml, emptyTxt)) 146 | .describedAs("Alpha order type - gt") 147 | .isLessThan(0); 148 | 149 | Assertions.assertThat(comparator.compare(dir1, dir1)) 150 | .describedAs("Same file type - directory - should equal - no extension") 151 | .isEqualTo(0); 152 | Assertions.assertThat(comparator.compare(dir1, dir2)) 153 | .describedAs("Alpha order type - directory - lt") 154 | .isGreaterThan(0); 155 | Assertions.assertThat(comparator.compare(dir2, dir1)) 156 | .describedAs("Alpha order type - directory - gt") 157 | .isLessThan(0); 158 | 159 | Assertions.assertThat(comparator.compare(dir1, aaa)) 160 | .describedAs("Alpha order type - file and directory - lt - dirs sort before files") 161 | .isGreaterThan(0); 162 | Assertions.assertThat(comparator.compare(aaa, dir1)) 163 | .describedAs("Alpha order type - file and directory - gt - dirs sort before files") 164 | .isLessThan(0); 165 | } 166 | 167 | @Test 168 | void testOrderByName() { 169 | final FileMetaDataComparator comparator = new FileMetaDataComparator(OrderBy.Name, OrderDirection.Ascending); 170 | 171 | Assertions.assertThat(comparator.compare(aaa, aaa)) 172 | .describedAs("Same file name should equal") 173 | .isEqualTo(0); 174 | Assertions.assertThat(comparator.compare(aaa, bbb)) 175 | .describedAs("Less than name should be less than") 176 | .isLessThan(0); 177 | Assertions.assertThat(comparator.compare(bbb, aaa)) 178 | .describedAs("Greater than name should be greater than") 179 | .isGreaterThan(0); 180 | } 181 | 182 | @Test 183 | void testOrderByName_Descending() { 184 | final FileMetaDataComparator comparator = new FileMetaDataComparator(OrderBy.Name, OrderDirection.Descending); 185 | 186 | Assertions.assertThat(comparator.compare(aaa, aaa)) 187 | .describedAs("Same file name should equal") 188 | .isEqualTo(0); 189 | Assertions.assertThat(comparator.compare(aaa, bbb)) 190 | .describedAs("Less than name should be greater than") 191 | .isGreaterThan(0); 192 | Assertions.assertThat(comparator.compare(bbb, aaa)) 193 | .describedAs("Greater than name should be less than") 194 | .isLessThan(0); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/PropertiesPreviewPane.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.icons.Icons; 4 | import com.chainstaysoftware.filechooser.icons.IconsImpl; 5 | import com.chainstaysoftware.filechooser.preview.PreviewPane; 6 | import com.chainstaysoftware.filechooser.preview.PreviewPaneQuery; 7 | import javafx.application.Platform; 8 | import javafx.geometry.Pos; 9 | import javafx.scene.Cursor; 10 | import javafx.scene.control.Label; 11 | import javafx.scene.control.OverrunStyle; 12 | import javafx.scene.image.Image; 13 | import javafx.scene.image.ImageView; 14 | import javafx.scene.layout.GridPane; 15 | import javafx.scene.layout.HBox; 16 | import javafx.scene.layout.Pane; 17 | import javafx.scene.layout.Priority; 18 | import javafx.scene.layout.Region; 19 | import javafx.scene.layout.VBox; 20 | import org.apache.commons.io.FileUtils; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.nio.file.Files; 25 | import java.nio.file.attribute.BasicFileAttributes; 26 | import java.nio.file.attribute.FileTime; 27 | import java.time.ZoneId; 28 | import java.time.ZonedDateTime; 29 | import java.time.format.DateTimeFormatter; 30 | import java.time.format.FormatStyle; 31 | import java.util.Map; 32 | import java.util.Optional; 33 | import java.util.ResourceBundle; 34 | import java.util.logging.Level; 35 | import java.util.logging.Logger; 36 | 37 | /** 38 | * Pane used to preview a file contents and show the properties of a file (name, size, etc). 39 | */ 40 | public class PropertiesPreviewPane { 41 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.PropertiesPreviewPane"); 42 | 43 | private static final int CREATED_LABEL_COL = 0; 44 | private static final int CREATED_LABEL_ROW = 0; 45 | private static final int CREATED_VAL_COL = 1; 46 | private static final int CREATED_VAL_ROW = 0; 47 | 48 | private static final int MODIFIED_LABEL_COL = 0; 49 | private static final int MODIFIED_LABEL_ROW = 1; 50 | private static final int MODIFIED_VAL_COL = 1; 51 | private static final int MODIFIED_VAL_ROW = 1; 52 | 53 | private static final int LASTOPENED_LABEL_COL = 0; 54 | private static final int LASTOPENED_LABEL_ROW = 2; 55 | private static final int LASTOPENED_VAL_COL = 1; 56 | private static final int LASTOPENED_VAL_ROW = 2; 57 | 58 | private static final int SIZE_LABEL_COL = 0; 59 | private static final int SIZE_LABEL_ROW = 3; 60 | private static final int SIZE_VAL_COL = 1; 61 | private static final int SIZE_VAL_ROW = 3; 62 | 63 | private final ResourceBundle resourceBundle = ResourceBundle.getBundle("filechooser"); 64 | private final Icons icons; 65 | private final Map> previewHandlers; 66 | 67 | private final VBox vBox; 68 | private final Label nameLabel = createNameValueLabel(); 69 | private final Label createdValLabel = createValueLabel(); 70 | private final Label modifiedValLabel = createValueLabel(); 71 | private final Label lastOpenedLabel = createValueLabel(); 72 | private final Label sizeLabel = createValueLabel(); 73 | private final HBox previewPaneContainerPane = createPreviewContainerPane(); 74 | private final ImageView imageView = createImageView(); 75 | 76 | public PropertiesPreviewPane(final Map> previewHandlers, 77 | final Icons icons) 78 | { 79 | this.previewHandlers = previewHandlers; 80 | this.icons = icons; 81 | 82 | final GridPane gridPane = createGridPane(); 83 | 84 | vBox = new VBox(); 85 | vBox.setId("propertiesPreviewVbox"); 86 | vBox.getStyleClass().add("propertiespreview-vbox"); 87 | vBox.setAlignment(Pos.CENTER); 88 | vBox.getChildren().addAll(previewPaneContainerPane, nameLabel, gridPane); 89 | 90 | previewPaneContainerPane.minWidthProperty().bind(vBox.minWidthProperty()); 91 | previewPaneContainerPane.maxWidthProperty().bind(vBox.maxWidthProperty()); 92 | previewPaneContainerPane.prefWidthProperty().bind(vBox.prefWidthProperty()); 93 | VBox.setVgrow(previewPaneContainerPane, Priority.ALWAYS); 94 | } 95 | 96 | /** 97 | * Create control to contain the preview for a file. 98 | */ 99 | private HBox createPreviewContainerPane() { 100 | final HBox hBox = new HBox(); 101 | hBox.setId("previewContainerPane"); 102 | hBox.setAlignment(Pos.CENTER); 103 | return hBox; 104 | } 105 | 106 | /** 107 | * Create the Grid to contain the properties for the previewed file. 108 | */ 109 | private GridPane createGridPane() { 110 | final GridPane gridPane = new GridPane(); 111 | gridPane.setId("propertiesPreviewGrid"); 112 | gridPane.getStyleClass().add("propertiespreview-grid"); 113 | 114 | gridPane.add(createLabel("propertiespreview.create"), CREATED_LABEL_COL, CREATED_LABEL_ROW); 115 | gridPane.add(createdValLabel, CREATED_VAL_COL, CREATED_VAL_ROW); 116 | 117 | gridPane.add(createLabel("propertiespreview.modified"), MODIFIED_LABEL_COL, MODIFIED_LABEL_ROW); 118 | gridPane.add(modifiedValLabel, MODIFIED_VAL_COL, MODIFIED_VAL_ROW); 119 | 120 | gridPane.add(createLabel("propertiespreview.lastopened"), LASTOPENED_LABEL_COL, LASTOPENED_LABEL_ROW); 121 | gridPane.add(lastOpenedLabel, LASTOPENED_VAL_COL, LASTOPENED_VAL_ROW); 122 | 123 | gridPane.add(createLabel("propertiespreview.size"), SIZE_LABEL_COL, SIZE_LABEL_ROW); 124 | gridPane.add(sizeLabel, SIZE_VAL_COL, SIZE_VAL_ROW); 125 | 126 | gridPane.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); 127 | 128 | return gridPane; 129 | } 130 | 131 | /** 132 | * Create ImageView to contain the Icon for the previewed file if no other 133 | * preview is available. 134 | */ 135 | private ImageView createImageView() { 136 | final ImageView view = new ImageView(); 137 | view.setId("propertiesPreviewImageView"); 138 | view.setPreserveRatio(true); 139 | return view; 140 | } 141 | 142 | /** 143 | * Sets the file to display within the Pane. 144 | * 145 | * @param file 146 | */ 147 | public void setFile(final File file) { 148 | vBox.getScene().setCursor(Cursor.WAIT); 149 | 150 | Platform.runLater(() -> { 151 | try { 152 | setContainerNode(file); 153 | 154 | nameLabel.setText(file.getName()); 155 | 156 | final BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class); 157 | 158 | createdValLabel.setText(formatTime(attr.creationTime())); 159 | modifiedValLabel.setText(formatTime(attr.lastModifiedTime())); 160 | lastOpenedLabel.setText(formatTime(attr.lastAccessTime())); 161 | sizeLabel.setText(FileUtils.byteCountToDisplaySize(attr.size())); 162 | } catch (IOException e) { 163 | logger.log(Level.WARNING, "Could not retrieve file attributes for - " + file, e); 164 | 165 | imageView.setImage(null); 166 | nameLabel.setText(null); 167 | createdValLabel.setText(null); 168 | modifiedValLabel.setText(null); 169 | lastOpenedLabel.setText(null); 170 | sizeLabel.setText(null); 171 | } 172 | 173 | vBox.getScene().setCursor(null); 174 | }); 175 | } 176 | 177 | /** 178 | * Update the preview node with the preview of the passed in file. 179 | */ 180 | private void setContainerNode(final File file) { 181 | final Class previewPaneClass = PreviewPaneQuery.query(previewHandlers, file); 182 | if (previewPaneClass == null) { 183 | final Image image = file.isDirectory() 184 | ? icons.getIcon(IconsImpl.FOLDER_64) 185 | : icons.getIconForFile(file); 186 | imageView.setImage(image); 187 | previewPaneContainerPane.getChildren().setAll(imageView); 188 | } else { 189 | final Optional previewPaneOpt = PreviewPaneFactory.create(previewPaneClass); 190 | if (!previewPaneOpt.isPresent()) { 191 | logger.log(Level.SEVERE, "No PreviewPane created."); 192 | return; 193 | } 194 | 195 | final PreviewPane previewPane = previewPaneOpt.get(); 196 | previewPaneContainerPane.getChildren().setAll(previewPane.getPane()); 197 | previewPane.setFile(file); 198 | HBox.setHgrow(previewPane.getPane(), Priority.ALWAYS); 199 | } 200 | } 201 | 202 | private String formatTime(final FileTime fileTime) { 203 | final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(fileTime.toInstant(), 204 | ZoneId.systemDefault()); 205 | 206 | return DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG) 207 | .format(zonedDateTime); 208 | } 209 | 210 | public Pane getPane() { 211 | return vBox; 212 | } 213 | 214 | private Label createLabel(final String resourceTag) { 215 | final Label label = new Label(resourceBundle.getString(resourceTag)); 216 | label.getStyleClass().add("propertiespreview-label"); 217 | return label; 218 | } 219 | 220 | private Label createValueLabel() { 221 | final Label label = new Label(); 222 | label.getStyleClass().add("propertiespreview-valuelabel"); 223 | return label; 224 | } 225 | 226 | private Label createNameValueLabel() { 227 | final Label label = new Label(); 228 | label.getStyleClass().add("propertiespreview-namevaluelabel"); 229 | label.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); 230 | return label; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/PlacesTreeItemCellFactory.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.icons.IconsImpl; 4 | import javafx.collections.ObservableList; 5 | import javafx.event.EventHandler; 6 | import javafx.scene.SnapshotParameters; 7 | import javafx.scene.control.TreeCell; 8 | import javafx.scene.control.TreeItem; 9 | import javafx.scene.control.TreeView; 10 | import javafx.scene.image.Image; 11 | import javafx.scene.image.ImageView; 12 | import javafx.scene.input.ClipboardContent; 13 | import javafx.scene.input.DragEvent; 14 | import javafx.scene.input.Dragboard; 15 | import javafx.scene.input.MouseEvent; 16 | import javafx.scene.input.TransferMode; 17 | import javafx.scene.paint.Color; 18 | import javafx.util.Callback; 19 | 20 | import javax.swing.filechooser.FileSystemView; 21 | import java.io.File; 22 | import java.util.Collections; 23 | import java.util.LinkedList; 24 | import java.util.List; 25 | import java.util.Optional; 26 | import java.util.stream.Collectors; 27 | 28 | /** 29 | * CellFactory for rendering {@link PlacesTreeItem} instances within a 30 | * {@link TreeView} 31 | */ 32 | public class PlacesTreeItemCellFactory implements Callback, TreeCell> { 33 | private final ObservableList favoriteDirs; 34 | 35 | public PlacesTreeItemCellFactory(final ObservableList favoriteDirs) { 36 | this.favoriteDirs = favoriteDirs; 37 | } 38 | 39 | @Override 40 | public TreeCell call(TreeView param) { 41 | return new PlacesTreeItemCell(); 42 | } 43 | 44 | private class PlacesTreeItemCell extends TreeCell { 45 | public PlacesTreeItemCell() { 46 | setOnDragDetected(new DragDetectedHandler()); 47 | setOnDragOver(new DragOverHandler()); 48 | setOnDragEntered(new DragEnteredHandler()); 49 | setOnDragExited(new DragExitedHandler()); 50 | setOnDragDropped(new DragDroppedHandler()); 51 | } 52 | 53 | @Override 54 | protected void updateItem(PlacesTreeItem item, boolean empty) { 55 | super.updateItem(item, empty); 56 | 57 | if (empty || item == null) { 58 | setText(null); 59 | setGraphic(null); 60 | } else if (!item.getFile().isPresent()) { 61 | setText(item.getText().orElse("")); 62 | setGraphic(toGraphic(item.getIcon())); 63 | } else { 64 | final File file = item.getFile().orElseThrow(IllegalStateException::new); 65 | final String systemDisplayName = FileSystemView.getFileSystemView().getSystemDisplayName(file); 66 | setText("".equals(systemDisplayName) ? file.toString() : systemDisplayName); 67 | setGraphic(toGraphic(item.getIcon())); 68 | } 69 | } 70 | 71 | /** 72 | * Handler to setup the {@link Dragboard} if the {@link PlacesTreeItemCell} 73 | * supports drag and drop. Note that drag and drop is only supported to 74 | * reorder favorites. 75 | */ 76 | private class DragDetectedHandler implements EventHandler { 77 | @Override 78 | public void handle(MouseEvent mouseEvent) { 79 | if (!getTreeItem().isLeaf() || !getItem().isFavorite()) { 80 | return; 81 | } 82 | 83 | final SnapshotParameters snapshotParameters = new SnapshotParameters(); 84 | snapshotParameters.setFill(Color.TRANSPARENT); 85 | final Dragboard dragboard = getTreeView().startDragAndDrop(TransferMode.MOVE); 86 | dragboard.setDragView(snapshot(snapshotParameters, null)); 87 | final ClipboardContent content = new ClipboardContent(); 88 | content.putFiles(Collections.singletonList(getItem().getFile().get())); 89 | dragboard.setContent(content); 90 | mouseEvent.consume(); 91 | } 92 | } 93 | 94 | /** 95 | * Handler to indicate that the {@link PlacesTreeItemCell} can accept 96 | * move operations (if supported). 97 | */ 98 | private class DragOverHandler implements EventHandler { 99 | @Override 100 | public void handle(DragEvent event) { 101 | // Only supporting reordering of favorites. 102 | if (event.getDragboard().hasFiles() && getItem() != null && getItem().isFavorite()) { 103 | event.acceptTransferModes(TransferMode.MOVE); 104 | } 105 | } 106 | } 107 | 108 | /** 109 | * Handler to apply styling to {@link PlacesTreeItemCell} when drag 110 | * has moved onto a cell. Only applies styling if the cell can accept 111 | * move operations. 112 | */ 113 | private class DragEnteredHandler implements EventHandler { 114 | @Override 115 | public void handle(DragEvent event) { 116 | if (getItem() == null || !getItem().isFavorite()) { 117 | return; 118 | } 119 | 120 | final File draggedFile = event.getDragboard().getFiles().get(0); 121 | if (getItem().getFile().equals(Optional.of(draggedFile))) { 122 | // NOOP. 123 | return; 124 | } 125 | 126 | getStyleClass().add("places-droptarget"); 127 | } 128 | } 129 | 130 | /** 131 | * Handler to remove styling when drag has moved off of cell. 132 | */ 133 | private class DragExitedHandler implements EventHandler { 134 | @Override 135 | public void handle(DragEvent event) { 136 | getStyleClass().remove("places-droptarget"); 137 | } 138 | } 139 | 140 | /** 141 | * Handler to apply move (drop) operation. 142 | */ 143 | private class DragDroppedHandler implements EventHandler { 144 | @Override 145 | public void handle(DragEvent event) { 146 | final TreeItem dropTarget = getTreeItem(); 147 | final File draggedFile = event.getDragboard().getFiles().get(0); 148 | 149 | if (dropTarget.getValue().getFile().equals(Optional.of(draggedFile))) { 150 | // NOOP. 151 | return; 152 | } 153 | 154 | if (!dropTarget.isLeaf()) { 155 | moveToHead(dropTarget, draggedFile); 156 | } else { 157 | moveAfter(dropTarget, draggedFile); 158 | } 159 | } 160 | 161 | /** 162 | * Move the draggedDir to the head of the list. Update the View and 163 | * the favoriteDirs property. 164 | */ 165 | private void moveToHead(final TreeItem dropTarget, 166 | final File draggedDir) { 167 | final List> currentItems = dropTarget.getChildren(); 168 | if (currentItems.isEmpty()) { 169 | // NOOP 170 | return; 171 | } 172 | 173 | final List> updated = new LinkedList<>(); 174 | updated.add(null); 175 | for (TreeItem item : currentItems) { 176 | if (item.getValue().getFile().equals(Optional.of(draggedDir))) { 177 | updated.set(0, item); 178 | } else { 179 | updated.add(item); 180 | } 181 | } 182 | 183 | dropTarget.getChildren().setAll(updated); 184 | updateFavoriteDirsProperty(updated); 185 | } 186 | 187 | /** 188 | * Move the draggedDir to the position After the dropTarget. Update the View and 189 | * the favoriteDirs property. 190 | */ 191 | private void moveAfter(final TreeItem dropTarget, 192 | final File draggedFile) { 193 | final List> currentItems = dropTarget.getParent().getChildren(); 194 | final List> updated = new LinkedList<>(); 195 | 196 | final TreeItem draggedItem = currentItems.stream() 197 | .filter(ti -> ti.getValue().getFile().equals(Optional.of(draggedFile))) 198 | .findFirst() 199 | .orElseThrow(IllegalStateException::new); 200 | 201 | for (TreeItem item : currentItems) { 202 | if (item.equals(draggedItem)) { 203 | continue; 204 | } 205 | 206 | updated.add(item); 207 | 208 | if (item.equals(dropTarget)) { 209 | // insert draggedItem after this item. 210 | updated.add(draggedItem); 211 | } 212 | } 213 | 214 | dropTarget.getParent().getChildren().setAll(updated); 215 | updateFavoriteDirsProperty(updated); 216 | } 217 | 218 | /** 219 | * Update the favoriteDirs property, so that the user can optionally 220 | * persist the new ordering. 221 | */ 222 | private void updateFavoriteDirsProperty(final List> updated) { 223 | final List updatedFavDirs = updated.stream() 224 | .map(ti -> ti.getValue().getFile().get()) 225 | .collect(Collectors.toList()); 226 | favoriteDirs.setAll(updatedFavDirs); 227 | } 228 | } 229 | 230 | /** 231 | * Create an {@link ImageView} from an {@link Image} 232 | */ 233 | private ImageView toGraphic(final Image image) { 234 | if (image == null) { 235 | return null; 236 | } 237 | 238 | final ImageView graphic = new ImageView(image); 239 | graphic.setFitWidth(IconsImpl.SMALL_ICON_WIDTH); 240 | graphic.setFitHeight(IconsImpl.SMALL_ICON_HEIGHT); 241 | graphic.setPreserveRatio(true); 242 | return graphic; 243 | } 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/ListFilesWithPreviewView.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.icons.Icons; 4 | import com.chainstaysoftware.filechooser.preview.PreviewPane; 5 | import javafx.application.Platform; 6 | import javafx.beans.property.ReadOnlyObjectWrapper; 7 | import javafx.beans.value.ChangeListener; 8 | import javafx.beans.value.ObservableValue; 9 | import javafx.collections.FXCollections; 10 | import javafx.collections.ObservableList; 11 | import javafx.collections.transformation.SortedList; 12 | import javafx.concurrent.Service; 13 | import javafx.concurrent.Task; 14 | import javafx.event.EventHandler; 15 | import javafx.geometry.Pos; 16 | import javafx.scene.Cursor; 17 | import javafx.scene.Node; 18 | import javafx.scene.control.Label; 19 | import javafx.scene.control.SplitPane; 20 | import javafx.scene.control.TableColumn; 21 | import javafx.scene.control.TableView; 22 | import javafx.scene.input.KeyCode; 23 | import javafx.scene.input.KeyEvent; 24 | import javafx.scene.layout.HBox; 25 | import javafx.scene.layout.Priority; 26 | import javafx.stage.Stage; 27 | 28 | import java.io.File; 29 | import java.nio.file.DirectoryStream; 30 | import java.nio.file.Path; 31 | import java.util.LinkedList; 32 | import java.util.List; 33 | import java.util.Map; 34 | import java.util.ResourceBundle; 35 | import java.util.concurrent.CountDownLatch; 36 | import java.util.function.Predicate; 37 | import java.util.logging.Logger; 38 | 39 | /** 40 | * View files as list along with preview of file. 41 | */ 42 | class ListFilesWithPreviewView extends AbstractFilesView { 43 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.ListFilesWithPreviewView"); 44 | 45 | private final TableView tableView = new TableView<>(); 46 | private final SplitPane splitPane; 47 | private final HBox previewHbox; 48 | private final PropertiesPreviewPane propertiesPreviewPane; 49 | private final List> sortOrder; 50 | private final FilesViewCallback callback; 51 | 52 | private EventHandler keyEventHandler; 53 | private final TableColumn nameColumn; 54 | 55 | ListFilesWithPreviewView(final Stage parent, 56 | final Map> previewHandlers, 57 | final Icons icons, 58 | final double dividerPosition, 59 | final FilesViewCallback callback) { 60 | super(parent); 61 | 62 | propertiesPreviewPane = new PropertiesPreviewPane(previewHandlers, icons); 63 | this.callback = callback; 64 | 65 | previewHbox = new HBox(); 66 | previewHbox.setId("previewHbox"); 67 | previewHbox.setAlignment(Pos.CENTER); 68 | previewHbox.setMinSize(0, 0); 69 | 70 | final ResourceBundle resourceBundle = ResourceBundle.getBundle("filechooser"); 71 | nameColumn = new TableColumn<>(resourceBundle.getString("listfilesview.name")); 72 | nameColumn.setCellValueFactory(param -> new ReadOnlyObjectWrapper<>(param.getValue())); 73 | nameColumn.setCellFactory(new DirListNameColumnCellFactory(true, callback, icons)); 74 | nameColumn.prefWidthProperty().bind(tableView.widthProperty()); 75 | nameColumn.setComparator((o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getFile().getName(), o2.getFile().getName())); 76 | nameColumn.setSortType(orderDirectionToSortType(callback.orderDirectionProperty().get())); 77 | nameColumn.sortTypeProperty().addListener((observable, oldValue, newValue) -> 78 | callback.orderDirectionProperty().set(sortTypeToOrderDirection(newValue))); 79 | 80 | tableView.getColumns().addAll(nameColumn); 81 | tableView.setOnKeyPressed(new KeyPressedHandler()); 82 | tableView.getSelectionModel().selectedItemProperty().addListener(new SelectedItemChanged()); 83 | tableView.setPlaceholder(new Label("")); 84 | sortOrder = new LinkedList<>(); 85 | sortOrder.add(nameColumn); 86 | 87 | splitPane = new SplitPane(); 88 | splitPane.setId("previewSplitPane"); 89 | splitPane.getItems().addAll(tableView, previewHbox); 90 | splitPane.setDividerPositions(dividerPosition); 91 | } 92 | 93 | @Override 94 | public Node getNode() { 95 | return splitPane; 96 | } 97 | 98 | /** 99 | * sets the files on the view. 100 | * @param directoryStream Filtered stream of files to show in view. 101 | * @param remainingDirectoryStream This stream contains the files not to 102 | * show in the view. But, may include 103 | * directories that should be shown in the 104 | * view but were filtered out in the directoryStream. 105 | */ 106 | @Override 107 | public void setFiles(final DirectoryStream directoryStream, 108 | final DirectoryStream remainingDirectoryStream) { 109 | saveSortOrder(); 110 | 111 | final ObservableList directoryListItems = FXCollections.observableArrayList(); 112 | final SortedList items = directoryListItems.sorted(); 113 | items.comparatorProperty().bind(tableView.comparatorProperty()); 114 | tableView.setItems(items); 115 | 116 | // Update the TableView from Services so that the UI is not blocked on OS calls. 117 | final UpdateDirectoryList updateDirectoryListService = new UpdateDirectoryList(directoryStream, remainingDirectoryStream, directoryListItems); 118 | 119 | final Predicate shouldHideFile 120 | = new ShowHiddenFilesPredicate(callback.showHiddenFilesProperty(), callback.shouldHideFilesProperty()); 121 | final FilterHiddenFromDirList filterListService = new FilterHiddenFromDirList(directoryListItems, shouldHideFile); 122 | 123 | final SelectCurrentService selectCurrentService = new SelectCurrentService(); 124 | 125 | filterListService.setOnSucceeded(event -> selectCurrentService.start()); 126 | 127 | updateDirectoryListService.setOnSucceeded(event -> { 128 | filterListService.start(); 129 | tableView.setCursor(null); 130 | }); 131 | updateDirectoryListService.setOnRunning(event -> tableView.setCursor(Cursor.WAIT)); 132 | setServiceFailureHandlers(updateDirectoryListService); 133 | updateDirectoryListService.start(); 134 | } 135 | 136 | /** 137 | * Sets up event handlers to reset wait icon when service fails or is cancelled. 138 | */ 139 | private void setServiceFailureHandlers(final Service service) { 140 | service.setOnCancelled(event -> { 141 | logger.warning("Service cancelled - " + service.getClass().getCanonicalName()); 142 | tableView.setCursor(null); 143 | 144 | }); 145 | service.setOnFailed(event -> { 146 | logger.warning("Service failed - " + service.getClass().getCanonicalName()); 147 | tableView.setCursor(null); 148 | }); 149 | } 150 | 151 | /** 152 | * Save the sort order of the filesTreeView 153 | */ 154 | private void saveSortOrder() { 155 | if (!tableView.getSortOrder().isEmpty()) { 156 | sortOrder.clear(); 157 | sortOrder.addAll(tableView.getSortOrder()); 158 | 159 | // Only name sort is supported. 160 | callback.orderByProperty().setValue(OrderBy.Name); 161 | } 162 | } 163 | 164 | /** 165 | * If there is a currently selected file, then update the TableView with 166 | * the selection. 167 | */ 168 | private class SelectCurrentService extends Service { 169 | 170 | protected Task createTask() { 171 | return new SelectTask(); 172 | } 173 | 174 | private class SelectTask extends Task { 175 | private final CountDownLatch latch = new CountDownLatch(1); 176 | 177 | @Override 178 | protected Void call() throws Exception { 179 | Platform.runLater(() -> { 180 | restoreSortOrder(); 181 | 182 | final File currentSelectedFile = callback.getCurrentSelection(); 183 | tableView.getItems() 184 | .stream() 185 | .filter(item -> compareFilePaths(item.getFile(), currentSelectedFile)) 186 | .findFirst() 187 | .ifPresent(item -> tableView.getSelectionModel().select(item)); 188 | 189 | latch.countDown(); 190 | }); 191 | 192 | latch.await(); 193 | 194 | return null; 195 | } 196 | 197 | /** 198 | * Reapply the sort order of the filesTreeView 199 | */ 200 | private void restoreSortOrder() { 201 | if (sortOrder != null) { 202 | tableView.getSortOrder().clear(); 203 | tableView.getSortOrder().addAll(sortOrder); 204 | sortOrder.get(0).setSortable(true); // This performs a sort 205 | } 206 | } 207 | } 208 | } 209 | 210 | /** 211 | * Map {@link OrderDirection} to {@link javafx.scene.control.TableColumn.SortType} 212 | */ 213 | private TableColumn.SortType orderDirectionToSortType(final OrderDirection orderDirection) { 214 | return OrderDirection.Descending.equals(orderDirection) 215 | ? TableColumn.SortType.DESCENDING 216 | : TableColumn.SortType.ASCENDING; 217 | } 218 | 219 | /** 220 | * Map {@link javafx.scene.control.TableColumn.SortType} to {@link OrderDirection} 221 | */ 222 | private OrderDirection sortTypeToOrderDirection(final TableColumn.SortType sortType) { 223 | return TableColumn.SortType.DESCENDING.equals(sortType) 224 | ? OrderDirection.Descending 225 | : OrderDirection.Ascending; 226 | } 227 | 228 | void setOnKeyPressed(final EventHandler eventHandler) { 229 | this.keyEventHandler = eventHandler; 230 | } 231 | 232 | double getDividerPosition() { 233 | return splitPane.getDividerPositions()[0]; 234 | } 235 | 236 | private class KeyPressedHandler implements EventHandler { 237 | @Override 238 | public void handle(KeyEvent event) { 239 | if (keyEventHandler != null) { 240 | keyEventHandler.handle(event); 241 | } 242 | 243 | if (event.isConsumed()) { 244 | return; 245 | } 246 | 247 | if (KeyCode.ENTER.equals(event.getCode())) { 248 | final File file = tableView.getSelectionModel().getSelectedItem().getFile(); 249 | callback.requestChangeDirectory(file); 250 | } 251 | } 252 | } 253 | 254 | private class SelectedItemChanged implements ChangeListener { 255 | @Override 256 | public void changed(ObservableValue observable, 257 | DirectoryListItem oldValue, 258 | DirectoryListItem newValue) { 259 | previewHbox.getChildren().clear(); 260 | 261 | File newFile = newValue == null ? null : newValue.getFile(); 262 | 263 | ListFilesWithPreviewView.this.getNode().getScene().setCursor(Cursor.WAIT); 264 | Platform.runLater(() -> { 265 | callback.setCurrentSelection(newFile); 266 | ListFilesWithPreviewView.this.getNode().getScene().setCursor(null); 267 | 268 | if (newFile != null) { 269 | preview(newFile); 270 | } 271 | }); 272 | } 273 | 274 | private void preview(final File file) { 275 | previewHbox.getChildren().setAll(propertiesPreviewPane.getPane()); 276 | propertiesPreviewPane.setFile(file); 277 | HBox.setHgrow(propertiesPreviewPane.getPane(), Priority.ALWAYS); 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /licenses/commons-io-license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /licenses/commons-lang-license.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/main/java/com/chainstaysoftware/filechooser/IconsFilesView.java: -------------------------------------------------------------------------------- 1 | package com.chainstaysoftware.filechooser; 2 | 3 | import com.chainstaysoftware.filechooser.icons.Icons; 4 | import com.chainstaysoftware.filechooser.preview.PreviewPane; 5 | import com.chainstaysoftware.filechooser.preview.PreviewPaneQuery; 6 | import impl.org.controlsfx.skin.GridViewSkin; 7 | import javafx.application.Platform; 8 | import javafx.beans.property.IntegerProperty; 9 | import javafx.beans.property.SimpleIntegerProperty; 10 | import javafx.collections.FXCollections; 11 | import javafx.collections.ObservableList; 12 | import javafx.collections.transformation.SortedList; 13 | import javafx.concurrent.Service; 14 | import javafx.concurrent.Task; 15 | import javafx.event.EventHandler; 16 | import javafx.scene.Cursor; 17 | import javafx.scene.Node; 18 | import javafx.scene.control.CheckMenuItem; 19 | import javafx.scene.control.ContextMenu; 20 | import javafx.scene.control.Menu; 21 | import javafx.scene.control.MenuItem; 22 | import javafx.scene.control.RadioMenuItem; 23 | import javafx.scene.control.SeparatorMenuItem; 24 | import javafx.scene.control.ToggleGroup; 25 | import javafx.scene.input.KeyCode; 26 | import javafx.scene.input.KeyEvent; 27 | import javafx.scene.input.MouseButton; 28 | import javafx.scene.input.MouseEvent; 29 | import javafx.stage.Stage; 30 | import org.controlsfx.control.GridView; 31 | 32 | import java.io.File; 33 | import java.nio.file.DirectoryStream; 34 | import java.nio.file.Path; 35 | import java.util.List; 36 | import java.util.Map; 37 | import java.util.ResourceBundle; 38 | import java.util.concurrent.CountDownLatch; 39 | import java.util.function.Predicate; 40 | import java.util.logging.Logger; 41 | import java.util.stream.IntStream; 42 | 43 | class IconsFilesView extends AbstractFilesView { 44 | private static Logger logger = Logger.getLogger("com.chainstaysoftware.filechooser.IconsFilesView"); 45 | 46 | private static final int NOT_SELECTED = -1; 47 | 48 | // Width and Height of Icon and Filename Cell. 49 | private static final int CELL_HEIGHT = 90; 50 | private static final int CELL_WIDTH = 90; 51 | private static final int CELL_SPACING = 6; 52 | 53 | private final GridView gridView = new GridView<>(); 54 | private final Map> previewHandlers; 55 | private final IntegerProperty selectedCellIndex = new SimpleIntegerProperty(NOT_SELECTED); 56 | private final ResourceBundle resourceBundle = ResourceBundle.getBundle("filechooser"); 57 | private final FilesViewCallback callback; 58 | 59 | private EventHandler keyEventHandler; 60 | private boolean disableListeners; 61 | 62 | IconsFilesView(final Stage parent, 63 | final Map> previewHandlers, 64 | final Icons icons, 65 | final FilesViewCallback callback) { 66 | super(parent); 67 | 68 | this.previewHandlers = previewHandlers; 69 | this.callback = callback; 70 | 71 | gridView.setCellFactory(gridView1 -> { 72 | final IconGridCell cell = new IconGridCell(true, new IconGridCellContextMenuFactImpl(), icons); 73 | cell.indexProperty().addListener((observable, oldValue, newValue) -> 74 | cell.updateSelected(selectedCellIndex.intValue() == newValue.intValue())); 75 | selectedCellIndex.addListener((observable, oldValue, newValue) -> 76 | cell.updateSelected(newValue.intValue() == cell.getIndex())); 77 | return cell; 78 | }); 79 | gridView.setCellHeight(CELL_HEIGHT); 80 | gridView.setCellWidth(CELL_WIDTH); 81 | gridView.setHorizontalCellSpacing(CELL_SPACING); 82 | gridView.setVerticalCellSpacing(CELL_SPACING); 83 | gridView.setOnMouseClicked(new MouseClickHandler()); 84 | gridView.setOnKeyPressed(new KeyClickHandler()); 85 | } 86 | 87 | @Override 88 | public Node getNode() { 89 | return gridView; 90 | } 91 | 92 | /** 93 | * sets the files on the view. 94 | * @param directoryStream Filtered stream of files to show in view. 95 | * @param remainingDirectoryStream This stream contains the files not to 96 | * show in the view. But, may include 97 | * directories that should be shown in the 98 | * view but were filtered out in the directoryStream. 99 | */ 100 | @Override 101 | public void setFiles(final DirectoryStream directoryStream, 102 | final DirectoryStream remainingDirectoryStream) { 103 | selectedCellIndex.setValue(NOT_SELECTED); 104 | 105 | // Disable event listeners in gridView while being updated programmatically 106 | disableListeners = true; 107 | final ObservableList directoryListItems = FXCollections.observableArrayList(); 108 | final SortedList items 109 | = directoryListItems.sorted(new DirListItemComparator(callback.orderByProperty().get(), 110 | callback.orderDirectionProperty().get())); 111 | gridView.setItems(items); 112 | disableListeners = false; 113 | 114 | // Update the GridView from Services so that the UI is not blocked on OS calls. 115 | final UpdateDirectoryList updateDirectoryListService = new UpdateDirectoryList(directoryStream, remainingDirectoryStream, 116 | directoryListItems); 117 | 118 | final Predicate shouldHideFile 119 | = new ShowHiddenFilesPredicate(callback.showHiddenFilesProperty(), callback.shouldHideFilesProperty()); 120 | final FilterHiddenFromDirList filterListService = new FilterHiddenFromDirList(directoryListItems, shouldHideFile); 121 | 122 | final SelectCurrentService selectCurrentService = new SelectCurrentService(); 123 | 124 | filterListService.setOnSucceeded(event -> selectCurrentService.start()); 125 | 126 | updateDirectoryListService.setOnSucceeded(event -> { 127 | gridView.setCursor(null); 128 | filterListService.start(); 129 | }); 130 | updateDirectoryListService.setOnRunning(event -> gridView.setCursor(Cursor.WAIT)); 131 | setServiceFailureHandlers(updateDirectoryListService); 132 | updateDirectoryListService.start(); 133 | } 134 | 135 | /** 136 | * Sets up event handlers to reset wait icon when service fails or is cancelled. 137 | */ 138 | private void setServiceFailureHandlers(final Service service) { 139 | service.setOnCancelled(event -> { 140 | logger.warning("Service cancelled - " + service.getClass().getCanonicalName()); 141 | gridView.setCursor(null); 142 | 143 | }); 144 | service.setOnFailed(event -> { 145 | logger.warning("Service failed - " + service.getClass().getCanonicalName()); 146 | gridView.setCursor(null); 147 | }); 148 | } 149 | 150 | /** 151 | * If there is a currently selected file, then update the GridView with 152 | * the selection. 153 | */ 154 | private class SelectCurrentService extends Service { 155 | 156 | protected Task createTask() { 157 | return new SelectTask(); 158 | } 159 | 160 | private class SelectTask extends Task { 161 | private final CountDownLatch latch = new CountDownLatch(1); 162 | 163 | @Override 164 | protected Void call() throws Exception { 165 | Platform.runLater(() -> { 166 | // HACK!! Force repaint of GridView. The GridView was 167 | // not repainting when file filter was changed on a huge directory. 168 | ((GridViewSkin)gridView.getSkin()).updateGridViewItems(); 169 | 170 | selectCurrent(); 171 | 172 | latch.countDown(); 173 | }); 174 | 175 | latch.await(); 176 | 177 | return null; 178 | } 179 | } 180 | } 181 | 182 | private void selectCurrent() { 183 | final File currentSelection = callback.getCurrentSelection(); 184 | final List items = gridView.getItems(); 185 | IntStream.range(0, items.size()) 186 | .filter(i -> compareFilePaths(items.get(i).getFile(), currentSelection)) 187 | .findFirst() 188 | .ifPresent(selectedCellIndex::setValue); 189 | } 190 | 191 | /** 192 | * Sort the existing view contents. 193 | */ 194 | private void sort() { 195 | if (gridView.getItems() instanceof SortedList) { 196 | ((SortedList) gridView.getItems()).setComparator(new DirListItemComparator(callback.orderByProperty().get(), callback.orderDirectionProperty().get())); 197 | selectCurrent(); 198 | } 199 | } 200 | 201 | void setOnKeyPressed(final EventHandler eventHandler) { 202 | this.keyEventHandler = eventHandler; 203 | } 204 | 205 | private class IconGridCellContextMenuFactImpl implements IconGridCellContextMenuFactory { 206 | @Override 207 | public ContextMenu create(final DirectoryListItem item) { 208 | final ContextMenu contextMenu = createContextMenu(); 209 | 210 | final File file = item.getFile(); 211 | if (file.isDirectory()) { 212 | return contextMenu; 213 | } 214 | 215 | final Class previewPaneClass = PreviewPaneQuery.query(previewHandlers, file); 216 | if (previewPaneClass == null) { 217 | return contextMenu; 218 | } 219 | 220 | final MenuItem imagePreviewItem = new MenuItem("Preview"); 221 | imagePreviewItem.setOnAction(v -> showPreview(previewPaneClass, file)); 222 | 223 | contextMenu.getItems().addAll(new SeparatorMenuItem(), imagePreviewItem); 224 | return contextMenu; 225 | } 226 | 227 | private ContextMenu createContextMenu() { 228 | final OrderBy initialOrderBy = callback.orderByProperty().get(); 229 | final OrderDirection initialDirection = callback.orderDirectionProperty().get(); 230 | 231 | final ToggleGroup toggleGroup = new ToggleGroup(); 232 | 233 | final RadioMenuItem nameItem = new RadioMenuItem(); 234 | nameItem.setId("nameMenuItem"); 235 | nameItem.setText(resourceBundle.getString("iconsview.context.name")); 236 | nameItem.setToggleGroup(toggleGroup); 237 | nameItem.onActionProperty().setValue(event -> sort(OrderBy.Name)); 238 | if (OrderBy.Name.equals(initialOrderBy)) { 239 | nameItem.setSelected(true); 240 | } 241 | 242 | final RadioMenuItem sizeItem = new RadioMenuItem(); 243 | sizeItem.setId("sizeMenuItem"); 244 | sizeItem.setText(resourceBundle.getString("iconsview.context.size")); 245 | sizeItem.setToggleGroup(toggleGroup); 246 | sizeItem.onActionProperty().setValue(event -> sort(OrderBy.Size)); 247 | if (OrderBy.Size.equals(initialOrderBy)) { 248 | sizeItem.setSelected(true); 249 | } 250 | 251 | final RadioMenuItem typeItem = new RadioMenuItem(); 252 | typeItem.setId("typeMenuItem"); 253 | typeItem.setText(resourceBundle.getString("iconsview.context.type")); 254 | typeItem.setToggleGroup(toggleGroup); 255 | typeItem.onActionProperty().setValue(event -> sort(OrderBy.Type)); 256 | if (OrderBy.Type.equals(initialOrderBy)) { 257 | typeItem.setSelected(true); 258 | } 259 | 260 | final RadioMenuItem dateItem = new RadioMenuItem(); 261 | dateItem.setId("dateMenuItem"); 262 | dateItem.setText(resourceBundle.getString("iconsview.context.modificationdate")); 263 | dateItem.setToggleGroup(toggleGroup); 264 | dateItem.onActionProperty().setValue(event -> sort(OrderBy.ModificationDate)); 265 | if (OrderBy.ModificationDate.equals(initialOrderBy)) { 266 | dateItem.setSelected(true); 267 | } 268 | 269 | final CheckMenuItem reverseOrder = new CheckMenuItem(); 270 | reverseOrder.setId("reverserOrderItem"); 271 | reverseOrder.setText(resourceBundle.getString("iconsview.context.reverseorder")); 272 | reverseOrder.selectedProperty().addListener((observable, oldValue, newValue) -> { 273 | if (disableListeners) { 274 | return; 275 | } 276 | 277 | callback.orderDirectionProperty().setValue(newValue ? OrderDirection.Descending : OrderDirection.Ascending); 278 | sort(callback.orderByProperty().get()); 279 | }); 280 | 281 | disableListeners = true; 282 | reverseOrder.setSelected(OrderDirection.Descending.equals(initialDirection)); 283 | disableListeners = false; 284 | 285 | final Menu sortOrderMenu = new Menu(); 286 | sortOrderMenu.setId("sortOrderMenu"); 287 | sortOrderMenu.setText(resourceBundle.getString("iconsview.context.arrangeby")); 288 | sortOrderMenu.getItems().addAll(nameItem, sizeItem, typeItem, dateItem, 289 | new SeparatorMenuItem(), reverseOrder); 290 | 291 | final ContextMenu contextMenu = new ContextMenu(); 292 | contextMenu.getItems().addAll(sortOrderMenu); 293 | 294 | return contextMenu; 295 | } 296 | 297 | /** 298 | * Sort the existing view contents. 299 | */ 300 | private void sort(final OrderBy orderBy) { 301 | callback.orderByProperty().set(orderBy); 302 | 303 | IconsFilesView.this.sort(); 304 | } 305 | } 306 | 307 | private class MouseClickHandler implements EventHandler { 308 | @Override 309 | public void handle(MouseEvent event) { 310 | if (!(event.getTarget() instanceof IconGridCell)) { 311 | selectedCellIndex.setValue(NOT_SELECTED); 312 | 313 | Platform.runLater(() -> { 314 | callback.setCurrentSelection(null); 315 | IconsFilesView.this.getNode().getParent().requestLayout(); 316 | }); 317 | return; 318 | } 319 | 320 | final IconGridCell target = (IconGridCell)event.getTarget(); 321 | if (isDoubleClick(event)) { 322 | final File file = (File)target.getUserData(); 323 | if (file.isDirectory()) { 324 | callback.requestChangeDirectory(file); 325 | } else { 326 | callback.fireDoneButton(); 327 | } 328 | } else { 329 | selectedCellIndex.setValue(target.getIndex()); 330 | 331 | final File file = gridView.getItems().get(selectedCellIndex.get()).getFile(); 332 | IconsFilesView.this.getNode().getScene().setCursor(Cursor.WAIT); 333 | Platform.runLater(() -> { 334 | callback.setCurrentSelection(file); 335 | IconsFilesView.this.getNode().getScene().setCursor(null); 336 | }); 337 | } 338 | } 339 | 340 | private boolean isDoubleClick(MouseEvent event) { 341 | return event.getButton().equals(MouseButton.PRIMARY) && event.getClickCount() == 2; 342 | } 343 | } 344 | 345 | private class KeyClickHandler implements EventHandler { 346 | @Override 347 | public void handle(KeyEvent event) { 348 | if (keyEventHandler != null) { 349 | keyEventHandler.handle(event); 350 | 351 | if (event.isConsumed()) { 352 | return; 353 | } 354 | } 355 | 356 | final KeyCode keyCode = event.getCode(); 357 | if (keyCode == KeyCode.LEFT) { 358 | selectedCellIndex.setValue(Math.max(0, selectedCellIndex.get() - 1)); 359 | event.consume(); 360 | } else if (keyCode == KeyCode.RIGHT) { 361 | selectedCellIndex.setValue(Math.min(selectedCellIndex.get() + 1, gridView.getItems().size() - 1)); 362 | event.consume(); 363 | } else if (keyCode == KeyCode.UP) { 364 | selectedCellIndex.setValue(Math.max(0, selectedCellIndex.get() - ((GridViewSkin)gridView.getSkin()).computeMaxCellsInRow())); 365 | event.consume(); 366 | } else if (keyCode == KeyCode.DOWN) { 367 | selectedCellIndex.setValue(Math.min(gridView.getItems().size() - 1, selectedCellIndex.get() + ((GridViewSkin)gridView.getSkin()).computeMaxCellsInRow())); 368 | event.consume(); 369 | } else if (keyCode == KeyCode.ENTER) { 370 | if (selectedCellIndex.get() == NOT_SELECTED) { 371 | return; 372 | } 373 | 374 | final DirectoryListItem item = gridView.getItems().get(selectedCellIndex.get()); 375 | final File file = item.getFile(); 376 | if (!file.isDirectory()) { 377 | return; 378 | } 379 | 380 | callback.requestChangeDirectory(file); 381 | event.consume(); 382 | } 383 | 384 | final File file = selectedCellIndex.get() != NOT_SELECTED 385 | ? gridView.getItems().get(selectedCellIndex.get()).getFile() 386 | : null; 387 | 388 | callback.setCurrentSelection(file); 389 | } 390 | } 391 | } 392 | --------------------------------------------------------------------------------