Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ captures
!*.xcworkspace/contents.xcworkspacedata
**/xcshareddata/WorkspaceSettings.xcsettings
node_modules/
.scratch/

#So we don't accidentally commit our private keys
*.gpg
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.github.vinceglb.filekit.dialogs.platform.windows

import com.sun.jna.platform.win32.Ole32
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors

internal interface WindowsComRuntime {
fun initializeSta(): Int

fun uninitialize()
}

internal object JnaWindowsComRuntime : WindowsComRuntime {
override fun initializeSta(): Int = Ole32.INSTANCE
.CoInitializeEx(
null,
Ole32.COINIT_APARTMENTTHREADED or Ole32.COINIT_DISABLE_OLE1DDE,
).toInt()

override fun uninitialize() {
Ole32.INSTANCE.CoUninitialize()
}
}

internal class WindowsDialogExecutor(
private val comRuntime: WindowsComRuntime,
) : AutoCloseable {
private val dispatcher = Executors
.newSingleThreadExecutor { runnable ->
Thread(runnable, THREAD_NAME).apply {
isDaemon = true
}
}.asCoroutineDispatcher()

suspend fun <T> execute(block: () -> T): T = withContext(dispatcher) {
val initializationResult = comRuntime.initializeSta()
if (initializationResult != S_OK && initializationResult != S_FALSE) {
throw RuntimeException(
"CoInitializeEx failed with HRESULT 0x${initializationResult.toUInt().toString(16)}",
)
}

try {
block()
} finally {
comRuntime.uninitialize()
}
}

override fun close() {
dispatcher.close()
}

private companion object {
const val THREAD_NAME = "FileKit-Windows-Dialog"
const val S_OK = 0
const val S_FALSE = 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package io.github.vinceglb.filekit.dialogs.platform.windows

import com.sun.jna.Native
import com.sun.jna.WString
import com.sun.jna.platform.win32.COM.COMUtils
import com.sun.jna.platform.win32.COM.COMUtils.FAILED
import com.sun.jna.platform.win32.Guid
import com.sun.jna.platform.win32.Ole32
import com.sun.jna.platform.win32.Ole32.COINIT_APARTMENTTHREADED
import com.sun.jna.platform.win32.WTypes
import com.sun.jna.platform.win32.Win32Exception
import com.sun.jna.platform.win32.WinDef
Expand Down Expand Up @@ -35,12 +33,12 @@ import io.github.vinceglb.filekit.dialogs.platform.windows.jna.ShellItem
import io.github.vinceglb.filekit.dialogs.platform.windows.jna.ShellItemArray
import io.github.vinceglb.filekit.dialogs.platform.windows.util.GuidFixed
import io.github.vinceglb.filekit.path
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.awt.Window
import java.io.File

internal class WindowsFilePicker : PlatformFilePicker {
internal class WindowsFilePicker(
private val dialogExecutor: WindowsDialogExecutor = WindowsDialogExecutor(JnaWindowsComRuntime),
) : PlatformFilePicker {
override suspend fun openFilePicker(
fileExtensions: Set<String>?,
directory: PlatformFile?,
Expand Down Expand Up @@ -152,12 +150,9 @@ internal class WindowsFilePicker : PlatformFilePicker {
private suspend fun <FD : FileDialog, T> useFileDialog(
type: FileDialogType<FD>,
block: (FD) -> T,
): T = withContext(Dispatchers.IO) {
): T = dialogExecutor.execute {
var fileDialog: FD? = null
try {
// Initialize COM
initCom()

// Create FileOpenDialog
val pbrFileDialog = PointerByReference()
fileDialog = Ole32.INSTANCE
Expand All @@ -174,7 +169,6 @@ internal class WindowsFilePicker : PlatformFilePicker {
block(fileDialog)
} finally {
fileDialog?.Release()
Ole32.INSTANCE.CoUninitialize()
}
}

Expand All @@ -199,19 +193,6 @@ internal class WindowsFilePicker : PlatformFilePicker {
}
}

private fun initCom() {
Ole32.INSTANCE
.CoInitializeEx(
null,
COINIT_APARTMENTTHREADED or Ole32.COINIT_DISABLE_OLE1DDE,
).verify("CoInitializeEx failed")

val isInit = COMUtils.comIsInitialized()
if (!isInit) {
throw RuntimeException("COM initialization failed")
}
}

private fun FileDialog.setDefaultPath(defaultPath: PlatformFile) {
val pbrFolder = PointerByReference()
val resultFolder = Shell32.INSTANCE.SHCreateItemFromParsingName(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.github.vinceglb.filekit.dialogs.platform.windows

import com.sun.jna.platform.win32.Ole32
import com.sun.jna.platform.win32.WTypes
import com.sun.jna.ptr.PointerByReference
import io.github.vinceglb.filekit.dialogs.platform.windows.jna.FileOpenDialog
import io.github.vinceglb.filekit.dialogs.platform.windows.jna.IFileOpenDialog
import io.github.vinceglb.filekit.utils.Platform
import io.github.vinceglb.filekit.utils.PlatformUtil
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.util.concurrent.Executors
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull

@Suppress("ktlint:standard:function-naming", "FunctionName")
class WindowsDialogExecutorIntegrationTest {
@Test
fun WindowsDialogExecutor_executeFromMtaCaller_createsFileDialogOnOwnedStaThread() = runBlocking {
if (PlatformUtil.current != Platform.Windows) return@runBlocking

val callerDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
try {
withContext(callerDispatcher) {
val callerThread = Thread.currentThread()
val callerInitialization = Ole32.INSTANCE
.CoInitializeEx(null, Ole32.COINIT_MULTITHREADED)
.toInt()
assertEquals(S_OK, callerInitialization)

try {
val dialogExecutor = WindowsDialogExecutor(JnaWindowsComRuntime)
try {
val dialogThread = dialogExecutor.execute {
val dialogPointer = PointerByReference()
val creationResult = Ole32.INSTANCE
.CoCreateInstance(
IFileOpenDialog.CLSID_FILEOPENDIALOG,
null,
WTypes.CLSCTX_ALL,
IFileOpenDialog.IID_IFILEOPENDIALOG,
dialogPointer,
).toInt()

assertEquals(S_OK, creationResult)
val pointer = assertNotNull(dialogPointer.value)
val fileDialog = FileOpenDialog(pointer)
try {
Thread.currentThread()
} finally {
fileDialog.Release()
}
}

assertNotEquals(callerThread, dialogThread)
assertEquals("FileKit-Windows-Dialog", dialogThread.name)
} finally {
dialogExecutor.close()
}
} finally {
Ole32.INSTANCE.CoUninitialize()
}
}
} finally {
callerDispatcher.close()
}
}

private companion object {
const val S_OK = 0
}
}
Loading