-
Notifications
You must be signed in to change notification settings - Fork 76
✨ Add unified JVM dialog parent #624
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| package io.github.vinceglb.filekit.dialogs | ||
|
|
||
| import java.awt.Window | ||
|
|
||
| /** | ||
| * The JVM window or platform handle which owns a FileKit dialog. | ||
| * | ||
| * Use a factory matching the integration which owns the window. Native handles are | ||
| * deliberately typed by platform: a Wayland token is not a pointer and an X11 XID | ||
| * is not a Windows HWND. | ||
| */ | ||
| public sealed class FileKitDialogParent private constructor() { | ||
| internal class Awt( | ||
| val window: Window, | ||
| ) : FileKitDialogParent() | ||
|
|
||
| internal class Windows( | ||
| val hwnd: Long, | ||
| ) : FileKitDialogParent() | ||
|
|
||
| internal class X11( | ||
| val identifier: String, | ||
| ) : FileKitDialogParent() | ||
|
|
||
| internal class Wayland( | ||
| val identifier: String, | ||
| ) : FileKitDialogParent() | ||
|
|
||
| public companion object { | ||
| private val POINTER_SHAPED_TOKEN: Regex = Regex("[+-]?(?:0[xX][0-9a-fA-F]+|[0-9]+)") | ||
|
|
||
| /** Uses an AWT window as the dialog parent. */ | ||
| public fun awt(window: Window): FileKitDialogParent = Awt(window) | ||
|
|
||
| /** | ||
| * Uses a non-zero Windows HWND as the dialog parent. | ||
| * | ||
| * Negative values are accepted because they can be valid pointer bit patterns. | ||
| */ | ||
| public fun windows(hwnd: Long): FileKitDialogParent { | ||
| require(hwnd != 0L) { "A Windows HWND must not be zero." } | ||
| return Windows(hwnd) | ||
| } | ||
|
|
||
| /** Uses a non-zero 32-bit X11 XID as the dialog parent. */ | ||
| public fun x11(xid: Long): FileKitDialogParent = X11(x11Identifier(xid)) | ||
|
|
||
| /** | ||
| * Uses an opaque xdg-portal/xdg-foreign Wayland parent token. | ||
| * | ||
| * The token must come from the caller's Wayland integration. FileKit never | ||
| * accepts or dereferences a raw `wl_surface*` pointer. | ||
| */ | ||
| public fun wayland(portalToken: String): FileKitDialogParent { | ||
| require(portalToken.isNotBlank()) { "A Wayland portal token must not be blank." } | ||
| require(portalToken.none(Char::isWhitespace)) { | ||
| "A Wayland portal token must not contain whitespace." | ||
| } | ||
| require(!portalToken.startsWith("wayland:")) { | ||
| "Pass the opaque Wayland portal token without the wayland: prefix." | ||
| } | ||
| require(!portalToken.matches(POINTER_SHAPED_TOKEN)) { | ||
| "A Wayland portal token must not be a raw pointer-shaped value." | ||
| } | ||
| return Wayland("wayland:$portalToken") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| internal fun FileKitDialogParent?.awtWindowOrNull(): Window? = when (this) { | ||
| null -> null | ||
| is FileKitDialogParent.Awt -> window | ||
| else -> null | ||
| } | ||
|
|
||
| internal fun FileKitDialogParent?.requireAwtWindowOrNull(adapter: String): Window? = when (this) { | ||
| null -> null | ||
| is FileKitDialogParent.Awt -> window | ||
| else -> throw IllegalArgumentException("$adapter only supports an AWT dialog parent.") | ||
| } | ||
|
|
||
| internal fun FileKitDialogParent?.resolveWindowsHandle( | ||
| awtWindowHandle: (Window) -> Long, | ||
| ): Long? = when (this) { | ||
| null -> null | ||
| is FileKitDialogParent.Awt -> awtWindowHandle(window).requireNonZeroHandle("An AWT window") | ||
| is FileKitDialogParent.Windows -> hwnd | ||
| else -> throw IllegalArgumentException("Windows dialogs only support AWT or Windows dialog parents.") | ||
| } | ||
|
|
||
| internal fun FileKitDialogParent?.resolveXdgPortalParent( | ||
| awtWindowXid: (Window) -> Long, | ||
| ): String = when (this) { | ||
| null -> "" | ||
| is FileKitDialogParent.Awt -> x11Identifier(awtWindowXid(window)) | ||
| is FileKitDialogParent.X11 -> identifier | ||
| is FileKitDialogParent.Wayland -> identifier | ||
| else -> throw IllegalArgumentException("XDG portal dialogs only support AWT, X11, or Wayland dialog parents.") | ||
| } | ||
|
|
||
| private fun x11Identifier(xid: Long): String { | ||
| require(xid in 1..X11_XID_MAX) { "An X11 XID must be between 1 and 0xffffffff." } | ||
| return "x11:${xid.toString(16)}" | ||
| } | ||
|
|
||
| private fun Long.requireNonZeroHandle(label: String): Long { | ||
| require(this != 0L) { "$label must resolve to a non-zero native handle." } | ||
| return this | ||
| } | ||
|
|
||
| private const val X11_XID_MAX: Long = 0xffff_ffffL | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,14 +6,90 @@ import java.awt.Window | |
| * JVM implementation of [FileKitDialogSettings]. | ||
| * | ||
| * @property title The title of the dialog. | ||
| * @property parentWindow The parent window for the dialog. | ||
| * @property parent The parent for the dialog, when one is available. | ||
| * @property macOS Specific settings for macOS when running on JVM. | ||
| */ | ||
| public actual data class FileKitDialogSettings( | ||
| public val title: String? = null, | ||
| public val parentWindow: Window? = null, | ||
| public val parent: FileKitDialogParent? = null, | ||
| public val macOS: FileKitMacOSSettings = FileKitMacOSSettings(), | ||
| ) { | ||
| /** | ||
| * Compatibility adapter for the former AWT-only setting. | ||
| * | ||
| * This constructor normalizes immediately into [parent], so an instance never | ||
| * stores competing AWT and native parents. It intentionally requires | ||
| * [parentWindow] to keep `FileKitDialogSettings()` unambiguous. | ||
| */ | ||
| @Deprecated( | ||
| message = "Use parent = parentWindow?.let(FileKitDialogParent::awt) instead.", | ||
| replaceWith = ReplaceWith( | ||
| "FileKitDialogSettings(title = title, parent = parentWindow?.let(FileKitDialogParent::awt), macOS = macOS)", | ||
| ), | ||
| ) | ||
| public constructor( | ||
| title: String? = null, | ||
| parentWindow: Window?, | ||
| macOS: FileKitMacOSSettings = FileKitMacOSSettings(), | ||
|
Comment on lines
+30
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Existing source such as Useful? React with 👍 / 👎. |
||
| ) : this( | ||
| title = title, | ||
| parent = parentWindow?.let(FileKitDialogParent::awt), | ||
| macOS = macOS, | ||
| ) | ||
|
|
||
| /** | ||
| * Compatibility adapter for the former positional AWT constructor call. | ||
| * | ||
| * The parameter is intentionally named [window] so existing named | ||
| * `parentWindow = ...` calls continue to select the adapter above. | ||
| */ | ||
| @Deprecated( | ||
| message = "Use parent = FileKitDialogParent.awt(window) instead.", | ||
| replaceWith = ReplaceWith("FileKitDialogSettings(parent = FileKitDialogParent.awt(window))"), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| ) | ||
| public constructor( | ||
| window: Window?, | ||
| macOS: FileKitMacOSSettings = FileKitMacOSSettings(), | ||
| ) : this( | ||
| parent = window?.let(FileKitDialogParent::awt), | ||
| macOS = macOS, | ||
| ) | ||
|
|
||
| /** | ||
| * The AWT window represented by [parent], if any. | ||
| * | ||
| * Native [FileKitDialogParent] values return `null`; callers should migrate to | ||
| * [parent] rather than treating that result as an unparented dialog. | ||
| */ | ||
| @Deprecated( | ||
| message = "Use parent instead.", | ||
| replaceWith = ReplaceWith("parent"), | ||
| ) | ||
| public val parentWindow: Window? | ||
| get() = parent.awtWindowOrNull() | ||
|
|
||
| /** | ||
| * Compatibility adapter for copying a former AWT-only setting. | ||
| * | ||
| * A canonical and a legacy parent cannot be supplied in the same call: the | ||
| * overloads deliberately expose either `parent` or `parentWindow`, never both. | ||
| */ | ||
| @Deprecated( | ||
| message = "Use copy(parent = parentWindow?.let(FileKitDialogParent::awt)) instead.", | ||
| replaceWith = ReplaceWith( | ||
| "copy(title = title, parent = parentWindow?.let(FileKitDialogParent::awt), macOS = macOS)", | ||
| ), | ||
| ) | ||
| public fun copy( | ||
| title: String? = this.title, | ||
| parentWindow: Window?, | ||
| macOS: FileKitMacOSSettings = this.macOS, | ||
| ): FileKitDialogSettings = FileKitDialogSettings( | ||
| title = title, | ||
| parent = parentWindow?.let(FileKitDialogParent::awt), | ||
| macOS = macOS, | ||
| ) | ||
|
|
||
| public actual companion object { | ||
| /** | ||
| * Creates a default instance of [FileKitDialogSettings]. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because
Awtuses identity equality, two settings independently created with the same window—such asFileKitDialogSettings(parentWindow = window)—now compare unequal, whereas the previous data class compared the storedWindowreferences and considered them equal. This breaks the value semantics ofFileKitDialogSettingsfor equality checks, sets, and caches; the wrapper should compare its contained window rather than its wrapper allocation.Useful? React with 👍 / 👎.