ChromeWindowState.kt 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. package org.mjdev.desktop.windows
  2. import androidx.compose.runtime.Composable
  3. import androidx.compose.runtime.MutableState
  4. import androidx.compose.runtime.mutableStateListOf
  5. import androidx.compose.runtime.remember
  6. import androidx.compose.runtime.rememberCoroutineScope
  7. import androidx.compose.ui.unit.Dp
  8. import androidx.compose.ui.unit.DpOffset
  9. import androidx.compose.ui.unit.DpSize
  10. import androidx.compose.ui.window.WindowPosition
  11. import kotlinx.coroutines.CoroutineScope
  12. import kotlinx.coroutines.Dispatchers
  13. import kotlinx.coroutines.launch
  14. import org.mjdev.desktop.components.sliding.base.VisibilityState
  15. import org.mjdev.desktop.extensions.LaunchedEffect.LaunchedEffect
  16. import org.mjdev.desktop.extensions.MutableStateExt.rememberState
  17. import org.mjdev.desktop.log.Log
  18. import java.awt.Window
  19. @Suppress("RedundantSuspendModifier", "MemberVisibilityCanBePrivate")
  20. open class ChromeWindowState(
  21. position: DpOffset = DpOffset.Zero,
  22. size: DpSize = DpSize.Zero,
  23. closeAction: WindowCloseAction,
  24. visible: Boolean = false,
  25. enabled: Boolean = true,
  26. hideDelay: Long = 0L,
  27. scope: CoroutineScope = CoroutineScope(Dispatchers.Main),
  28. ) : VisibilityState(
  29. visible = visible,
  30. enabled = enabled,
  31. hideDelay = hideDelay,
  32. scope = scope,
  33. ),
  34. WindowState {
  35. private var isCreated = false
  36. private var onFocusChange: MutableList<ChromeWindowState.(focus: Boolean) -> Unit> =
  37. mutableStateListOf()
  38. private var onOpened: MutableList<ChromeWindowState.() -> Unit> =
  39. mutableStateListOf()
  40. private var onClosed: MutableList<ChromeWindowState.() -> Unit> =
  41. mutableStateListOf()
  42. var window: Window? = null
  43. get() =
  44. field ?: run {
  45. Log.e("Window is empty, implementation error.")
  46. null
  47. }
  48. set(value) {
  49. if (value != null) {
  50. field = value
  51. isCreated = true
  52. } else {
  53. Log.e("Attempt to set window value to null.")
  54. }
  55. }
  56. val focusHelper: WindowFocusListener =
  57. WindowFocusListener { _, focus ->
  58. this@ChromeWindowState.onFocusChange.forEach { listener ->
  59. listener.invoke(this@ChromeWindowState, focus)
  60. }
  61. }
  62. val stateHelper: WindowStateListener =
  63. WindowStateListener({
  64. this@ChromeWindowState.onOpened.forEach { state ->
  65. state.invoke(this@ChromeWindowState)
  66. }
  67. }, {
  68. this@ChromeWindowState.onClosed.forEach { state ->
  69. state.invoke(this@ChromeWindowState)
  70. }
  71. })
  72. @Suppress("CanBePrimaryConstructorProperty")
  73. val closeAction: WindowCloseAction = closeAction
  74. val isFocused
  75. get() = window?.isFocused ?: false
  76. override var position: DpOffset = position
  77. set(value) {
  78. Log.d("ChromeWindow position: $field -> $value")
  79. field = value
  80. if (isCreated) {
  81. scope.launch {
  82. setPosition(value)
  83. }
  84. }
  85. }
  86. override var size: DpSize = size
  87. set(value) {
  88. Log.d("ChromeWindow size: $field -> $value")
  89. val oldSize = field
  90. field = value
  91. if (isCreated) {
  92. scope.launch {
  93. moveBy(
  94. value.width - oldSize.width,
  95. value.height - oldSize.height,
  96. )
  97. setSize(value)
  98. }
  99. }
  100. }
  101. override var isMinimized: Boolean = false
  102. val x: Dp
  103. get() = position.x
  104. val y: Dp
  105. get() = position.y
  106. /** name + intended position/size + actual AWT bounds, for logging the window geometry */
  107. private fun geom(): String {
  108. val w = if (isCreated) window else null
  109. val bounds = w?.let { " bounds=[${it.x},${it.y} ${it.width}x${it.height}]" } ?: ""
  110. return "name=${w?.name ?: "-"} pos=$position size=$size$bounds"
  111. }
  112. override suspend fun show() {
  113. Log.d("ChromeWindow show: ${geom()}")
  114. super.show()
  115. }
  116. override suspend fun hide(force: Boolean) {
  117. Log.d("ChromeWindow hide(force=$force): ${geom()}")
  118. super.hide(force)
  119. }
  120. fun onOpened(block: ChromeWindowState.() -> Unit) {
  121. onOpened.add(block)
  122. }
  123. fun onClosed(block: ChromeWindowState.() -> Unit) {
  124. onClosed.add(block)
  125. }
  126. fun onFocusChange(block: ChromeWindowState.(Boolean) -> Unit) {
  127. onFocusChange.add(block)
  128. }
  129. suspend fun moveBy(
  130. x: Dp,
  131. y: Dp,
  132. ) = runCatching {
  133. Log.d("ChromeWindow moveBy ($x, $y): ${geom()}")
  134. setPosition(
  135. DpOffset(
  136. position.x - x,
  137. position.y - y,
  138. ),
  139. )
  140. }.onFailure { e -> Log.e(e) }
  141. suspend fun setSize(size: DpSize) =
  142. runCatching {
  143. Log.d("ChromeWindow setSize -> $size: ${geom()}")
  144. window?.setSizeSafely(size, WindowPlacement.Floating)
  145. }.onFailure { e -> Log.e(e) }
  146. suspend fun setPosition(position: DpOffset) =
  147. runCatching {
  148. Log.d("ChromeWindow setPosition -> $position: ${geom()}")
  149. window?.setPosition(
  150. WindowPosition.Absolute(position.x, position.y),
  151. WindowPlacement.Floating,
  152. )
  153. }.onFailure { e -> Log.e(e) }
  154. override suspend fun focus() {
  155. runCatching {
  156. Log.d("ChromeWindow focus: ${geom()}")
  157. window?.toFront()
  158. if (!isFocused) {
  159. window?.requestFocus()
  160. }
  161. }.onFailure { e -> Log.e(e) }
  162. }
  163. suspend fun showOrFocus() {
  164. if (isNotVisible) {
  165. show()
  166. } else {
  167. focus()
  168. }
  169. }
  170. companion object {
  171. @Composable
  172. fun rememberChromeWindowState(
  173. position: DpOffset = DpOffset.Unspecified,
  174. size: DpSize = DpSize.Zero,
  175. placement: WindowPlacement = WindowPlacement.Floating,
  176. closeAction: WindowCloseAction = WindowCloseAction.CLOSE,
  177. visible: Boolean = false,
  178. enabled: Boolean = true,
  179. hideDelay: Long = 0L,
  180. scope: CoroutineScope = rememberCoroutineScope(),
  181. ) = remember(
  182. position,
  183. size,
  184. placement,
  185. closeAction,
  186. visible,
  187. enabled,
  188. hideDelay,
  189. scope,
  190. ) {
  191. ChromeWindowState(
  192. position,
  193. size,
  194. closeAction,
  195. visible,
  196. enabled,
  197. hideDelay,
  198. scope,
  199. )
  200. }
  201. @Composable
  202. fun rememberWnState() = rememberState(false)
  203. @Composable
  204. fun updateWindowState(
  205. visible: Boolean,
  206. wnState: MutableState<Boolean>,
  207. state: ChromeWindowState,
  208. ) = LaunchedEffect {
  209. if (wnState.value == wnState.value && !visible) {
  210. if (state.closeAction == WindowCloseAction.CLOSE) {
  211. wnState.value = false
  212. }
  213. }
  214. }
  215. }
  216. }