diff --git a/.gitignore b/.gitignore index 1e9eed6..0dfc64b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ xcuserdata .kotlin .idea/* !.idea/runConfigurations/ +Package.resolved \ No newline at end of file diff --git a/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt b/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt index 8a43a70..94659a3 100644 --- a/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt +++ b/filekit-core/src/androidMain/kotlin/io/github/vinceglb/filekit/core/FileKit.android.kt @@ -7,6 +7,7 @@ import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageAndVideo import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia.ImageOnly @@ -49,41 +50,36 @@ public actual object FileKit { PickerType.Image, PickerType.Video, PickerType.ImageAndVideo -> { - when (mode) { - is PickerMode.Single -> { + val request = when (type) { + PickerType.Image -> PickVisualMediaRequest(ImageOnly) + PickerType.Video -> PickVisualMediaRequest(VideoOnly) + PickerType.ImageAndVideo -> PickVisualMediaRequest(ImageAndVideo) + else -> throw IllegalArgumentException("Unsupported type: $type") + } + + val launcher = when { + mode is PickerMode.Single || mode is PickerMode.Multiple && mode.maxItems == 1 -> { val contract = PickVisualMedia() - val launcher = registry.register(key, contract) { uri -> + registry.register(key, contract) { uri -> val result = uri?.let { listOf(PlatformFile(it, context)) } continuation.resume(result) } - - val request = when (type) { - PickerType.Image -> PickVisualMediaRequest(ImageOnly) - PickerType.Video -> PickVisualMediaRequest(VideoOnly) - PickerType.ImageAndVideo -> PickVisualMediaRequest(ImageAndVideo) - else -> throw IllegalArgumentException("Unsupported type: $type") - } - - launcher.launch(request) } - is PickerMode.Multiple -> { - val contract = ActivityResultContracts.PickMultipleVisualMedia() - val launcher = registry.register(key, contract) { uri -> + mode is PickerMode.Multiple -> { + val contract = when { + mode.maxItems != null -> PickMultipleVisualMedia(mode.maxItems) + else -> PickMultipleVisualMedia() + } + registry.register(key, contract) { uri -> val result = uri.map { PlatformFile(it, context) } continuation.resume(result) } - - val request = when (type) { - PickerType.Image -> PickVisualMediaRequest(ImageOnly) - PickerType.Video -> PickVisualMediaRequest(VideoOnly) - PickerType.ImageAndVideo -> PickVisualMediaRequest(ImageAndVideo) - else -> throw IllegalArgumentException("Unsupported type: $type") - } - - launcher.launch(request) } + + else -> throw IllegalArgumentException("Unsupported mode: $mode") } + launcher.launch(request) } is PickerType.File -> { @@ -98,6 +94,8 @@ public actual object FileKit { } is PickerMode.Multiple -> { + // TODO there might be a way to limit the amount of documents, but + // I haven't found it yet. val contract = ActivityResultContracts.OpenMultipleDocuments() val launcher = registry.register(key, contract) { uris -> val result = uris.map { PlatformFile(it, context) } diff --git a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt index ba8d1cc..2638f8b 100644 --- a/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt +++ b/filekit-core/src/commonMain/kotlin/io/github/vinceglb/filekit/core/PickerMode.kt @@ -9,7 +9,17 @@ public sealed class PickerMode { } } - public data object Multiple : PickerMode() { + /** + * @property maxItems sets the limit of how many items can be selected. NOTE: This is only + * supported by Android / iOS and only when picking media files, not any kind of file. + */ + public data class Multiple(val maxItems: Int? = null) : PickerMode() { + init { + require(maxItems == null || maxItems in 1..50) { + "maxItems must be contained between 1 <= maxItems <= 50 but current value is $maxItems" + } + } + override fun parseResult(value: PlatformFiles?): PlatformFiles? { return value?.takeIf { it.isNotEmpty() } } diff --git a/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt b/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt index d5de2b8..dc2ae63 100644 --- a/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt +++ b/filekit-core/src/iosMain/kotlin/io/github/vinceglb/filekit/core/FileKit.ios.kt @@ -49,7 +49,7 @@ public actual object FileKit { // Use PHPickerViewController for images and videos is PickerType.Image, is PickerType.Video -> callPhPicker( - isMultipleMode = mode is PickerMode.Multiple, + mode = mode, type = type )?.map { PlatformFile(it) }?.let { mode.parseResult(it) } @@ -163,8 +163,8 @@ public actual object FileKit { } @OptIn(ExperimentalForeignApi::class) - private suspend fun callPhPicker( - isMultipleMode: Boolean, + private suspend fun callPhPicker( + mode: PickerMode, type: PickerType, ): List? { val pickerResults: List = suspendCoroutine { continuation -> @@ -177,7 +177,10 @@ public actual object FileKit { val configuration = PHPickerConfiguration(sharedPhotoLibrary()) // Number of medias to select - configuration.selectionLimit = if (isMultipleMode) 0 else 1 + configuration.selectionLimit = when (mode) { + is PickerMode.Multiple -> mode.maxItems?.toLong() ?: 0 + PickerMode.Single -> 1 + } // Filter configuration configuration.filter = when (type) { diff --git a/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.js.kt b/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.js.kt index f7e9759..b9b612c 100644 --- a/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.js.kt +++ b/filekit-core/src/jsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.js.kt @@ -42,6 +42,8 @@ public actual object FileKit { // Set the multiple attribute multiple = mode is PickerMode.Multiple + + // max is not supported for file inputs } // Setup the change listener diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/FileKit.jvm.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/FileKit.jvm.kt index bc9a245..75991aa 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/FileKit.jvm.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/FileKit.jvm.kt @@ -25,14 +25,14 @@ public actual object FileKit { // Open native file picker val result = when (mode) { - PickerMode.Single -> PlatformFilePicker.current.pickFile( + is PickerMode.Single -> PlatformFilePicker.current.pickFile( title = title, initialDirectory = initialDirectory, fileExtensions = extensions, parentWindow = platformSettings?.parentWindow, )?.let { listOf(PlatformFile(it)) } - PickerMode.Multiple -> PlatformFilePicker.current.pickFiles( + is PickerMode.Multiple -> PlatformFilePicker.current.pickFiles( title = title, initialDirectory = initialDirectory, fileExtensions = extensions, diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/awt/AwtFilePicker.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/awt/AwtFilePicker.kt index b3baebb..5caa0ff 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/awt/AwtFilePicker.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/awt/AwtFilePicker.kt @@ -79,6 +79,8 @@ internal class AwtFilePicker : PlatformFilePicker { // Set multiple mode dialog.isMultipleMode = isMultipleMode + // MaxItems is not supported by FileDialog + // Set mime types dialog.filenameFilter = FilenameFilter { _, name -> fileExtensions?.any { name.endsWith(it) } ?: true diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt index 7cc9aa5..748d9d6 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/mac/MacOSFilePicker.kt @@ -7,150 +7,155 @@ import java.awt.Window import java.io.File internal class MacOSFilePicker : PlatformFilePicker { - override suspend fun pickFile( - initialDirectory: String?, - fileExtensions: List?, - title: String?, - parentWindow: Window?, - ): File? { - return callNativeMacOSPicker( - mode = MacOSFilePickerMode.SingleFile, - initialDirectory = initialDirectory, - fileExtensions = fileExtensions, - title = title - ) - } - - override suspend fun pickFiles( - initialDirectory: String?, - fileExtensions: List?, - title: String?, - parentWindow: Window?, - ): List? { - return callNativeMacOSPicker( - mode = MacOSFilePickerMode.MultipleFiles, - initialDirectory = initialDirectory, - fileExtensions = fileExtensions, - title = title - ) - } - - override suspend fun pickDirectory( - initialDirectory: String?, - title: String?, - parentWindow: Window?, - ): File? { - return callNativeMacOSPicker( - mode = MacOSFilePickerMode.Directories, - initialDirectory = initialDirectory, - fileExtensions = null, - title = title - ) - } - - private fun callNativeMacOSPicker( - mode: MacOSFilePickerMode, - initialDirectory: String?, - fileExtensions: List?, - title: String?, - ): T? { - val pool = Foundation.NSAutoreleasePool() - return try { - var response: T? = null - - Foundation.executeOnMainThread( - withAutoreleasePool = false, - waitUntilDone = true, - ) { - // Create the file picker - val openPanel = Foundation.invoke("NSOpenPanel", "new") - - // Setup single, multiple selection or directory mode - mode.setupPickerMode(openPanel) - - // Set the title - title?.let { - Foundation.invoke(openPanel, "setMessage:", Foundation.nsString(it)) - } - - // Set initial directory - initialDirectory?.let { - Foundation.invoke(openPanel, "setDirectoryURL:", Foundation.nsURL(it)) - } - - // Set file extensions - fileExtensions?.let { extensions -> - val items = extensions.map { Foundation.nsString(it) } - val nsData = Foundation.invokeVarArg("NSArray", "arrayWithObjects:", *items.toTypedArray()) - Foundation.invoke(openPanel, "setAllowedFileTypes:", nsData) - } - - // Open the file picker - val result = Foundation.invoke(openPanel, "runModal") - - // Get the path(s) from the file picker if the user validated the selection - if (result.toInt() == 1) { - response = mode.getResult(openPanel) - } - } - - response - } finally { - pool.drain() - } - } - - private companion object { - fun singlePath(openPanel: ID): File? { - val url = Foundation.invoke(openPanel, "URL") - val nsPath = Foundation.invoke(url, "path") - val path = Foundation.toStringViaUTF8(nsPath) - return path?.let { File(it) } - } - - fun multiplePaths(openPanel: ID): List? { - val urls = Foundation.invoke(openPanel, "URLs") - val urlCount = Foundation.invoke(urls, "count").toInt() - - return (0 until urlCount).mapNotNull { index -> - val url = Foundation.invoke(urls, "objectAtIndex:", index) - val nsPath = Foundation.invoke(url, "path") - val path = Foundation.toStringViaUTF8(nsPath) - path?.let { File(it) } - }.ifEmpty { null } - } - } - - private sealed class MacOSFilePickerMode { - abstract fun setupPickerMode(openPanel: ID) - abstract fun getResult(openPanel: ID): T? - - data object SingleFile : MacOSFilePickerMode() { - override fun setupPickerMode(openPanel: ID) { - Foundation.invoke(openPanel, "setCanChooseFiles:", true) - Foundation.invoke(openPanel, "setCanChooseDirectories:", false) - } - - override fun getResult(openPanel: ID): File? = singlePath(openPanel) - } - - data object MultipleFiles : MacOSFilePickerMode>() { - override fun setupPickerMode(openPanel: ID) { - Foundation.invoke(openPanel, "setCanChooseFiles:", true) - Foundation.invoke(openPanel, "setCanChooseDirectories:", false) - Foundation.invoke(openPanel, "setAllowsMultipleSelection:", true) - } - - override fun getResult(openPanel: ID): List? = multiplePaths(openPanel) - } - - data object Directories : MacOSFilePickerMode() { - override fun setupPickerMode(openPanel: ID) { - Foundation.invoke(openPanel, "setCanChooseFiles:", false) - Foundation.invoke(openPanel, "setCanChooseDirectories:", true) - } - - override fun getResult(openPanel: ID): File? = singlePath(openPanel) - } - } + override suspend fun pickFile( + initialDirectory: String?, + fileExtensions: List?, + title: String?, + parentWindow: Window?, + ): File? { + return callNativeMacOSPicker( + mode = MacOSFilePickerMode.SingleFile, + initialDirectory = initialDirectory, + fileExtensions = fileExtensions, + title = title + ) + } + + override suspend fun pickFiles( + initialDirectory: String?, + fileExtensions: List?, + title: String?, + parentWindow: Window?, + ): List? { + return callNativeMacOSPicker( + mode = MacOSFilePickerMode.MultipleFiles, + initialDirectory = initialDirectory, + fileExtensions = fileExtensions, + title = title + ) + } + + override suspend fun pickDirectory( + initialDirectory: String?, + title: String?, + parentWindow: Window?, + ): File? { + return callNativeMacOSPicker( + mode = MacOSFilePickerMode.Directories, + initialDirectory = initialDirectory, + fileExtensions = null, + title = title + ) + } + + private fun callNativeMacOSPicker( + mode: MacOSFilePickerMode, + initialDirectory: String?, + fileExtensions: List?, + title: String?, + ): T? { + val pool = Foundation.NSAutoreleasePool() + return try { + var response: T? = null + + Foundation.executeOnMainThread( + withAutoreleasePool = false, + waitUntilDone = true, + ) { + // Create the file picker + val openPanel = Foundation.invoke("NSOpenPanel", "new") + + // Setup single, multiple selection or directory mode + mode.setupPickerMode(openPanel) + + // Set the title + title?.let { + Foundation.invoke(openPanel, "setMessage:", Foundation.nsString(it)) + } + + // Set initial directory + initialDirectory?.let { + Foundation.invoke(openPanel, "setDirectoryURL:", Foundation.nsURL(it)) + } + + // Set file extensions + fileExtensions?.let { extensions -> + val items = extensions.map { Foundation.nsString(it) } + val nsData = Foundation.invokeVarArg( + "NSArray", + "arrayWithObjects:", + *items.toTypedArray(), + ) + Foundation.invoke(openPanel, "setAllowedFileTypes:", nsData) + } + + // Open the file picker + val result = Foundation.invoke(openPanel, "runModal") + + // Get the path(s) from the file picker if the user validated the selection + if (result.toInt() == 1) { + response = mode.getResult(openPanel) + } + } + + response + } finally { + pool.drain() + } + } + + private companion object { + fun singlePath(openPanel: ID): File? { + val url = Foundation.invoke(openPanel, "URL") + val nsPath = Foundation.invoke(url, "path") + val path = Foundation.toStringViaUTF8(nsPath) + return path?.let { File(it) } + } + + fun multiplePaths(openPanel: ID): List? { + val urls = Foundation.invoke(openPanel, "URLs") + val urlCount = Foundation.invoke(urls, "count").toInt() + + return (0 until urlCount).mapNotNull { index -> + val url = Foundation.invoke(urls, "objectAtIndex:", index) + val nsPath = Foundation.invoke(url, "path") + val path = Foundation.toStringViaUTF8(nsPath) + path?.let { File(it) } + }.ifEmpty { null } + } + } + + private sealed class MacOSFilePickerMode { + abstract fun setupPickerMode(openPanel: ID) + abstract fun getResult(openPanel: ID): T? + + data object SingleFile : MacOSFilePickerMode() { + override fun setupPickerMode(openPanel: ID) { + Foundation.invoke(openPanel, "setCanChooseFiles:", true) + Foundation.invoke(openPanel, "setCanChooseDirectories:", false) + } + + override fun getResult(openPanel: ID): File? = singlePath(openPanel) + } + + data object MultipleFiles : MacOSFilePickerMode>() { + override fun setupPickerMode(openPanel: ID) { + Foundation.invoke(openPanel, "setCanChooseFiles:", true) + Foundation.invoke(openPanel, "setCanChooseDirectories:", false) + Foundation.invoke(openPanel, "setAllowsMultipleSelection:", true) + // MaxItems is not supported by MacOSFilePicker + } + + override fun getResult(openPanel: ID): List? = multiplePaths(openPanel) + } + + data object Directories : MacOSFilePickerMode() { + override fun setupPickerMode(openPanel: ID) { + Foundation.invoke(openPanel, "setCanChooseFiles:", false) + Foundation.invoke(openPanel, "setCanChooseDirectories:", true) + } + + override fun getResult(openPanel: ID): File? = singlePath(openPanel) + } + } } diff --git a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt index 9feab92..e3f1dc6 100644 --- a/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt +++ b/filekit-core/src/jvmMain/kotlin/io/github/vinceglb/filekit/core/platform/windows/api/JnaFileChooser.kt @@ -29,268 +29,269 @@ import javax.swing.filechooser.FileNameExtensionFilter * @see JFileChooser, WindowsFileChooser, WindowsFileBrowser */ internal class JnaFileChooser() { - private enum class Action { - Open, Save - } - - /** - * the availabe selection modes of the dialog - */ - enum class Mode(val jFileChooserValue: Int) { - Files(JFileChooser.FILES_ONLY), - Directories(JFileChooser.DIRECTORIES_ONLY), - FilesAndDirectories(JFileChooser.FILES_AND_DIRECTORIES) - } - - var selectedFiles: Array - protected set - var currentDirectory: File? = null - protected set - protected var filters: ArrayList> = ArrayList() - - /** - * sets whether to enable multiselection - * - * @param enabled true to enable multiselection, false to disable it - */ - var isMultiSelectionEnabled: Boolean = false - - /** - * sets the selection mode - * - * @param mode the selection mode - */ - var mode: Mode = Mode.Files - - private var defaultFile: String = "" - private var dialogTitle: String = "" - private var openButtonText: String = "" - private var saveButtonText: String = "" - - /** - * creates a new file chooser with multiselection disabled and mode set - * to allow file selection only. - */ - init { - selectedFiles = arrayOf(null) - } - - /** - * creates a new file chooser with the specified initial directory - * - * @param currentDirectory the initial directory - */ - constructor(currentDirectory: File?) : this() { - if (currentDirectory != null) { - this.currentDirectory = - if (currentDirectory.isDirectory) currentDirectory else currentDirectory.parentFile - } - } - - /** - * creates a new file chooser with the specified initial directory - * - * @param currentDirectory the initial directory - */ - constructor(currentDirectoryPath: String?) : this( - if (currentDirectoryPath != null) File( - currentDirectoryPath - ) else null - ) - - /** - * shows a dialog for opening files - * - * @param parent the parent window - * - * @return true if the user clicked OK - */ - fun showOpenDialog(parent: Window?): Boolean { - return showDialog(parent, Action.Open) - } - - /** - * shows a dialog for saving files - * - * @param parent the parent window - * - * @return true if the user clicked OK - */ - fun showSaveDialog(parent: Window): Boolean { - return showDialog(parent, Action.Save) - } - - private fun showDialog(parent: Window?, action: Action): Boolean { - // native windows filechooser doesn't support mixed selection mode - if (Platform.isWindows() && mode != Mode.FilesAndDirectories) { - // windows filechooser can only multiselect files - if (isMultiSelectionEnabled && mode == Mode.Files) { - // TODO Here we would use the native windows dialog - // to choose multiple files. However I haven't been able - // to get it to work properly yet because it requires - // tricky callback magic and somehow this didn't work for me - // quite as documented (probably because I messed something up). - // Because I don't need this feature right now I've put it on - // hold to get on with stuff. - // Example code: http://support.microsoft.com/kb/131462/en-us - // GetOpenFileName: http://msdn.microsoft.com/en-us/library/ms646927.aspx - // OFNHookProc: http://msdn.microsoft.com/en-us/library/ms646931.aspx - // CDN_SELCHANGE: http://msdn.microsoft.com/en-us/library/ms646865.aspx - // SendMessage: http://msdn.microsoft.com/en-us/library/ms644950.aspx - } else if (!isMultiSelectionEnabled) { - if (mode == Mode.Files) { - return showWindowsFileChooser(parent, action) - } else if (mode == Mode.Directories) { - return showWindowsFolderBrowser(parent) - } - } - } - - // fallback to Swing - return showSwingFileChooser(parent, action) - } - - private fun showSwingFileChooser(parent: Window?, action: Action): Boolean { - val fc = JFileChooser(currentDirectory) - fc.isMultiSelectionEnabled = isMultiSelectionEnabled - fc.fileSelectionMode = mode.jFileChooserValue - - // set select file - if (!defaultFile.isEmpty() and (action == Action.Save)) { - val fsel = File(defaultFile) - fc.selectedFile = fsel - } - if (!dialogTitle.isEmpty()) { - fc.dialogTitle = dialogTitle - } - if ((action == Action.Open) and !openButtonText.isEmpty()) { - fc.approveButtonText = openButtonText - } else if ((action == Action.Save) and !saveButtonText.isEmpty()) { - fc.approveButtonText = saveButtonText - } - - // build filters - if (filters.size > 0) { - var useAcceptAllFilter = false - for (spec in filters) { - // the "All Files" filter is handled specially by JFileChooser - if (spec[1] == "*") { - useAcceptAllFilter = true - continue - } - fc.addChoosableFileFilter( - FileNameExtensionFilter( - spec[0], *Arrays.copyOfRange(spec, 1, spec.size) - ) - ) - } - fc.isAcceptAllFileFilterUsed = useAcceptAllFilter - } - - var result = -1 - result = if (action == Action.Open) { - fc.showOpenDialog(parent) - } else { - if (saveButtonText.isEmpty()) { - fc.showSaveDialog(parent) - } else { - fc.showDialog(parent, null) - } - } - if (result == JFileChooser.APPROVE_OPTION) { - selectedFiles = - if (isMultiSelectionEnabled) fc.selectedFiles else arrayOf(fc.selectedFile) - currentDirectory = fc.currentDirectory - return true - } - - return false - } - - private fun showWindowsFileChooser(parent: Window?, action: Action): Boolean { - val fc = WindowsFileChooser(currentDirectory) - fc.setFilters(filters) - - if (!defaultFile.isEmpty()) fc.setDefaultFilename(defaultFile) - - if (!dialogTitle.isEmpty()) { - fc.setTitle(dialogTitle) - } - - val result = fc.showDialog(parent, action == Action.Open) - if (result) { - selectedFiles = arrayOf(fc.selectedFile) - currentDirectory = fc.currentDirectory - } - return result - } - - private fun showWindowsFolderBrowser(parent: Window?): Boolean { - val fb = WindowsFolderBrowser() - if (!dialogTitle.isEmpty()) { - fb.setTitle(dialogTitle) - } - val file = fb.showDialog(parent) - if (file != null) { - selectedFiles = arrayOf(file) - currentDirectory = if (file.parentFile != null) file.parentFile else file - return true - } - - return false - } - - /** - * add a filter to the user-selectable list of file filters - * - * @param name name of the filter - * @param filter you must pass at least 1 argument, the arguments are the file - * extensions. - */ - fun addFilter(name: String, vararg filter: String) { - require(filter.isNotEmpty()) - val parts = ArrayList() - parts.add(name) - Collections.addAll(parts, *filter) - filters.add(parts.toTypedArray()) - } - - fun setCurrentDirectory(currentDirectoryPath: String?) { - this.currentDirectory = - (if (currentDirectoryPath != null) File(currentDirectoryPath) else null) - } - - fun setDefaultFileName(dfile: String) { - this.defaultFile = dfile - } - - /** - * set a title name - * - * @param Title of dialog - */ - fun setTitle(title: String) { - this.dialogTitle = title - } - - /** - * set a open button name - * - * @param open button text - */ - fun setOpenButtonText(buttonText: String) { - this.openButtonText = buttonText - } - - /** - * set a saveFile button name - * - * @param saveFile button text - */ - fun setSaveButtonText(buttonText: String) { - this.saveButtonText = buttonText - } - - val selectedFile: File? - get() = selectedFiles[0] + private enum class Action { + Open, Save + } + + /** + * the availabe selection modes of the dialog + */ + enum class Mode(val jFileChooserValue: Int) { + Files(JFileChooser.FILES_ONLY), + Directories(JFileChooser.DIRECTORIES_ONLY), + FilesAndDirectories(JFileChooser.FILES_AND_DIRECTORIES) + } + + var selectedFiles: Array + protected set + var currentDirectory: File? = null + protected set + protected var filters: ArrayList> = ArrayList() + + /** + * sets whether to enable multiselection + * + * @param enabled true to enable multiselection, false to disable it + */ + var isMultiSelectionEnabled: Boolean = false + + /** + * sets the selection mode + * + * @param mode the selection mode + */ + var mode: Mode = Mode.Files + + private var defaultFile: String = "" + private var dialogTitle: String = "" + private var openButtonText: String = "" + private var saveButtonText: String = "" + + /** + * creates a new file chooser with multiselection disabled and mode set + * to allow file selection only. + */ + init { + selectedFiles = arrayOf(null) + } + + /** + * creates a new file chooser with the specified initial directory + * + * @param currentDirectory the initial directory + */ + constructor(currentDirectory: File?) : this() { + if (currentDirectory != null) { + this.currentDirectory = + if (currentDirectory.isDirectory) currentDirectory else currentDirectory.parentFile + } + } + + /** + * creates a new file chooser with the specified initial directory + * + * @param currentDirectory the initial directory + */ + constructor(currentDirectoryPath: String?) : this( + if (currentDirectoryPath != null) File( + currentDirectoryPath + ) else null + ) + + /** + * shows a dialog for opening files + * + * @param parent the parent window + * + * @return true if the user clicked OK + */ + fun showOpenDialog(parent: Window?): Boolean { + return showDialog(parent, Action.Open) + } + + /** + * shows a dialog for saving files + * + * @param parent the parent window + * + * @return true if the user clicked OK + */ + fun showSaveDialog(parent: Window): Boolean { + return showDialog(parent, Action.Save) + } + + private fun showDialog(parent: Window?, action: Action): Boolean { + // native windows filechooser doesn't support mixed selection mode + if (Platform.isWindows() && mode != Mode.FilesAndDirectories) { + // windows filechooser can only multiselect files + if (isMultiSelectionEnabled && mode == Mode.Files) { + // TODO Here we would use the native windows dialog + // to choose multiple files. However I haven't been able + // to get it to work properly yet because it requires + // tricky callback magic and somehow this didn't work for me + // quite as documented (probably because I messed something up). + // Because I don't need this feature right now I've put it on + // hold to get on with stuff. + // Example code: http://support.microsoft.com/kb/131462/en-us + // GetOpenFileName: http://msdn.microsoft.com/en-us/library/ms646927.aspx + // OFNHookProc: http://msdn.microsoft.com/en-us/library/ms646931.aspx + // CDN_SELCHANGE: http://msdn.microsoft.com/en-us/library/ms646865.aspx + // SendMessage: http://msdn.microsoft.com/en-us/library/ms644950.aspx + } else if (!isMultiSelectionEnabled) { + if (mode == Mode.Files) { + return showWindowsFileChooser(parent, action) + } else if (mode == Mode.Directories) { + return showWindowsFolderBrowser(parent) + } + } + } + + // fallback to Swing + return showSwingFileChooser(parent, action) + } + + private fun showSwingFileChooser(parent: Window?, action: Action): Boolean { + val fc = JFileChooser(currentDirectory) + fc.isMultiSelectionEnabled = isMultiSelectionEnabled + fc.fileSelectionMode = mode.jFileChooserValue + + // MaxItems is not supported by JFileChooser + + // set select file + if (defaultFile.isNotEmpty() and (action == Action.Save)) { + val fsel = File(defaultFile) + fc.selectedFile = fsel + } + if (dialogTitle.isNotEmpty()) { + fc.dialogTitle = dialogTitle + } + if ((action == Action.Open) and openButtonText.isNotEmpty()) { + fc.approveButtonText = openButtonText + } else if ((action == Action.Save) and saveButtonText.isNotEmpty()) { + fc.approveButtonText = saveButtonText + } + + // build filters + if (filters.size > 0) { + var useAcceptAllFilter = false + for (spec in filters) { + // the "All Files" filter is handled specially by JFileChooser + if (spec[1] == "*") { + useAcceptAllFilter = true + continue + } + fc.addChoosableFileFilter( + FileNameExtensionFilter( + spec[0], *Arrays.copyOfRange(spec, 1, spec.size) + ) + ) + } + fc.isAcceptAllFileFilterUsed = useAcceptAllFilter + } + + val result = if (action == Action.Open) { + fc.showOpenDialog(parent) + } else { + if (saveButtonText.isEmpty()) { + fc.showSaveDialog(parent) + } else { + fc.showDialog(parent, null) + } + } + if (result == JFileChooser.APPROVE_OPTION) { + selectedFiles = + if (isMultiSelectionEnabled) fc.selectedFiles else arrayOf(fc.selectedFile) + currentDirectory = fc.currentDirectory + return true + } + + return false + } + + private fun showWindowsFileChooser(parent: Window?, action: Action): Boolean { + val fc = WindowsFileChooser(currentDirectory) + fc.setFilters(filters) + + if (!defaultFile.isEmpty()) fc.setDefaultFilename(defaultFile) + + if (!dialogTitle.isEmpty()) { + fc.setTitle(dialogTitle) + } + + val result = fc.showDialog(parent, action == Action.Open) + if (result) { + selectedFiles = arrayOf(fc.selectedFile) + currentDirectory = fc.currentDirectory + } + return result + } + + private fun showWindowsFolderBrowser(parent: Window?): Boolean { + val fb = WindowsFolderBrowser() + if (!dialogTitle.isEmpty()) { + fb.setTitle(dialogTitle) + } + val file = fb.showDialog(parent) + if (file != null) { + selectedFiles = arrayOf(file) + currentDirectory = if (file.parentFile != null) file.parentFile else file + return true + } + + return false + } + + /** + * add a filter to the user-selectable list of file filters + * + * @param name name of the filter + * @param filter you must pass at least 1 argument, the arguments are the file + * extensions. + */ + fun addFilter(name: String, vararg filter: String) { + require(filter.isNotEmpty()) + val parts = ArrayList() + parts.add(name) + Collections.addAll(parts, *filter) + filters.add(parts.toTypedArray()) + } + + fun setCurrentDirectory(currentDirectoryPath: String?) { + this.currentDirectory = + (if (currentDirectoryPath != null) File(currentDirectoryPath) else null) + } + + fun setDefaultFileName(dfile: String) { + this.defaultFile = dfile + } + + /** + * set a title name + * + * @param Title of dialog + */ + fun setTitle(title: String) { + this.dialogTitle = title + } + + /** + * set a open button name + * + * @param open button text + */ + fun setOpenButtonText(buttonText: String) { + this.openButtonText = buttonText + } + + /** + * set a saveFile button name + * + * @param saveFile button text + */ + fun setSaveButtonText(buttonText: String) { + this.saveButtonText = buttonText + } + + val selectedFile: File? + get() = selectedFiles[0] } diff --git a/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.wasmJs.kt b/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.wasmJs.kt index 09711fb..7a2a31b 100644 --- a/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.wasmJs.kt +++ b/filekit-core/src/wasmJsMain/kotlin/io/github/vinceglb/filekit/core/FileKit.wasmJs.kt @@ -42,6 +42,8 @@ public actual object FileKit { // Set the multiple attribute multiple = mode is PickerMode.Multiple + + // max is not supported for file inputs } // Setup the change listener diff --git a/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt b/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt index 538b56a..9c6ba4c 100644 --- a/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt +++ b/samples/sample-compose/composeApp/src/commonMain/kotlin/App.kt @@ -54,7 +54,7 @@ private fun SampleApp() { val multipleFilesPicker = rememberFilePickerLauncher( type = PickerType.Image, - mode = PickerMode.Multiple, + mode = PickerMode.Multiple(maxItems = 4), title = "Multiple files picker", initialDirectory = directory?.path, onResult = { file -> file?.let { files += it } } @@ -69,7 +69,7 @@ private fun SampleApp() { val filesPicker = rememberFilePickerLauncher( type = PickerType.File(listOf("png")), - mode = PickerMode.Multiple, + mode = PickerMode.Multiple(), title = "Multiple files picker, only png", initialDirectory = directory?.path, onResult = { file -> file?.let { files += it } } diff --git a/samples/sample-core/appleApps/PickerKotlinSampleCore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/samples/sample-core/appleApps/PickerKotlinSampleCore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index fa3bd43..0000000 --- a/samples/sample-core/appleApps/PickerKotlinSampleCore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,24 +0,0 @@ -{ - "originHash" : "3eff510532cbd886b428b7d14e1f4ed286820714fa9ef4fbe832fd481f0742d1", - "pins" : [ - { - "identity" : "kmm-viewmodel", - "kind" : "remoteSourceControl", - "location" : "https://github.com/rickclephas/KMM-ViewModel.git", - "state" : { - "revision" : "677d657ed678fadf50efac37b5d177170d6873a8", - "version" : "1.0.0-ALPHA-20" - } - }, - { - "identity" : "kmp-observableviewmodel", - "kind" : "remoteSourceControl", - "location" : "https://github.com/rickclephas/KMP-ObservableViewModel.git", - "state" : { - "branch" : "master", - "revision" : "a94525baa10dfb5870d16b9a7155e0854deb705c" - } - } - ], - "version" : 3 -} diff --git a/samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt b/samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt index cc1b0d8..0bd807e 100644 --- a/samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt +++ b/samples/sample-core/shared/src/commonMain/kotlin/io/github/vinceglb/sample/core/MainViewModel.kt @@ -38,7 +38,7 @@ class MainViewModel : ViewModel() { // Pick files val files = FileKit.pickFile( type = PickerType.Image, - mode = PickerMode.Multiple + mode = PickerMode.Multiple() ) // Add files to the state @@ -66,7 +66,7 @@ class MainViewModel : ViewModel() { // Pick files val files = FileKit.pickFile( type = PickerType.File(extensions = listOf("png")), - mode = PickerMode.Multiple + mode = PickerMode.Multiple() ) // Add files to the state