@legendapp/spark 0.0.1-next.2 → 0.0.1-next.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (399) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -4
  3. package/ai-codex.ts +1 -0
  4. package/ai.ts +1 -0
  5. package/app-documents.ts +1 -0
  6. package/config-plugin.d.cts +2 -0
  7. package/contracts.ts +2 -0
  8. package/diagnostics.ts +1 -0
  9. package/expo-config.d.cts +1 -0
  10. package/metro.cjs +4 -1
  11. package/metro.d.cts +12 -0
  12. package/native.cjs +3 -1
  13. package/native.d.cts +6 -0
  14. package/package.json +82 -14
  15. package/schema.json +276 -58
  16. package/settings-observable.ts +1 -0
  17. package/settings-window.ts +1 -0
  18. package/shortcuts-commands.ts +1 -0
  19. package/shortcuts-keyboard.ts +3 -0
  20. package/ui-classnames.ts +1 -0
  21. package/ui-glass.ts +1 -0
  22. package/ui-search.ts +1 -0
  23. package/ui-sidebar.ts +1 -0
  24. package/ui-split-view.ts +1 -0
  25. package/ui-symbol.ts +1 -0
  26. package/vendor/node_modules/@legendapp/spark-ai/package.json +19 -0
  27. package/vendor/node_modules/@legendapp/spark-ai/src/availability.ts +17 -0
  28. package/vendor/node_modules/@legendapp/spark-ai/src/index.ts +15 -0
  29. package/vendor/node_modules/@legendapp/spark-ai/src/invocations.ts +34 -0
  30. package/vendor/node_modules/@legendapp/spark-ai/src/output.ts +62 -0
  31. package/vendor/node_modules/@legendapp/spark-ai/src/run.ts +12 -0
  32. package/vendor/node_modules/@legendapp/spark-ai/src/types.ts +16 -0
  33. package/vendor/node_modules/@legendapp/spark-audio/macos/RNSparkAudio.h +2 -1
  34. package/vendor/node_modules/@legendapp/spark-audio/macos/RNSparkAudio.mm +126 -24
  35. package/vendor/node_modules/@legendapp/spark-audio/package.json +5 -2
  36. package/vendor/node_modules/@legendapp/spark-audio/src/NativeSparkAudio.ts +5 -1
  37. package/vendor/node_modules/@legendapp/spark-audio/src/desktop.ts +36 -32
  38. package/vendor/node_modules/@legendapp/spark-audio/src/events.ts +11 -0
  39. package/vendor/node_modules/@legendapp/spark-audio/src/hooks.ts +65 -0
  40. package/vendor/node_modules/@legendapp/spark-audio/src/index.web.ts +92 -33
  41. package/vendor/node_modules/@legendapp/spark-audio/src/media-session-unavailable.ts +6 -4
  42. package/vendor/node_modules/@legendapp/spark-audio/src/media-session.ts +52 -19
  43. package/vendor/node_modules/@legendapp/spark-audio/src/media-session.web.ts +18 -10
  44. package/vendor/node_modules/@legendapp/spark-audio/src/media-types.ts +40 -13
  45. package/vendor/node_modules/@legendapp/spark-audio/src/mobile.ts +40 -32
  46. package/vendor/node_modules/@legendapp/spark-audio/src/player.ts +97 -0
  47. package/vendor/node_modules/@legendapp/spark-audio/src/status-listeners.ts +21 -15
  48. package/vendor/node_modules/@legendapp/spark-audio/src/types.ts +9 -4
  49. package/vendor/node_modules/@legendapp/spark-audio/windows/SparkAudio/SparkAudio.h +67 -20
  50. package/vendor/node_modules/@legendapp/spark-auth-session/package.json +4 -3
  51. package/vendor/node_modules/@legendapp/spark-auth-session/src/core.ts +60 -42
  52. package/vendor/node_modules/@legendapp/spark-auth-session/src/index.ts +28 -12
  53. package/vendor/node_modules/@legendapp/spark-auth-session/src/mobile.ts +97 -41
  54. package/vendor/node_modules/@legendapp/spark-auth-session/src/types.ts +20 -9
  55. package/vendor/node_modules/@legendapp/spark-cli/dist/add-desktop.js +50 -7
  56. package/vendor/node_modules/@legendapp/spark-cli/dist/build-mode.js +1 -1
  57. package/vendor/node_modules/@legendapp/spark-cli/dist/build.js +83 -48
  58. package/vendor/node_modules/@legendapp/spark-cli/dist/cli-arguments.js +60 -0
  59. package/vendor/node_modules/@legendapp/spark-cli/dist/commands.js +7 -2
  60. package/vendor/node_modules/@legendapp/spark-cli/dist/create.js +31 -5
  61. package/vendor/node_modules/@legendapp/spark-cli/dist/credentials.js +69 -23
  62. package/vendor/node_modules/@legendapp/spark-cli/dist/dev-command.js +12 -3
  63. package/vendor/node_modules/@legendapp/spark-cli/dist/dev.js +12 -9
  64. package/vendor/node_modules/@legendapp/spark-cli/dist/examples.js +1 -1
  65. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-dev-patch.cjs +4 -2
  66. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-metro.cjs +1 -1
  67. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-node.js +3 -3
  68. package/vendor/node_modules/@legendapp/spark-cli/dist/index.js +10 -38
  69. package/vendor/node_modules/@legendapp/spark-cli/dist/local.js +1 -1
  70. package/vendor/node_modules/@legendapp/spark-cli/dist/metro.cjs +26 -5
  71. package/vendor/node_modules/@legendapp/spark-cli/dist/native-metadata.js +26 -0
  72. package/vendor/node_modules/@legendapp/spark-cli/dist/native-patch-preflight.js +84 -0
  73. package/vendor/node_modules/@legendapp/spark-cli/dist/native-preparation.js +88 -0
  74. package/vendor/node_modules/@legendapp/spark-cli/dist/notary-profiles.js +75 -0
  75. package/vendor/node_modules/@legendapp/spark-cli/dist/package.js +4 -2
  76. package/vendor/node_modules/@legendapp/spark-cli/dist/platform.js +16 -2
  77. package/vendor/node_modules/@legendapp/spark-cli/dist/project.js +66 -13
  78. package/vendor/node_modules/@legendapp/spark-cli/dist/release.js +84 -4
  79. package/vendor/node_modules/@legendapp/spark-cli/dist/release.json +23 -18
  80. package/vendor/node_modules/@legendapp/spark-cli/dist/runner-install.js +145 -31
  81. package/vendor/node_modules/@legendapp/spark-cli/dist/signing.js +4 -3
  82. package/vendor/node_modules/@legendapp/spark-cli/dist/updates.js +41 -4
  83. package/vendor/node_modules/@legendapp/spark-cli/dist/windows.js +4 -1
  84. package/vendor/node_modules/@legendapp/spark-cli/package.json +7 -6
  85. package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/App.tsx +6 -12
  86. package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/desktop.config.json +5 -3
  87. package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/package.json +11 -9
  88. package/vendor/node_modules/@legendapp/spark-cli/templates/diff-lite/Git.macos.tsx +2 -2
  89. package/vendor/node_modules/@legendapp/spark-cli/templates/document-editor/Integration.macos.tsx +23 -21
  90. package/vendor/node_modules/@legendapp/spark-cli/templates/document-editor/Integration.windows.tsx +23 -21
  91. package/vendor/node_modules/@legendapp/spark-cli/templates/document-editor/io.desktop.ts +11 -8
  92. package/vendor/node_modules/@legendapp/spark-cli/templates/document-editor/io.macos.ts +3 -3
  93. package/vendor/node_modules/@legendapp/spark-cli/templates/example-shared/Lifecycle.desktop.tsx +8 -14
  94. package/vendor/node_modules/@legendapp/spark-cli/templates/example-shared/OpenFiles.desktop.tsx +5 -10
  95. package/vendor/node_modules/@legendapp/spark-cli/templates/example-shared/io.desktop.ts +11 -8
  96. package/vendor/node_modules/@legendapp/spark-cli/templates/example-shared/io.macos.ts +3 -3
  97. package/vendor/node_modules/@legendapp/spark-cli/templates/music-lite/assets.desktop.ts +2 -2
  98. package/vendor/node_modules/@legendapp/spark-cli/templates/notes-lite/Activation.desktop.tsx +2 -2
  99. package/vendor/node_modules/@legendapp/spark-cli/templates/notes-lite/Desktop.desktop.ts +37 -15
  100. package/vendor/node_modules/@legendapp/spark-cli/templates/notes-lite/session.ts +5 -6
  101. package/vendor/node_modules/@legendapp/spark-cli/templates/notes-lite/window-session.ts +5 -4
  102. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/desktop.config.json +5 -3
  103. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/metro.config.js +1 -1
  104. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/package.json +13 -11
  105. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/react-native.config.js +1 -1
  106. package/vendor/node_modules/@legendapp/spark-cli/templates/windows/package.json +13 -11
  107. package/vendor/node_modules/@legendapp/spark-cli/templates/windows/react-native.config.js +1 -1
  108. package/vendor/node_modules/@legendapp/spark-clipboard/macos/RNDesktopClipboard.mm +10 -16
  109. package/vendor/node_modules/@legendapp/spark-clipboard/package.json +2 -2
  110. package/vendor/node_modules/@legendapp/spark-clipboard/src/NativeDesktopClipboard.ts +1 -1
  111. package/vendor/node_modules/@legendapp/spark-clipboard/src/desktop.ts +55 -31
  112. package/vendor/node_modules/@legendapp/spark-clipboard/src/expo.ts +21 -11
  113. package/vendor/node_modules/@legendapp/spark-clipboard/src/options.ts +8 -0
  114. package/vendor/node_modules/@legendapp/spark-clipboard/src/types.ts +10 -0
  115. package/vendor/node_modules/@legendapp/spark-clipboard/windows/SparkClipboard/SparkClipboard.h +38 -15
  116. package/vendor/node_modules/@legendapp/spark-clipboard/windows/SparkClipboard/SparkClipboard.vcxproj +2 -0
  117. package/vendor/node_modules/@legendapp/spark-codex/RNCodex.podspec +22 -0
  118. package/vendor/node_modules/@legendapp/spark-codex/cpp/HybridCodexAppServer.hpp +23 -0
  119. package/vendor/node_modules/@legendapp/spark-codex/cpp/HybridCodexAppServer.mm +1529 -0
  120. package/vendor/node_modules/@legendapp/spark-codex/nitro.json +21 -0
  121. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/.gitattributes +1 -0
  122. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodex+autolinking.rb +62 -0
  123. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodex-Swift-Cxx-Bridge.cpp +17 -0
  124. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodex-Swift-Cxx-Bridge.hpp +27 -0
  125. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodex-Swift-Cxx-Umbrella.hpp +38 -0
  126. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodexAutolinking.mm +35 -0
  127. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodexAutolinking.swift +16 -0
  128. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/shared/c++/CodexAvailability.hpp +95 -0
  129. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/shared/c++/CodexRunResult.hpp +99 -0
  130. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/shared/c++/HybridCodexAppServerSpec.cpp +24 -0
  131. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/shared/c++/HybridCodexAppServerSpec.hpp +71 -0
  132. package/vendor/node_modules/@legendapp/spark-codex/package.json +35 -0
  133. package/vendor/node_modules/@legendapp/spark-codex/react-native.config.js +11 -0
  134. package/vendor/node_modules/@legendapp/spark-codex/src/Codex.nitro.ts +30 -0
  135. package/vendor/node_modules/@legendapp/spark-codex/src/index.ts +65 -0
  136. package/vendor/node_modules/@legendapp/spark-codex/src/native.ts +6 -0
  137. package/vendor/node_modules/@legendapp/spark-commands/package.json +21 -0
  138. package/vendor/node_modules/@legendapp/spark-commands/src/bindings.ts +90 -0
  139. package/vendor/node_modules/@legendapp/spark-commands/src/index.tsx +400 -0
  140. package/vendor/node_modules/@legendapp/spark-commands/src/router.ts +138 -0
  141. package/vendor/node_modules/@legendapp/spark-commands/src/storage.ts +20 -0
  142. package/vendor/node_modules/@legendapp/spark-context-menu/macos/RNContextMenu.mm +7 -8
  143. package/vendor/node_modules/@legendapp/spark-context-menu/package.json +4 -3
  144. package/vendor/node_modules/@legendapp/spark-context-menu/src/NativeContextMenu.ts +1 -1
  145. package/vendor/node_modules/@legendapp/spark-context-menu/src/index.ts +26 -26
  146. package/vendor/node_modules/@legendapp/spark-context-menu/windows/SparkContextMenu/SparkContextMenu.h +3 -2
  147. package/vendor/node_modules/@legendapp/spark-desktop-app/RNDesktopApp.podspec +2 -1
  148. package/vendor/node_modules/@legendapp/spark-desktop-app/cpp/SparkBinaryJSI.hpp +89 -0
  149. package/vendor/node_modules/@legendapp/spark-desktop-app/cpp/SparkBinaryWindows.hpp +22 -0
  150. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/RNDesktopApp.mm +13 -7
  151. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkBinaryJSI.h +48 -0
  152. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkDesktop.h +4 -0
  153. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkDesktop.mm +33 -33
  154. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkDesktopError.h +20 -0
  155. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkLifecycle.h +26 -0
  156. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkLifecycle.mm +49 -0
  157. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkQuitCoordinator.h +9 -0
  158. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkQuitCoordinator.mm +70 -0
  159. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkWindow.mm +9 -6
  160. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/recent-documents/RNRecentDocumentController.mm +55 -0
  161. package/vendor/node_modules/@legendapp/spark-desktop-app/package.json +5 -3
  162. package/vendor/node_modules/@legendapp/spark-desktop-app/src/NativeDesktopApp.ts +1 -1
  163. package/vendor/node_modules/@legendapp/spark-desktop-app/src/api.ts +86 -40
  164. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/accelerator.ts +40 -0
  165. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/index.ts +56 -0
  166. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/menu.ts +114 -0
  167. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/native-buffer.ts +22 -0
  168. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/path.ts +24 -0
  169. package/vendor/node_modules/@legendapp/spark-desktop-app/src/events.ts +75 -0
  170. package/vendor/node_modules/@legendapp/spark-desktop-app/src/index.ts +2 -0
  171. package/vendor/node_modules/@legendapp/spark-desktop-app/src/index.windows.ts +2 -0
  172. package/vendor/node_modules/@legendapp/spark-desktop-app/src/open-requests.ts +43 -0
  173. package/vendor/node_modules/@legendapp/spark-desktop-app/src/restoration.ts +11 -0
  174. package/vendor/node_modules/@legendapp/spark-desktop-app/src/transport.ts +10 -0
  175. package/vendor/node_modules/@legendapp/spark-desktop-config/app.plugin.cjs +10 -5
  176. package/vendor/node_modules/@legendapp/spark-desktop-config/app.plugin.d.cts +3 -0
  177. package/vendor/node_modules/@legendapp/spark-desktop-config/autolinking.cjs +35 -0
  178. package/vendor/node_modules/@legendapp/spark-desktop-config/config.cjs +16 -3
  179. package/vendor/node_modules/@legendapp/spark-desktop-config/config.d.cts +89 -9
  180. package/vendor/node_modules/@legendapp/spark-desktop-config/expo.d.cts +3 -0
  181. package/vendor/node_modules/@legendapp/spark-desktop-config/identity.cjs +3 -2
  182. package/vendor/node_modules/@legendapp/spark-desktop-config/lifecycle.cjs +20 -0
  183. package/vendor/node_modules/@legendapp/spark-desktop-config/package.json +6 -5
  184. package/vendor/node_modules/@legendapp/spark-desktop-config/schema.json +276 -58
  185. package/vendor/node_modules/@legendapp/spark-desktop-config/updates.cjs +1 -0
  186. package/vendor/node_modules/@legendapp/spark-desktop-config/validation.cjs +35 -0
  187. package/vendor/node_modules/@legendapp/spark-desktop-config/windows.plugin.cjs +2 -1
  188. package/vendor/node_modules/@legendapp/spark-desktop-host/AppDelegate.mm +129 -11
  189. package/vendor/node_modules/@legendapp/spark-desktop-host/package.json +2 -2
  190. package/vendor/node_modules/@legendapp/spark-desktop-host/windows/application.inc +277 -121
  191. package/vendor/node_modules/@legendapp/spark-desktop-links/common/AuthLoopback.h +5 -7
  192. package/vendor/node_modules/@legendapp/spark-desktop-links/macos/RNDesktopLinks.mm +10 -9
  193. package/vendor/node_modules/@legendapp/spark-desktop-links/package.json +2 -2
  194. package/vendor/node_modules/@legendapp/spark-desktop-links/src/NativeDesktopLinks.ts +1 -1
  195. package/vendor/node_modules/@legendapp/spark-desktop-links/src/api.ts +27 -33
  196. package/vendor/node_modules/@legendapp/spark-desktop-links/src/expo.ts +3 -4
  197. package/vendor/node_modules/@legendapp/spark-desktop-links/src/native.ts +9 -0
  198. package/vendor/node_modules/@legendapp/spark-desktop-links/windows/SparkLinks/SparkLinks.h +22 -22
  199. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/RNDesktopShortcuts.podspec +1 -0
  200. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/macos/RNDesktopShortcuts.mm +1 -1
  201. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/macos/keyboard-manager/RNKeyboardManager.h +9 -0
  202. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/macos/keyboard-manager/RNKeyboardManager.mm +307 -0
  203. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/package.json +8 -5
  204. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/NativeDesktopShortcuts.ts +1 -1
  205. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/api.ts +39 -14
  206. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/keyboard-manager/NativeKeyboardManager.ts +12 -0
  207. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/keyboard-manager/codes.ts +108 -0
  208. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/keyboard-manager/index.ts +99 -0
  209. package/vendor/node_modules/@legendapp/spark-desktop-windows/RNDesktopWindows.podspec +2 -0
  210. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/RNDesktopWindows.mm +229 -103
  211. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/SparkWindowGeometry.h +63 -0
  212. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/LegendWindowRegistry.h +20 -0
  213. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/LegendWindowRegistry.mm +27 -0
  214. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManager.h +12 -0
  215. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManager.mm +3130 -0
  216. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManagerStartup.mm +31 -0
  217. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/SidebarStartupFallbacks.mm +14 -0
  218. package/vendor/node_modules/@legendapp/spark-desktop-windows/package.json +10 -6
  219. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/NativeDesktopWindowManager.ts +1 -1
  220. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/api.ts +144 -70
  221. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/diagnostics.ts +17 -0
  222. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/index.ts +3 -1
  223. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/index.windows.ts +3 -1
  224. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/macos-adapter.ts +53 -0
  225. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/macos.ts +51 -0
  226. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/transport.ts +41 -0
  227. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/types.ts +140 -0
  228. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/validation.ts +162 -0
  229. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/window-manager/NativeWindowManager.ts +28 -0
  230. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/WindowProvider.tsx +20 -0
  231. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/createWindowsNavigator.tsx +78 -0
  232. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/index.ts +6 -0
  233. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/primaryWindowLifecycle.ts +34 -0
  234. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/types.ts +11 -0
  235. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/usePrimaryWindowLifecycle.ts +18 -0
  236. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/useWindowFocusEffect.ts +17 -0
  237. package/vendor/node_modules/@legendapp/spark-documents/package.json +22 -0
  238. package/vendor/node_modules/@legendapp/spark-documents/src/controller.ts +97 -0
  239. package/vendor/node_modules/@legendapp/spark-documents/src/documentTransition.ts +13 -0
  240. package/vendor/node_modules/@legendapp/spark-documents/src/hooks.ts +139 -0
  241. package/vendor/node_modules/@legendapp/spark-documents/src/index.tsx +50 -0
  242. package/vendor/node_modules/@legendapp/spark-documents/src/reload.ts +39 -0
  243. package/vendor/node_modules/@legendapp/spark-documents/src/requests.ts +15 -0
  244. package/vendor/node_modules/@legendapp/spark-drag-drop/package.json +4 -3
  245. package/vendor/node_modules/@legendapp/spark-drag-drop/src/DesktopDragViewNativeComponent.ts +1 -1
  246. package/vendor/node_modules/@legendapp/spark-drag-drop/src/contracts.ts +73 -18
  247. package/vendor/node_modules/@legendapp/spark-drag-drop/src/index.tsx +58 -15
  248. package/vendor/node_modules/@legendapp/spark-drag-drop/windows/SparkDragDrop/SparkDragDrop.h +14 -8
  249. package/vendor/node_modules/@legendapp/spark-file-dialog/ios/RNFileDialog.mm +34 -121
  250. package/vendor/node_modules/@legendapp/spark-file-dialog/package.json +12 -5
  251. package/vendor/node_modules/@legendapp/spark-file-dialog/src/NativeFileDialog.ts +1 -5
  252. package/vendor/node_modules/@legendapp/spark-file-dialog/src/index.ts +19 -84
  253. package/vendor/node_modules/@legendapp/spark-file-dialog/src/native.ts +12 -0
  254. package/vendor/node_modules/@legendapp/spark-file-dialog/src/options.ts +34 -14
  255. package/vendor/node_modules/@legendapp/spark-file-dialog/src/types.ts +26 -0
  256. package/vendor/node_modules/@legendapp/spark-file-dialog/windows/SparkFileDialog/SparkFileDialog.h +42 -58
  257. package/vendor/node_modules/@legendapp/spark-file-system/RNDesktopFileSystem.podspec +1 -0
  258. package/vendor/node_modules/@legendapp/spark-file-system/macos/RNDesktopFileSystem.mm +32 -21
  259. package/vendor/node_modules/@legendapp/spark-file-system/macos/SparkFileMutations.h +24 -0
  260. package/vendor/node_modules/@legendapp/spark-file-system/macos/SparkFileMutations.mm +215 -0
  261. package/vendor/node_modules/@legendapp/spark-file-system/macos/file-scanner/RNFileScanner.h +9 -0
  262. package/vendor/node_modules/@legendapp/spark-file-system/macos/file-scanner/RNFileScanner.mm +82 -0
  263. package/vendor/node_modules/@legendapp/spark-file-system/macos/file-scanner/RNFileScannerCore.h +47 -0
  264. package/vendor/node_modules/@legendapp/spark-file-system/macos/file-scanner/RNFileScannerCore.mm +306 -0
  265. package/vendor/node_modules/@legendapp/spark-file-system/package.json +8 -6
  266. package/vendor/node_modules/@legendapp/spark-file-system/src/NativeDesktopFileSystem.ts +1 -1
  267. package/vendor/node_modules/@legendapp/spark-file-system/src/file-scanner/NativeFileScanner.ts +11 -0
  268. package/vendor/node_modules/@legendapp/spark-file-system/src/file-scanner/index.ts +93 -0
  269. package/vendor/node_modules/@legendapp/spark-file-system/src/handles.ts +48 -14
  270. package/vendor/node_modules/@legendapp/spark-file-system/src/index.ts +134 -45
  271. package/vendor/node_modules/@legendapp/spark-file-system/src/path.ts +1 -9
  272. package/vendor/node_modules/@legendapp/spark-file-system/windows/SparkFileSystem/SparkFileSystem.h +102 -26
  273. package/vendor/node_modules/@legendapp/spark-file-system/windows/SparkFileSystem/SparkFileSystem.vcxproj +2 -0
  274. package/vendor/node_modules/@legendapp/spark-global-shortcuts/RNDesktopGlobalShortcuts.podspec +1 -0
  275. package/vendor/node_modules/@legendapp/spark-global-shortcuts/macos/RNDesktopGlobalShortcuts.mm +7 -4
  276. package/vendor/node_modules/@legendapp/spark-global-shortcuts/package.json +4 -3
  277. package/vendor/node_modules/@legendapp/spark-global-shortcuts/src/NativeDesktopGlobalShortcuts.ts +1 -1
  278. package/vendor/node_modules/@legendapp/spark-global-shortcuts/src/index.ts +39 -10
  279. package/vendor/node_modules/@legendapp/spark-global-shortcuts/windows/SparkGlobalShortcuts/SparkGlobalShortcuts.h +5 -4
  280. package/vendor/node_modules/@legendapp/spark-message-dialog/macos/RNDesktopMessageDialog.mm +2 -2
  281. package/vendor/node_modules/@legendapp/spark-message-dialog/package.json +2 -2
  282. package/vendor/node_modules/@legendapp/spark-message-dialog/src/NativeDesktopMessageDialog.ts +1 -1
  283. package/vendor/node_modules/@legendapp/spark-message-dialog/src/index.ts +48 -11
  284. package/vendor/node_modules/@legendapp/spark-native-menu/ios/RNNativeMenu.mm +141 -609
  285. package/vendor/node_modules/@legendapp/spark-native-menu/package.json +9 -4
  286. package/vendor/node_modules/@legendapp/spark-native-menu/src/NativeMenu.ts +2 -7
  287. package/vendor/node_modules/@legendapp/spark-native-menu/src/api.ts +76 -121
  288. package/vendor/node_modules/@legendapp/spark-native-menu/src/hooks.ts +97 -0
  289. package/vendor/node_modules/@legendapp/spark-native-menu/src/windows-menus.ts +43 -37
  290. package/vendor/node_modules/@legendapp/spark-notifications/macos/RNDesktopNotifications.mm +75 -30
  291. package/vendor/node_modules/@legendapp/spark-notifications/package.json +2 -2
  292. package/vendor/node_modules/@legendapp/spark-notifications/src/NativeDesktopNotifications.ts +1 -1
  293. package/vendor/node_modules/@legendapp/spark-notifications/src/index.ts +130 -42
  294. package/vendor/node_modules/@legendapp/spark-notifications/windows/SparkNotifications/SparkNotifications.h +46 -8
  295. package/vendor/node_modules/@legendapp/spark-processes/macos/RNDesktopProcesses.mm +94 -21
  296. package/vendor/node_modules/@legendapp/spark-processes/package.json +4 -3
  297. package/vendor/node_modules/@legendapp/spark-processes/src/NativeDesktopProcesses.ts +1 -1
  298. package/vendor/node_modules/@legendapp/spark-processes/src/index.ts +124 -28
  299. package/vendor/node_modules/@legendapp/spark-processes/src/types.ts +34 -0
  300. package/vendor/node_modules/@legendapp/spark-processes/windows/SparkProcesses/SparkProcesses.h +83 -34
  301. package/vendor/node_modules/@legendapp/spark-processes/windows/SparkProcesses/SparkProcesses.vcxproj +2 -0
  302. package/vendor/node_modules/@legendapp/spark-secure-storage/macos/RNDesktopSecureStorage.mm +49 -40
  303. package/vendor/node_modules/@legendapp/spark-secure-storage/package.json +2 -2
  304. package/vendor/node_modules/@legendapp/spark-secure-storage/src/NativeDesktopSecureStorage.ts +1 -1
  305. package/vendor/node_modules/@legendapp/spark-secure-storage/src/desktop.ts +16 -14
  306. package/vendor/node_modules/@legendapp/spark-secure-storage/src/expo.ts +0 -1
  307. package/vendor/node_modules/@legendapp/spark-secure-storage/src/options.ts +3 -2
  308. package/vendor/node_modules/@legendapp/spark-secure-storage/src/unavailable.ts +6 -3
  309. package/vendor/node_modules/@legendapp/spark-settings/package.json +10 -3
  310. package/vendor/node_modules/@legendapp/spark-settings/src/fileStorage.ts +11 -0
  311. package/vendor/node_modules/@legendapp/spark-settings/src/index.ts +7 -9
  312. package/vendor/node_modules/@legendapp/spark-settings/src/storage/index.ts +142 -0
  313. package/vendor/node_modules/@legendapp/spark-settings/src/store.ts +63 -21
  314. package/vendor/node_modules/@legendapp/spark-settings-ui/package.json +20 -0
  315. package/vendor/node_modules/@legendapp/spark-settings-ui/src/index.tsx +436 -0
  316. package/vendor/node_modules/@legendapp/spark-settings-ui/src/options.ts +10 -0
  317. package/vendor/node_modules/@legendapp/spark-sqlite/package.json +3 -3
  318. package/vendor/node_modules/@legendapp/spark-sqlite/src/database.ts +182 -0
  319. package/vendor/node_modules/@legendapp/spark-sqlite/src/index.ts +19 -6
  320. package/vendor/node_modules/@legendapp/spark-sqlite/src/types.ts +22 -0
  321. package/vendor/node_modules/@legendapp/spark-system/macos/RNDesktopSystem.mm +36 -11
  322. package/vendor/node_modules/@legendapp/spark-system/package.json +2 -2
  323. package/vendor/node_modules/@legendapp/spark-system/src/NativeDesktopSystem.ts +1 -1
  324. package/vendor/node_modules/@legendapp/spark-system/src/index.ts +89 -32
  325. package/vendor/node_modules/@legendapp/spark-system/windows/SparkSystem/SparkSystem.h +8 -4
  326. package/vendor/node_modules/@legendapp/spark-tray/macos/RNDesktopTray.mm +20 -8
  327. package/vendor/node_modules/@legendapp/spark-tray/package.json +2 -2
  328. package/vendor/node_modules/@legendapp/spark-tray/src/NativeDesktopTray.ts +1 -1
  329. package/vendor/node_modules/@legendapp/spark-tray/src/index.ts +91 -34
  330. package/vendor/node_modules/@legendapp/spark-tray/windows/SparkTray/SparkTray.h +45 -15
  331. package/vendor/node_modules/@legendapp/spark-ui/macos/RNSparkButton.mm +2 -1
  332. package/vendor/node_modules/@legendapp/spark-ui/macos/RNSparkTextInput.mm +10 -2
  333. package/vendor/node_modules/@legendapp/spark-ui/macos/appkit-split-view/RNSidebarSplitViewComponent.h +15 -0
  334. package/vendor/node_modules/@legendapp/spark-ui/macos/appkit-split-view/RNSidebarSplitViewComponent.mm +1182 -0
  335. package/vendor/node_modules/@legendapp/spark-ui/macos/glass-effect-view/RNGlassEffectView.mm +72 -0
  336. package/vendor/node_modules/@legendapp/spark-ui/macos/native-select/RNNativeSegmentedControl.h +15 -0
  337. package/vendor/node_modules/@legendapp/spark-ui/macos/native-select/RNNativeSegmentedControl.mm +194 -0
  338. package/vendor/node_modules/@legendapp/spark-ui/macos/native-select/RNNativeSelect.h +15 -0
  339. package/vendor/node_modules/@legendapp/spark-ui/macos/native-select/RNNativeSelect.mm +180 -0
  340. package/vendor/node_modules/@legendapp/spark-ui/macos/sf-symbol/RNSFSymbol.h +15 -0
  341. package/vendor/node_modules/@legendapp/spark-ui/macos/sf-symbol/RNSFSymbol.mm +155 -0
  342. package/vendor/node_modules/@legendapp/spark-ui/macos/sidebar/RNSidebar.h +18 -0
  343. package/vendor/node_modules/@legendapp/spark-ui/macos/sidebar/RNSidebar.mm +645 -0
  344. package/vendor/node_modules/@legendapp/spark-ui/macos/text-input-search/RNTextInputSearch.h +15 -0
  345. package/vendor/node_modules/@legendapp/spark-ui/macos/text-input-search/RNTextInputSearch.mm +164 -0
  346. package/vendor/node_modules/@legendapp/spark-ui/package.json +16 -4
  347. package/vendor/node_modules/@legendapp/spark-ui/src/SparkSelectNativeComponent.ts +3 -1
  348. package/vendor/node_modules/@legendapp/spark-ui/src/SparkTextInputNativeComponent.ts +6 -2
  349. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/SidebarSplitViewNativeComponent.ts +31 -0
  350. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/index.tsx +77 -0
  351. package/vendor/node_modules/@legendapp/spark-ui/src/availability.ts +10 -0
  352. package/vendor/node_modules/@legendapp/spark-ui/src/classnames/index.ts +8 -0
  353. package/vendor/node_modules/@legendapp/spark-ui/src/control.ts +42 -0
  354. package/vendor/node_modules/@legendapp/spark-ui/src/desktop.tsx +56 -0
  355. package/vendor/node_modules/@legendapp/spark-ui/src/glass-effect-view/index.tsx +15 -0
  356. package/vendor/node_modules/@legendapp/spark-ui/src/index.android.tsx +35 -21
  357. package/vendor/node_modules/@legendapp/spark-ui/src/index.ios.tsx +55 -26
  358. package/vendor/node_modules/@legendapp/spark-ui/src/index.tsx +1 -27
  359. package/vendor/node_modules/@legendapp/spark-ui/src/index.web.tsx +45 -14
  360. package/vendor/node_modules/@legendapp/spark-ui/src/index.windows.tsx +1 -42
  361. package/vendor/node_modules/@legendapp/spark-ui/src/mobile-input.tsx +11 -0
  362. package/vendor/node_modules/@legendapp/spark-ui/src/native-select/NativeSegmentedControlNativeComponent.ts +17 -0
  363. package/vendor/node_modules/@legendapp/spark-ui/src/native-select/NativeSelectNativeComponent.ts +17 -0
  364. package/vendor/node_modules/@legendapp/spark-ui/src/select.ts +5 -3
  365. package/vendor/node_modules/@legendapp/spark-ui/src/sf-symbol/SFSymbolNativeComponent.ts +13 -0
  366. package/vendor/node_modules/@legendapp/spark-ui/src/sf-symbol/index.tsx +27 -0
  367. package/vendor/node_modules/@legendapp/spark-ui/src/sidebar/SidebarItemNativeComponent.ts +25 -0
  368. package/vendor/node_modules/@legendapp/spark-ui/src/sidebar/SidebarNativeComponent.ts +24 -0
  369. package/vendor/node_modules/@legendapp/spark-ui/src/sidebar/index.tsx +104 -0
  370. package/vendor/node_modules/@legendapp/spark-ui/src/specialized.ts +22 -0
  371. package/vendor/node_modules/@legendapp/spark-ui/src/text-input-search/TextInputSearchNativeComponent.ts +33 -0
  372. package/vendor/node_modules/@legendapp/spark-ui/src/text-input-search/index.tsx +34 -0
  373. package/vendor/node_modules/@legendapp/spark-ui/src/types.ts +30 -19
  374. package/vendor/node_modules/@legendapp/spark-ui/uniwind.ts +3 -1
  375. package/vendor/node_modules/@legendapp/spark-ui/uniwind.web.ts +3 -1
  376. package/vendor/node_modules/@legendapp/spark-ui/windows/SparkUI/SparkUI.h +16 -11
  377. package/vendor/node_modules/@legendapp/spark-updates/macos/RNDesktopUpdates.mm +21 -8
  378. package/vendor/node_modules/@legendapp/spark-updates/package.json +2 -2
  379. package/vendor/node_modules/@legendapp/spark-updates/src/NativeDesktopUpdates.ts +3 -2
  380. package/vendor/node_modules/@legendapp/spark-updates/src/index.ts +71 -15
  381. package/vendor/node_modules/@legendapp/spark-webview/package.json +2 -2
  382. package/vendor/node_modules/@legendapp/spark-webview/src/WebView.tsx +11 -0
  383. package/vendor/node_modules/@legendapp/spark-webview/src/adapter.ts +55 -0
  384. package/vendor/node_modules/@legendapp/spark-webview/src/index.ts +2 -2
  385. package/vendor/node_modules/@legendapp/spark-webview/src/types.ts +32 -0
  386. package/vendor/node_modules/@legendapp/spark-webview/src/view-props.ts +28 -0
  387. package/vendor/node_modules/@legendapp/spark-window-options/index.cjs +51 -14
  388. package/vendor/node_modules/@legendapp/spark-window-options/index.d.ts +21 -8
  389. package/vendor/node_modules/@legendapp/spark-window-options/package.json +1 -1
  390. package/windows-macos.ts +1 -0
  391. package/expo-metro.cjs +0 -1
  392. package/message-dialog.ts +0 -1
  393. package/universal.cjs +0 -1
  394. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/accelerator.ts +0 -22
  395. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows-options.ts +0 -11
  396. package/vendor/node_modules/@legendapp/spark-file-dialog/src/index.windows.ts +0 -15
  397. package/vendor/node_modules/@legendapp/spark-global-shortcuts/src/accelerator.ts +0 -22
  398. package/vendor/node_modules/@legendapp/spark-ui/macos/RNSparkSelect.h +0 -4
  399. package/vendor/node_modules/@legendapp/spark-ui/macos/RNSparkSelect.mm +0 -53
@@ -0,0 +1,3130 @@
1
+ #include <math.h>
2
+ #import "RNWindowManager.h"
3
+ #import "LegendWindowRegistry.h"
4
+ #if TARGET_OS_OSX
5
+ extern "C" void SparkRebindWindowDelegate(NSWindow *, NSString *, id<NSWindowDelegate>);
6
+ #endif
7
+
8
+ #import <React-RCTAppDelegate/RCTRootViewFactory.h>
9
+ #import <React/RCTBridge.h>
10
+ #import <React/RCTBridgeModule.h>
11
+ #import <React/RCTRootView.h>
12
+ #import <React/RCTUIKit.h>
13
+ #import <React/RCTUtils.h>
14
+ #import <TargetConditionals.h>
15
+
16
+ #include <cmath>
17
+ #include <cxxreact/ReactMarker.h>
18
+
19
+ #if TARGET_OS_OSX && !__has_include(<RNDesktopApp/SparkDesktop.h>)
20
+ extern double LegendMainWindowFirstVisibleTimeMs;
21
+ extern double LegendMainWindowReactRootAttachedTimeMs;
22
+ #endif
23
+
24
+ #if TARGET_OS_OSX
25
+ #import <AppKit/AppKit.h>
26
+ #if __has_include(<RNDesktopApp/SparkDesktop.h>)
27
+ #import <RNDesktopApp/SparkDesktop.h>
28
+ #endif
29
+ #import <CoreImage/CoreImage.h>
30
+ #import <CoreGraphics/CoreGraphics.h>
31
+ #import <objc/runtime.h>
32
+ #import <QuartzCore/QuartzCore.h>
33
+ #endif
34
+
35
+ @protocol RNWindowManagerRootViewFactoryProvider <NSObject>
36
+ - (RCTRootViewFactory *)rootViewFactory;
37
+ #if TARGET_OS_OSX
38
+ - (NSWindow *)window;
39
+ #endif
40
+ @end
41
+
42
+ #if TARGET_OS_OSX
43
+ static char LegendManagedFullscreenPresentationKey;
44
+ static NSString * const LegendApplicationReopenRequestedNotification = @"LegendApplicationReopenRequestedNotification";
45
+ static NSString * const LegendMainWindowCloseRequestedNotification = @"LegendMainWindowCloseRequestedNotification";
46
+ extern "C" void LegendPrepareSidebarSplitViewStartup(NSWindow *, NSDictionary *) __attribute__((weak_import));
47
+ extern "C" BOOL LegendAttachSidebarSplitViewStartupRoot(NSWindow *, NSView *, void (^)(void)) __attribute__((weak_import));
48
+ extern "C" void LegendFinishSidebarSplitViewStartup(NSWindow *) __attribute__((weak_import));
49
+ static NSString * const LegendMainWindowStartupSplitViewKey = @"LegendMainWindowStartupSplitView";
50
+
51
+ extern "C" NSDictionary *LegendMainWindowStartupSplitViewConfiguration(void)
52
+ {
53
+ NSDictionary *configuration = [NSUserDefaults.standardUserDefaults dictionaryForKey:LegendMainWindowStartupSplitViewKey];
54
+ return [configuration[@"restoreOnLaunch"] isEqual:@NO] ? nil : configuration;
55
+ }
56
+
57
+ static NSString * const LegendRestorableWindowOptionsDefaultsKey = @"LegendRestorableWindowOptions";
58
+ static NSMutableDictionary<NSString *, NSWindow *> *LegendPrecreatedWindows;
59
+ static BOOL LegendApplicationIsTerminating = NO;
60
+
61
+ static NSString *LegendManagedWindowFrameAutosaveName(NSString *identifier)
62
+ {
63
+ NSString *bundleIdentifier = NSBundle.mainBundle.bundleIdentifier ?: @"legend-app";
64
+ return [NSString stringWithFormat:@"LegendManagedWindow.%@.%@", bundleIdentifier, identifier ?: @""];
65
+ }
66
+
67
+ static NSArray<NSDictionary *> *LegendReadRestorableWindowOptions(void)
68
+ {
69
+ NSArray *records = [NSUserDefaults.standardUserDefaults arrayForKey:LegendRestorableWindowOptionsDefaultsKey];
70
+ if (![records isKindOfClass:NSArray.class]) {
71
+ return @[];
72
+ }
73
+ NSMutableArray<NSDictionary *> *options = [NSMutableArray new];
74
+ for (NSString *record in records) {
75
+ if (![record isKindOfClass:NSString.class]) {
76
+ continue;
77
+ }
78
+ NSData *data = [record dataUsingEncoding:NSUTF8StringEncoding];
79
+ id value = data ? [NSJSONSerialization JSONObjectWithData:data options:0 error:nil] : nil;
80
+ if ([value isKindOfClass:NSDictionary.class]) {
81
+ [options addObject:value];
82
+ }
83
+ }
84
+ return options;
85
+ }
86
+
87
+ static NSDictionary *LegendFindRestorableWindowOptions(NSString *identifier)
88
+ {
89
+ for (NSDictionary *record in LegendReadRestorableWindowOptions()) {
90
+ if ([record[@"identifier"] isEqualToString:identifier]) {
91
+ return record;
92
+ }
93
+ }
94
+ return nil;
95
+ }
96
+
97
+ static NSDictionary *LegendRestorableWindowOptions(NSDictionary *options)
98
+ {
99
+ static NSArray<NSString *> *keys;
100
+ static dispatch_once_t onceToken;
101
+ dispatch_once(&onceToken, ^{
102
+ keys = @[
103
+ @"deferOrderFront", @"hasShadow", @"height", @"identifier", @"level", @"moduleName",
104
+ @"representedURL", @"restoreOnLaunch", @"title", @"transparentBackground", @"width", @"windowStyle",
105
+ @"x", @"y",
106
+ ];
107
+ });
108
+ NSMutableDictionary *restorableOptions = [NSMutableDictionary new];
109
+ for (NSString *key in keys) {
110
+ id value = options[key];
111
+ if (value != nil) {
112
+ restorableOptions[key] = value;
113
+ }
114
+ }
115
+ return restorableOptions;
116
+ }
117
+
118
+ static void LegendWriteRestorableWindowOptions(NSArray<NSDictionary *> *options)
119
+ {
120
+ NSMutableArray<NSString *> *records = [NSMutableArray new];
121
+ for (NSDictionary *record in options) {
122
+ NSData *data = [NSJSONSerialization dataWithJSONObject:record options:0 error:nil];
123
+ NSString *json = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : nil;
124
+ if (json) {
125
+ [records addObject:json];
126
+ }
127
+ }
128
+ [NSUserDefaults.standardUserDefaults setObject:records forKey:LegendRestorableWindowOptionsDefaultsKey];
129
+ }
130
+
131
+ static void LegendSetRestorableWindowOptions(NSString *identifier, NSDictionary *options)
132
+ {
133
+ if (identifier.length == 0) {
134
+ return;
135
+ }
136
+ NSMutableArray<NSDictionary *> *records = [LegendReadRestorableWindowOptions() mutableCopy];
137
+ NSIndexSet *matchingIndexes = [records indexesOfObjectsPassingTest:^BOOL(NSDictionary *record, NSUInteger index, BOOL *stop) {
138
+ return [record[@"identifier"] isEqualToString:identifier];
139
+ }];
140
+ if (matchingIndexes.count > 0) {
141
+ [records removeObjectsAtIndexes:matchingIndexes];
142
+ }
143
+ if (options != nil) {
144
+ [records addObject:LegendRestorableWindowOptions(options)];
145
+ }
146
+ LegendWriteRestorableWindowOptions(records);
147
+ }
148
+
149
+ static void LegendRemovePrecreatedWindow(NSString *identifier)
150
+ {
151
+ NSWindow *window = LegendPrecreatedWindows[identifier];
152
+ [LegendPrecreatedWindows removeObjectForKey:identifier];
153
+ [window close];
154
+ }
155
+
156
+ extern "C" NSWindow *LegendTakePrecreatedWindow(NSString *identifier)
157
+ {
158
+ NSWindow *window = LegendPrecreatedWindows[identifier];
159
+ [LegendPrecreatedWindows removeObjectForKey:identifier];
160
+ return window;
161
+ }
162
+
163
+ static inline NSAppearance *LegendDarkAppearance()
164
+ {
165
+ if (@available(macOS 10.14, *)) {
166
+ return [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua];
167
+ } else if (@available(macOS 10.10, *)) {
168
+ return [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark];
169
+ }
170
+ return nil;
171
+ }
172
+
173
+
174
+
175
+ static NSAppearance *LegendAppearanceForName(NSString *value)
176
+ {
177
+ if (![value isKindOfClass:NSString.class] || value.length == 0 || [value isEqualToString:@"system"]) {
178
+ return nil;
179
+ }
180
+
181
+ if ([value isEqualToString:@"light"]) {
182
+ return [NSAppearance appearanceNamed:NSAppearanceNameAqua];
183
+ }
184
+
185
+ if ([value isEqualToString:@"dark"]) {
186
+ return [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua] ?: LegendDarkAppearance();
187
+ }
188
+
189
+ return nil;
190
+ }
191
+
192
+ static void LegendApplyWindowAppearance(NSWindow *window, NSString *value)
193
+ {
194
+ if (![value isKindOfClass:NSString.class] || value.length == 0) {
195
+ return;
196
+ }
197
+
198
+ window.appearance = LegendAppearanceForName(value);
199
+ [window displayIfNeeded];
200
+ }
201
+
202
+ static void LegendApplyTitleVisibility(NSWindow *window, NSString *value)
203
+ {
204
+ if (![value isKindOfClass:NSString.class] || value.length == 0) {
205
+ return;
206
+ }
207
+
208
+ if ([value isEqualToString:@"hidden"]) {
209
+ window.titleVisibility = NSWindowTitleHidden;
210
+ } else if ([value isEqualToString:@"visible"]) {
211
+ window.titleVisibility = NSWindowTitleVisible;
212
+ }
213
+ }
214
+
215
+ static void LegendApplyToolbarStyle(NSWindow *window, NSString *value)
216
+ {
217
+ if (![value isKindOfClass:NSString.class] || value.length == 0) {
218
+ return;
219
+ }
220
+
221
+ if (@available(macOS 11.0, *)) {
222
+ if ([value isEqualToString:@"automatic"]) {
223
+ window.toolbarStyle = NSWindowToolbarStyleAutomatic;
224
+ } else if ([value isEqualToString:@"expanded"]) {
225
+ window.toolbarStyle = NSWindowToolbarStyleExpanded;
226
+ } else if ([value isEqualToString:@"preference"]) {
227
+ window.toolbarStyle = NSWindowToolbarStylePreference;
228
+ } else if ([value isEqualToString:@"unified"]) {
229
+ window.toolbarStyle = NSWindowToolbarStyleUnified;
230
+ } else if ([value isEqualToString:@"unifiedCompact"]) {
231
+ window.toolbarStyle = NSWindowToolbarStyleUnifiedCompact;
232
+ }
233
+ }
234
+ }
235
+
236
+ static void LegendApplyTitlebarSeparatorStyle(NSWindow *window, NSString *value)
237
+ {
238
+ if (![value isKindOfClass:NSString.class] || value.length == 0) {
239
+ return;
240
+ }
241
+
242
+ if (@available(macOS 11.0, *)) {
243
+ if ([value isEqualToString:@"automatic"]) {
244
+ window.titlebarSeparatorStyle = NSTitlebarSeparatorStyleAutomatic;
245
+ } else if ([value isEqualToString:@"none"]) {
246
+ window.titlebarSeparatorStyle = NSTitlebarSeparatorStyleNone;
247
+ } else if ([value isEqualToString:@"line"]) {
248
+ window.titlebarSeparatorStyle = NSTitlebarSeparatorStyleLine;
249
+ } else if ([value isEqualToString:@"shadow"]) {
250
+ window.titlebarSeparatorStyle = NSTitlebarSeparatorStyleShadow;
251
+ }
252
+ }
253
+ }
254
+
255
+ static NSColor *LegendColorFromHexString(NSString *value)
256
+ {
257
+ if (![value isKindOfClass:NSString.class] || value.length == 0) {
258
+ return nil;
259
+ }
260
+
261
+ if ([value isEqualToString:@"system"]) {
262
+ return NSColor.windowBackgroundColor;
263
+ }
264
+
265
+ NSString *hex = [value hasPrefix:@"#"] ? [value substringFromIndex:1] : value;
266
+ if (hex.length != 6 && hex.length != 8) {
267
+ return nil;
268
+ }
269
+
270
+ unsigned long long raw = 0;
271
+ NSScanner *scanner = [NSScanner scannerWithString:hex];
272
+ if (![scanner scanHexLongLong:&raw]) {
273
+ return nil;
274
+ }
275
+
276
+ CGFloat red = 0;
277
+ CGFloat green = 0;
278
+ CGFloat blue = 0;
279
+ CGFloat alpha = 1;
280
+
281
+ if (hex.length == 8) {
282
+ red = ((raw >> 24) & 0xff) / 255.0;
283
+ green = ((raw >> 16) & 0xff) / 255.0;
284
+ blue = ((raw >> 8) & 0xff) / 255.0;
285
+ alpha = (raw & 0xff) / 255.0;
286
+ } else {
287
+ red = ((raw >> 16) & 0xff) / 255.0;
288
+ green = ((raw >> 8) & 0xff) / 255.0;
289
+ blue = (raw & 0xff) / 255.0;
290
+ }
291
+
292
+ return [NSColor colorWithSRGBRed:red green:green blue:blue alpha:alpha];
293
+ }
294
+
295
+ static BOOL LegendAppearanceIsDark(NSAppearance *appearance)
296
+ {
297
+ if (@available(macOS 10.14, *)) {
298
+ NSAppearanceName match = [appearance bestMatchFromAppearancesWithNames:@[
299
+ NSAppearanceNameDarkAqua,
300
+ NSAppearanceNameAqua,
301
+ ]];
302
+ return [match isEqualToString:NSAppearanceNameDarkAqua];
303
+ }
304
+
305
+ return NO;
306
+ }
307
+
308
+ static BOOL LegendWindowUsesDarkAppearance(NSWindow *window)
309
+ {
310
+ if (window.appearance != nil) {
311
+ return LegendAppearanceIsDark(window.appearance);
312
+ }
313
+ if (NSApp.appearance != nil) {
314
+ return LegendAppearanceIsDark(NSApp.appearance);
315
+ }
316
+
317
+ NSString *interfaceStyle = [NSUserDefaults.standardUserDefaults stringForKey:@"AppleInterfaceStyle"];
318
+ return [interfaceStyle caseInsensitiveCompare:@"Dark"] == NSOrderedSame ||
319
+ LegendAppearanceIsDark(NSApp.effectiveAppearance);
320
+ }
321
+
322
+ static NSColor *LegendWindowStartupBackgroundColor(NSWindow *window, NSString *value)
323
+ {
324
+ NSColor *configuredColor = LegendColorFromHexString(value);
325
+ if (configuredColor != nil) {
326
+ return configuredColor;
327
+ }
328
+
329
+ return LegendWindowUsesDarkAppearance(window)
330
+ ? [NSColor colorWithSRGBRed:25.0 / 255.0 green:26.0 / 255.0 blue:27.0 / 255.0 alpha:1]
331
+ : [NSColor colorWithSRGBRed:245.0 / 255.0 green:246.0 / 255.0 blue:248.0 / 255.0 alpha:1];
332
+ }
333
+
334
+ static void LegendApplyBackgroundColorToView(NSView *view, NSColor *backgroundColor)
335
+ {
336
+ if (!view || !backgroundColor) {
337
+ return;
338
+ }
339
+
340
+ if ([view respondsToSelector:@selector(setBackgroundColor:)]) {
341
+ [(id)view setBackgroundColor:backgroundColor];
342
+ }
343
+ view.wantsLayer = YES;
344
+ view.layer.backgroundColor = backgroundColor.CGColor;
345
+ }
346
+
347
+ static BOOL LegendViewIsTitlebarContainer(NSView *view)
348
+ {
349
+ NSString *className = NSStringFromClass(view.class);
350
+ return [className isEqualToString:@"NSTitlebarContainerView"] ||
351
+ [className isEqualToString:@"NSTitlebarView"] ||
352
+ [className isEqualToString:@"NSTitlebarBackgroundView"];
353
+ }
354
+
355
+ static NSVisualEffectMaterial LegendVisualEffectMaterialForTitlebarMaterial(NSString *value)
356
+ {
357
+ if ([value isEqualToString:@"hudWindow"]) {
358
+ return NSVisualEffectMaterialHUDWindow;
359
+ }
360
+ if ([value isEqualToString:@"sidebar"]) {
361
+ return NSVisualEffectMaterialSidebar;
362
+ }
363
+ if ([value isEqualToString:@"windowBackground"]) {
364
+ return NSVisualEffectMaterialWindowBackground;
365
+ }
366
+ if ([value isEqualToString:@"titlebar"]) {
367
+ return NSVisualEffectMaterialTitlebar;
368
+ }
369
+ if ([value isEqualToString:@"glass"] || [value isEqualToString:@"headerView"]) {
370
+ if (@available(macOS 10.14, *)) {
371
+ return NSVisualEffectMaterialHeaderView;
372
+ }
373
+ return NSVisualEffectMaterialTitlebar;
374
+ }
375
+ return NSVisualEffectMaterialTitlebar;
376
+ }
377
+
378
+ static NSVisualEffectBlendingMode LegendVisualEffectBlendingModeForName(NSString *value)
379
+ {
380
+ return [value isEqualToString:@"withinWindow"] ? NSVisualEffectBlendingModeWithinWindow : NSVisualEffectBlendingModeBehindWindow;
381
+ }
382
+
383
+ static NSVisualEffectState LegendVisualEffectStateForName(NSString *value)
384
+ {
385
+ if ([value isEqualToString:@"active"]) {
386
+ return NSVisualEffectStateActive;
387
+ }
388
+ if ([value isEqualToString:@"inactive"]) {
389
+ return NSVisualEffectStateInactive;
390
+ }
391
+ return NSVisualEffectStateFollowsWindowActiveState;
392
+ }
393
+
394
+ static NSRect LegendTitlebarMaterialFrame(NSWindow *window, NSView *frameView)
395
+ {
396
+ NSRect frameBounds = frameView.bounds;
397
+ NSRect contentLayoutRect = [frameView convertRect:window.contentLayoutRect fromView:nil];
398
+ CGFloat materialMinY = NSMaxY(contentLayoutRect);
399
+ CGFloat materialHeight = NSMaxY(frameBounds) - materialMinY;
400
+
401
+ if (materialHeight <= 0 || materialHeight > NSHeight(frameBounds)) {
402
+ materialHeight = MAX(0, NSHeight(window.frame) - NSHeight(window.contentLayoutRect));
403
+ materialMinY = NSMaxY(frameBounds) - materialHeight;
404
+ }
405
+
406
+ return NSMakeRect(0, materialMinY, NSWidth(frameBounds), materialHeight);
407
+ }
408
+
409
+ API_AVAILABLE(macos(26.0))
410
+ static NSView *LegendCreateOverscannedGlassEffectView(NSRect frame)
411
+ {
412
+ NSView *containerView = [[NSView alloc] initWithFrame:frame];
413
+ containerView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
414
+ containerView.wantsLayer = YES;
415
+ containerView.layer.backgroundColor = NSColor.clearColor.CGColor;
416
+ containerView.layer.masksToBounds = YES;
417
+
418
+ CGFloat overscan = 48;
419
+ NSRect glassFrame = NSMakeRect(-overscan, 0, NSWidth(frame) + overscan, NSHeight(frame) + overscan);
420
+ NSGlassEffectView *glassView = [[NSGlassEffectView alloc] initWithFrame:glassFrame];
421
+ glassView.cornerRadius = 0;
422
+ glassView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
423
+ glassView.wantsLayer = YES;
424
+ glassView.layer.backgroundColor = NSColor.clearColor.CGColor;
425
+ [containerView addSubview:glassView];
426
+
427
+ return containerView;
428
+ }
429
+
430
+ static void LegendApplyTitlebarBackgroundColor(NSView *view, NSColor *backgroundColor)
431
+ {
432
+ BOOL isTitlebarContainer = LegendViewIsTitlebarContainer(view);
433
+
434
+ if (isTitlebarContainer) {
435
+ LegendApplyBackgroundColorToView(view, backgroundColor);
436
+ }
437
+
438
+ for (NSView *subview in view.subviews) {
439
+ LegendApplyTitlebarBackgroundColor(subview, backgroundColor);
440
+ }
441
+ }
442
+
443
+ static BOOL LegendUsesFullSizeContentLayout(NSWindow *window);
444
+
445
+ static void LegendApplyWindowBackgroundColor(NSWindow *window, NSString *value)
446
+ {
447
+ NSColor *backgroundColor = LegendColorFromHexString(value);
448
+ if (!backgroundColor) {
449
+ return;
450
+ }
451
+
452
+ window.backgroundColor = backgroundColor;
453
+ window.opaque = backgroundColor.alphaComponent >= 1;
454
+
455
+ NSView *contentView = window.contentView;
456
+ if (contentView) {
457
+ // System window backing must remain behind AppKit-managed sidebar materials.
458
+ LegendApplyBackgroundColorToView(contentView, [value isEqualToString:@"system"] ? NSColor.clearColor : backgroundColor);
459
+ }
460
+
461
+ NSView *frameView = contentView.superview;
462
+ if (frameView) {
463
+ NSColor *titlebarBackgroundColor = LegendUsesFullSizeContentLayout(window) ? NSColor.clearColor : backgroundColor;
464
+ LegendApplyTitlebarBackgroundColor(frameView, titlebarBackgroundColor);
465
+ dispatch_async(dispatch_get_main_queue(), ^{
466
+ NSView *currentFrameView = window.contentView.superview;
467
+ if (currentFrameView) {
468
+ LegendApplyTitlebarBackgroundColor(currentFrameView, titlebarBackgroundColor);
469
+ }
470
+ });
471
+ }
472
+ }
473
+
474
+ static char LegendManagedRootViewKey;
475
+ static char LegendContentLayoutModeKey;
476
+ static char LegendTitlebarControlMetadataKey;
477
+ static char LegendToolbarControlMetadataKey;
478
+ static char LegendToolbarWindowIdentifierKey;
479
+ static char LegendToolbarSearchFieldKey;
480
+ static char LegendToolbarSearchItemKey;
481
+ static char LegendToolbarSearchWidthKey;
482
+ static char LegendToolbarSearchButtonKey;
483
+ static char LegendToolbarSearchContainerKey;
484
+ static char LegendToolbarSearchWidthConstraintKey;
485
+ static const CGFloat LegendToolbarSearchCollapsedSize = 34;
486
+ static const CGFloat LegendToolbarIconControlWidth = 36;
487
+
488
+ // AppKit synchronizes all toolbars with the same identifier as a family.
489
+ // Our windows own independent item state, including across React reloads.
490
+ static NSToolbar *LegendCreateToolbar(NSString *windowIdentifier)
491
+ {
492
+ NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:
493
+ [@"LegendToolbar." stringByAppendingString:NSUUID.UUID.UUIDString]];
494
+ objc_setAssociatedObject(toolbar, &LegendToolbarWindowIdentifierKey,
495
+ windowIdentifier ?: @"", OBJC_ASSOCIATION_COPY_NONATOMIC);
496
+ toolbar.displayMode = NSToolbarDisplayModeIconOnly;
497
+ toolbar.showsBaselineSeparator = NO;
498
+ return toolbar;
499
+ }
500
+
501
+ static NSString *LegendToolbarWindowIdentifier(NSToolbar *toolbar)
502
+ {
503
+ return objc_getAssociatedObject(toolbar, &LegendToolbarWindowIdentifierKey) ?: @"";
504
+ }
505
+
506
+ static RCTUIView *LegendManagedRootView(NSWindow *window)
507
+ {
508
+ id associatedRootView = objc_getAssociatedObject(window, &LegendManagedRootViewKey);
509
+ if ([associatedRootView isKindOfClass:RCTUIView.class]) {
510
+ return associatedRootView;
511
+ }
512
+
513
+ if ([window.contentView isKindOfClass:RCTUIView.class]) {
514
+ return (RCTUIView *)window.contentView;
515
+ }
516
+
517
+ return nil;
518
+ }
519
+
520
+ static void LegendEnsureRootViewContainer(NSWindow *window, RCTUIView *rootView)
521
+ {
522
+ if (!window || !rootView || window.contentView != rootView) {
523
+ return;
524
+ }
525
+
526
+ NSView *containerView = [[NSView alloc] initWithFrame:rootView.frame];
527
+ containerView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
528
+ LegendApplyBackgroundColorToView(containerView, window.backgroundColor);
529
+
530
+ window.contentView = containerView;
531
+ [containerView addSubview:rootView];
532
+ objc_setAssociatedObject(window, &LegendManagedRootViewKey, rootView, OBJC_ASSOCIATION_ASSIGN);
533
+ }
534
+
535
+ static void LegendApplyContentLayoutModeOption(NSWindow *window, NSString *contentLayoutMode)
536
+ {
537
+ if (!window || ![contentLayoutMode isKindOfClass:NSString.class]) {
538
+ return;
539
+ }
540
+
541
+ objc_setAssociatedObject(window, &LegendContentLayoutModeKey, contentLayoutMode, OBJC_ASSOCIATION_COPY_NONATOMIC);
542
+ }
543
+
544
+ static BOOL LegendUsesFullSizeContentLayout(NSWindow *window)
545
+ {
546
+ id contentLayoutMode = objc_getAssociatedObject(window, &LegendContentLayoutModeKey);
547
+ return [contentLayoutMode isKindOfClass:NSString.class] && [(NSString *)contentLayoutMode isEqualToString:@"fullSize"];
548
+ }
549
+
550
+ static void LegendPrepareWindowForDisplay(NSWindow *window, NSString *backgroundColor)
551
+ {
552
+ LegendApplyWindowBackgroundColor(window, backgroundColor);
553
+ [window.contentView displayIfNeeded];
554
+ [window displayIfNeeded];
555
+ }
556
+
557
+ static void LegendApplyContentLayoutMode(NSWindow *window, NSNumber *maskNumber, BOOL usesTitlebarBackground)
558
+ {
559
+ if (usesTitlebarBackground) {
560
+ window.styleMask = window.styleMask | NSWindowStyleMaskFullSizeContentView;
561
+ return;
562
+ }
563
+
564
+ if (maskNumber && (maskNumber.unsignedIntegerValue & NSWindowStyleMaskFullSizeContentView) == 0) {
565
+ window.styleMask = window.styleMask & ~NSWindowStyleMaskFullSizeContentView;
566
+ }
567
+ }
568
+
569
+ static BOOL LegendDictionaryHasKey(NSDictionary *dictionary, NSString *key)
570
+ {
571
+ return [dictionary isKindOfClass:NSDictionary.class] && dictionary[key] != nil;
572
+ }
573
+
574
+ static void LegendSizeRootViewToWindow(RCTUIView *rootView, NSWindow *window);
575
+
576
+ static NSURL *LegendURLFromString(NSString *value)
577
+ {
578
+ if (![value isKindOfClass:NSString.class] || value.length == 0) {
579
+ return nil;
580
+ }
581
+
582
+ NSURL *url = [NSURL URLWithString:value];
583
+ if (url.scheme.length > 0) {
584
+ return url;
585
+ }
586
+
587
+ return [NSURL fileURLWithPath:value];
588
+ }
589
+
590
+ static void LegendApplyWindowTitleAndRepresentedURL(NSWindow *window,
591
+ NSString *title,
592
+ BOOL hasRepresentedURL,
593
+ id representedURLValue,
594
+ NSString *fallbackTitle)
595
+ {
596
+ NSURL *representedURL = [representedURLValue isKindOfClass:NSString.class]
597
+ ? LegendURLFromString((NSString *)representedURLValue)
598
+ : nil;
599
+
600
+ if (hasRepresentedURL) {
601
+ if ([representedURLValue isKindOfClass:NSNull.class]) {
602
+ window.representedURL = nil;
603
+ } else if (representedURL.fileURL && representedURL.path.length > 0) {
604
+ window.representedURL = representedURL;
605
+ } else {
606
+ window.representedURL = representedURL;
607
+ }
608
+ }
609
+
610
+ if (!title && representedURL.lastPathComponent.length > 0) {
611
+ title = representedURL.lastPathComponent;
612
+ }
613
+ window.title = title ?: fallbackTitle ?: @"";
614
+ }
615
+
616
+ static void LegendApplyWindowOptions(NSWindow *window, NSDictionary *options)
617
+ {
618
+ if (!window || ![options isKindOfClass:NSDictionary.class]) {
619
+ return;
620
+ }
621
+
622
+ BOOL hasTitle = LegendDictionaryHasKey(options, @"title");
623
+ BOOL hasRepresentedURL = LegendDictionaryHasKey(options, @"representedURL");
624
+ id representedURLValue = hasRepresentedURL ? options[@"representedURL"] : nil;
625
+ NSString *title = [options[@"title"] isKindOfClass:NSString.class] ? options[@"title"] : nil;
626
+ if (hasTitle || hasRepresentedURL) {
627
+ LegendApplyWindowTitleAndRepresentedURL(window, title, hasRepresentedURL, representedURLValue, nil);
628
+ }
629
+
630
+ NSDictionary *windowStyle = [options[@"windowStyle"] isKindOfClass:NSDictionary.class] ? options[@"windowStyle"] : @{};
631
+ if (windowStyle[@"startupSplitView"] == NSNull.null && LegendFinishSidebarSplitViewStartup) {
632
+ LegendFinishSidebarSplitViewStartup(window);
633
+ }
634
+ NSNumber *maskNumber = [windowStyle[@"mask"] isKindOfClass:NSNumber.class] ? windowStyle[@"mask"] : nil;
635
+ NSNumber *transparentTitlebar = [windowStyle[@"titlebarAppearsTransparent"] isKindOfClass:NSNumber.class]
636
+ ? windowStyle[@"titlebarAppearsTransparent"]
637
+ : nil;
638
+ NSString *titleVisibility = [windowStyle[@"titleVisibility"] isKindOfClass:NSString.class]
639
+ ? windowStyle[@"titleVisibility"]
640
+ : nil;
641
+ NSString *toolbarStyle = [windowStyle[@"toolbarStyle"] isKindOfClass:NSString.class] ? windowStyle[@"toolbarStyle"] : nil;
642
+ NSString *contentLayoutMode = [windowStyle[@"contentLayoutMode"] isKindOfClass:NSString.class]
643
+ ? windowStyle[@"contentLayoutMode"]
644
+ : nil;
645
+ NSString *titlebarSeparatorStyle = [windowStyle[@"titlebarSeparatorStyle"] isKindOfClass:NSString.class]
646
+ ? windowStyle[@"titlebarSeparatorStyle"]
647
+ : nil;
648
+ NSString *backgroundColor = [windowStyle[@"backgroundColor"] isKindOfClass:NSString.class]
649
+ ? windowStyle[@"backgroundColor"]
650
+ : nil;
651
+ NSString *appearance = [windowStyle[@"appearance"] isKindOfClass:NSString.class]
652
+ ? windowStyle[@"appearance"]
653
+ : nil;
654
+ BOOL hasToolbarKey = LegendDictionaryHasKey(windowStyle, @"hasToolbar");
655
+ BOOL hasToolbar = [windowStyle[@"hasToolbar"] boolValue];
656
+ BOOL usesTitlebarBackground = transparentTitlebar.boolValue && backgroundColor.length > 0;
657
+
658
+ if (maskNumber) window.styleMask = maskNumber.unsignedIntegerValue;
659
+ if (options[@"level"]) window.level = [options[@"level"] integerValue];
660
+ if (windowStyle[@"trafficLights"]) for (NSNumber *button in @[@(NSWindowCloseButton), @(NSWindowMiniaturizeButton), @(NSWindowZoomButton)]) [window standardWindowButton:(NSWindowButton)button.integerValue].hidden = ![windowStyle[@"trafficLights"] boolValue];
661
+ if (windowStyle[@"panelStyle"]) {
662
+ NSWindowStyleMask mask = window.styleMask & ~(NSWindowStyleMaskUtilityWindow | NSWindowStyleMaskDocModalWindow | NSWindowStyleMaskNonactivatingPanel);
663
+ NSString *panel = windowStyle[@"panelStyle"];
664
+ window.styleMask = mask | ([panel isEqual:@"utility"] ? NSWindowStyleMaskUtilityWindow : [panel isEqual:@"documentModal"] ? NSWindowStyleMaskDocModalWindow : NSWindowStyleMaskNonactivatingPanel);
665
+ }
666
+ LegendApplyContentLayoutModeOption(window, contentLayoutMode);
667
+ LegendApplyContentLayoutMode(window, maskNumber, usesTitlebarBackground);
668
+ if (transparentTitlebar != nil) {
669
+ window.titlebarAppearsTransparent = transparentTitlebar.boolValue;
670
+ }
671
+ LegendApplyTitleVisibility(window, titleVisibility);
672
+ if (hasToolbarKey) {
673
+ if (hasToolbar && !window.toolbar) {
674
+ NSToolbar *toolbar = LegendCreateToolbar([options[@"identifier"] isKindOfClass:NSString.class] ? options[@"identifier"] : @"main");
675
+ window.toolbar = toolbar;
676
+ } else if (!hasToolbar) {
677
+ window.toolbar = nil;
678
+ }
679
+ }
680
+ LegendApplyToolbarStyle(window, toolbarStyle);
681
+ LegendApplyTitlebarSeparatorStyle(window, titlebarSeparatorStyle);
682
+ LegendApplyWindowAppearance(window, appearance);
683
+ LegendApplyWindowBackgroundColor(window, backgroundColor);
684
+
685
+ RCTUIView *rootView = LegendManagedRootView(window);
686
+ if (usesTitlebarBackground && rootView) {
687
+ LegendEnsureRootViewContainer(window, rootView);
688
+ LegendSizeRootViewToWindow(rootView, window);
689
+ }
690
+ }
691
+
692
+ static void LegendSizeRootViewToWindow(RCTUIView *rootView, NSWindow *window)
693
+ {
694
+ if (!rootView || !window) {
695
+ return;
696
+ }
697
+
698
+ NSView *contentView = window.contentView;
699
+ if (contentView && contentView != rootView && (window.styleMask & NSWindowStyleMaskFullSizeContentView) != 0) {
700
+ rootView.frame = LegendUsesFullSizeContentLayout(window)
701
+ ? contentView.bounds
702
+ : [contentView convertRect:window.contentLayoutRect fromView:nil];
703
+ } else {
704
+ rootView.frame = contentView ? contentView.bounds : NSMakeRect(0, 0, window.frame.size.width, window.frame.size.height);
705
+ }
706
+ rootView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
707
+ [rootView setNeedsLayout:YES];
708
+ }
709
+
710
+ extern "C" void LegendPrecreateRestorableWindows(void)
711
+ {
712
+ if (LegendPrecreatedWindows != nil) {
713
+ return;
714
+ }
715
+
716
+ LegendPrecreatedWindows = [NSMutableDictionary new];
717
+ for (NSDictionary *options in LegendReadRestorableWindowOptions()) {
718
+ NSString *identifier = [options[@"identifier"] isKindOfClass:NSString.class] ? options[@"identifier"] : nil;
719
+ if (identifier.length == 0 || ![options[@"restoreOnLaunch"] boolValue]) {
720
+ continue;
721
+ }
722
+
723
+ NSDictionary *windowStyle = [options[@"windowStyle"] isKindOfClass:NSDictionary.class]
724
+ ? options[@"windowStyle"]
725
+ : @{};
726
+ NSNumber *maskNumber = [windowStyle[@"mask"] isKindOfClass:NSNumber.class] ? windowStyle[@"mask"] : nil;
727
+ NSNumber *widthNumber = [windowStyle[@"width"] isKindOfClass:NSNumber.class]
728
+ ? windowStyle[@"width"]
729
+ : ([options[@"width"] isKindOfClass:NSNumber.class] ? options[@"width"] : nil);
730
+ NSNumber *heightNumber = [windowStyle[@"height"] isKindOfClass:NSNumber.class]
731
+ ? windowStyle[@"height"]
732
+ : ([options[@"height"] isKindOfClass:NSNumber.class] ? options[@"height"] : nil);
733
+ CGFloat width = widthNumber ? MAX(widthNumber.doubleValue, 1) : 400;
734
+ CGFloat height = heightNumber ? MAX(heightNumber.doubleValue, 1) : 300;
735
+ NSUInteger styleMask = maskNumber
736
+ ? maskNumber.unsignedIntegerValue
737
+ : (NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable);
738
+ NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, width, height)
739
+ styleMask:styleMask
740
+ backing:NSBackingStoreBuffered
741
+ defer:NO];
742
+ window.releasedWhenClosed = NO;
743
+ LegendApplyWindowOptions(window, options);
744
+
745
+ NSNumber *levelNumber = [options[@"level"] isKindOfClass:NSNumber.class] ? options[@"level"] : nil;
746
+ if (levelNumber) {
747
+ window.level = levelNumber.integerValue;
748
+ }
749
+ NSNumber *hasShadow = [options[@"hasShadow"] isKindOfClass:NSNumber.class] ? options[@"hasShadow"] : nil;
750
+ if (hasShadow) {
751
+ window.hasShadow = hasShadow.boolValue;
752
+ }
753
+ BOOL transparentBackground = [options[@"transparentBackground"] boolValue];
754
+ NSString *backgroundColor = [windowStyle[@"backgroundColor"] isKindOfClass:NSString.class]
755
+ ? windowStyle[@"backgroundColor"]
756
+ : nil;
757
+ NSColor *startupBackgroundColor = transparentBackground
758
+ ? NSColor.clearColor
759
+ : LegendWindowStartupBackgroundColor(window, backgroundColor);
760
+ window.backgroundColor = startupBackgroundColor;
761
+ window.opaque = !transparentBackground && startupBackgroundColor.alphaComponent >= 1;
762
+
763
+ NSView *placeholder = [[NSView alloc] initWithFrame:window.contentView.bounds];
764
+ placeholder.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
765
+ LegendApplyBackgroundColorToView(placeholder, startupBackgroundColor);
766
+ window.contentView = placeholder;
767
+
768
+ NSString *autosaveName = LegendManagedWindowFrameAutosaveName(identifier);
769
+ [window setFrameAutosaveName:autosaveName];
770
+ if (![window setFrameUsingName:autosaveName]) {
771
+ NSNumber *originX = [options[@"x"] isKindOfClass:NSNumber.class] ? options[@"x"] : nil;
772
+ NSNumber *originY = [options[@"y"] isKindOfClass:NSNumber.class] ? options[@"y"] : nil;
773
+ if (originX || originY) {
774
+ NSPoint origin = window.frame.origin;
775
+ origin.x = originX ? originX.doubleValue : origin.x;
776
+ origin.y = originY ? originY.doubleValue : origin.y;
777
+ [window setFrameOrigin:origin];
778
+ } else {
779
+ [window center];
780
+ }
781
+ }
782
+
783
+ if (LegendPrepareSidebarSplitViewStartup) {
784
+ LegendPrepareSidebarSplitViewStartup(window, windowStyle[@"startupSplitView"]);
785
+ }
786
+ LegendPrecreatedWindows[identifier] = window;
787
+ if (![options[@"deferOrderFront"] boolValue]) {
788
+ [window makeKeyAndOrderFront:nil];
789
+ if (levelNumber) {
790
+ [window orderFrontRegardless];
791
+ }
792
+ }
793
+ }
794
+ }
795
+ #endif
796
+
797
+ @interface RNWindowManager ()
798
+ #if TARGET_OS_OSX
799
+ <NSWindowDelegate, NSSearchFieldDelegate, NSToolbarDelegate>
800
+ #endif
801
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, id> *windows;
802
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, RCTUIView *> *rootViews;
803
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, NSString *> *moduleNames;
804
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, NSArray<NSTitlebarAccessoryViewController *> *> *titlebarAccessoryControllers;
805
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, NSView *> *titlebarMaterialViews;
806
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, NSArray<NSDictionary *> *> *toolbarItemConfigs;
807
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, CIFilter *> *windowBlurFilters;
808
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, NSDictionary *> *windowOptions;
809
+ @property (nonatomic, strong) NSMutableSet<NSString *> *closeRequestIdentifiers;
810
+ @property (nonatomic, strong, nullable) id displaySleepActivity;
811
+ @property (nonatomic, assign) BOOL hasListeners;
812
+ @property (nonatomic, assign) BOOL mainWindowObserversInstalled;
813
+ #if TARGET_OS_OSX
814
+ - (void)sendToolbarSearchEventForField:(NSSearchField *)searchField submitted:(BOOL)submitted shiftKey:(BOOL)shiftKey;
815
+ - (void)recordWindowOptions:(NSDictionary *)options identifier:(NSString *)identifier moduleName:(NSString *)moduleName;
816
+ #endif
817
+ @end
818
+
819
+ @implementation RNWindowManager
820
+
821
+ RCT_EXPORT_MODULE(NativeWindowManager)
822
+
823
+ + (BOOL)requiresMainQueueSetup
824
+ {
825
+ return YES;
826
+ }
827
+
828
+ - (instancetype)init
829
+ {
830
+ if (self = [super init]) {
831
+ #if TARGET_OS_OSX
832
+ LegendWindowRegistry *registry = LegendWindowRegistry.sharedRegistry;
833
+ _windows = (id)registry.windows;
834
+ _rootViews = (id)registry.rootViews;
835
+ _moduleNames = registry.moduleNames;
836
+ _titlebarAccessoryControllers = registry.titlebarAccessoryControllers;
837
+ _titlebarMaterialViews = registry.titlebarMaterialViews;
838
+ _toolbarItemConfigs = registry.toolbarItemConfigs;
839
+ _windowBlurFilters = registry.windowBlurFilters;
840
+ _windowOptions = registry.windowOptions;
841
+ _closeRequestIdentifiers = registry.closeRequestIdentifiers;
842
+ for (NSString *identifier in _moduleNames.allKeys) {
843
+ NSWindow *window = _windows[identifier];
844
+ SparkRebindWindowDelegate(window, identifier, self);
845
+ [self bindChromeForWindow:window];
846
+ }
847
+ [self bindChromeForWindow:[RNWindowManager getMainWindow]];
848
+ #else
849
+ _windows = [NSMutableDictionary new];
850
+ _rootViews = [NSMutableDictionary new];
851
+ _moduleNames = [NSMutableDictionary new];
852
+ _titlebarAccessoryControllers = [NSMutableDictionary new];
853
+ _titlebarMaterialViews = [NSMutableDictionary new];
854
+ _toolbarItemConfigs = [NSMutableDictionary new];
855
+ _windowBlurFilters = [NSMutableDictionary new];
856
+ _windowOptions = [NSMutableDictionary new];
857
+ _closeRequestIdentifiers = [NSMutableSet new];
858
+ #endif
859
+ #if TARGET_OS_OSX
860
+ #if __has_include(<RNDesktopApp/SparkDesktop.h>)
861
+ [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(sparkDesktopEvent:) name:SparkDesktopEvent object:nil];
862
+ #endif
863
+ [NSNotificationCenter.defaultCenter addObserver:self
864
+ selector:@selector(applicationReopenRequested:)
865
+ name:LegendApplicationReopenRequestedNotification
866
+ object:nil];
867
+ [NSNotificationCenter.defaultCenter addObserver:self
868
+ selector:@selector(mainWindowCloseRequested:)
869
+ name:LegendMainWindowCloseRequestedNotification
870
+ object:nil];
871
+ [NSNotificationCenter.defaultCenter addObserver:self
872
+ selector:@selector(applicationWillTerminate:)
873
+ name:NSApplicationWillTerminateNotification
874
+ object:nil];
875
+ [NSNotificationCenter.defaultCenter addObserver:self
876
+ selector:@selector(windowDidBecomeKey:)
877
+ name:NSWindowDidBecomeKeyNotification
878
+ object:nil];
879
+ [NSNotificationCenter.defaultCenter addObserver:self
880
+ selector:@selector(screensChanged:)
881
+ name:NSApplicationDidChangeScreenParametersNotification
882
+ object:nil];
883
+ #endif
884
+ }
885
+ return self;
886
+ }
887
+
888
+ #if TARGET_OS_OSX
889
+ - (void)bindChromeControlView:(NSView *)view
890
+ {
891
+ BOOL managed = objc_getAssociatedObject(view, &LegendToolbarControlMetadataKey) != nil ||
892
+ objc_getAssociatedObject(view, &LegendTitlebarControlMetadataKey) != nil ||
893
+ objc_getAssociatedObject(view, &LegendToolbarSearchItemKey) != nil;
894
+ if (managed && [view isKindOfClass:NSControl.class]) {
895
+ ((NSControl *)view).target = self;
896
+ if ([view isKindOfClass:NSSearchField.class]) ((NSSearchField *)view).delegate = self;
897
+ }
898
+ for (NSView *child in view.subviews) [self bindChromeControlView:child];
899
+ }
900
+
901
+ - (void)bindChromeForWindow:(NSWindow *)window
902
+ {
903
+ NSToolbar *toolbar = window.toolbar;
904
+ if (objc_getAssociatedObject(toolbar, &LegendToolbarWindowIdentifierKey)) {
905
+ toolbar.delegate = self;
906
+ for (NSToolbarItem *item in toolbar.items) {
907
+ if (objc_getAssociatedObject(item, &LegendToolbarControlMetadataKey)) item.target = self;
908
+ [self bindChromeControlView:item.view];
909
+ }
910
+ }
911
+ for (NSTitlebarAccessoryViewController *controller in window.titlebarAccessoryViewControllers) {
912
+ [self bindChromeControlView:controller.view];
913
+ }
914
+ }
915
+ #endif
916
+
917
+ - (NSArray<NSString *> *)supportedEvents
918
+ {
919
+ return @[
920
+ @"onWindowClosed",
921
+ @"onWindowCloseRequested",
922
+ @"onMainWindowMoved",
923
+ @"onMainWindowResized",
924
+ @"onWindowMoved",
925
+ @"onWindowResized",
926
+ @"onApplicationReopenRequested",
927
+ @"onWindowFocused",
928
+ @"onTitlebarControlPressed",
929
+ @"onToolbarItemSelected",
930
+ @"onToolbarSearch",
931
+ @"onDisplaysChanged",
932
+ ];
933
+ }
934
+
935
+ - (void)startObserving
936
+ {
937
+ self.hasListeners = YES;
938
+ #if TARGET_OS_OSX
939
+ [self setupMainWindowObservers];
940
+ #endif
941
+ }
942
+
943
+ - (void)stopObserving
944
+ {
945
+ self.hasListeners = NO;
946
+ // Native observers belong to this module, not a JS subscription. React may
947
+ // briefly remove every listener while effects update or a document closes.
948
+ // Keep lifecycle/restoration tracking alive until dealloc; event emission is
949
+ // already gated by hasListeners.
950
+ }
951
+
952
+ - (dispatch_queue_t)methodQueue
953
+ {
954
+ return dispatch_get_main_queue();
955
+ }
956
+
957
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
958
+ (const facebook::react::ObjCTurboModule::InitParams &)params
959
+ {
960
+ return std::make_shared<facebook::react::NativeWindowManagerSpecJSI>(params);
961
+ }
962
+
963
+ - (NSDictionary *)parseObjectJSON:(NSString *)json
964
+ {
965
+ NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding];
966
+ if (!data) {
967
+ return @{};
968
+ }
969
+
970
+ id value = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
971
+ return [value isKindOfClass:NSDictionary.class] ? value : @{};
972
+ }
973
+
974
+ - (NSString *)jsonStringFromObject:(id)object
975
+ {
976
+ id value = object ?: [NSNull null];
977
+ NSData *data = [NSJSONSerialization dataWithJSONObject:value options:0 error:nil];
978
+ return data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] : @"null";
979
+ }
980
+
981
+ #if TARGET_OS_OSX
982
+ - (void)removeTitlebarMaterialForIdentifier:(NSString *)identifier
983
+ {
984
+ NSView *materialView = self.titlebarMaterialViews[identifier];
985
+ [materialView removeFromSuperview];
986
+ [self.titlebarMaterialViews removeObjectForKey:identifier];
987
+ }
988
+
989
+ - (NSView *)createTitlebarMaterialView:(NSString *)material frame:(NSRect)frame
990
+ {
991
+ if ([material isEqualToString:@"glass"]) {
992
+ if (@available(macOS 26.0, *)) {
993
+ return LegendCreateOverscannedGlassEffectView(frame);
994
+ }
995
+ }
996
+
997
+ NSVisualEffectView *effectView = [[NSVisualEffectView alloc] initWithFrame:frame];
998
+ effectView.material = LegendVisualEffectMaterialForTitlebarMaterial(material);
999
+ effectView.blendingMode = NSVisualEffectBlendingModeBehindWindow;
1000
+ effectView.state = NSVisualEffectStateFollowsWindowActiveState;
1001
+ effectView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
1002
+ effectView.wantsLayer = YES;
1003
+ effectView.layer.backgroundColor = NSColor.clearColor.CGColor;
1004
+ return effectView;
1005
+ }
1006
+
1007
+ - (void)applyTitlebarMaterialFromOptions:(NSDictionary *)options toWindow:(NSWindow *)window identifier:(NSString *)identifier
1008
+ {
1009
+ NSDictionary *windowStyle = [options[@"windowStyle"] isKindOfClass:NSDictionary.class] ? options[@"windowStyle"] : @{};
1010
+ if (!LegendDictionaryHasKey(windowStyle, @"titlebarMaterial")) {
1011
+ return;
1012
+ }
1013
+
1014
+ NSString *material = [windowStyle[@"titlebarMaterial"] isKindOfClass:NSString.class] ? windowStyle[@"titlebarMaterial"] : nil;
1015
+ [self removeTitlebarMaterialForIdentifier:identifier];
1016
+
1017
+ if (material.length == 0 || [material isEqualToString:@"none"]) {
1018
+ return;
1019
+ }
1020
+
1021
+ NSView *frameView = window.contentView.superview;
1022
+ if (!frameView) {
1023
+ return;
1024
+ }
1025
+
1026
+ NSView *materialView = [self createTitlebarMaterialView:material frame:LegendTitlebarMaterialFrame(window, frameView)];
1027
+ if ([materialView isKindOfClass:NSVisualEffectView.class]) {
1028
+ NSVisualEffectView *effectView = (NSVisualEffectView *)materialView;
1029
+ NSString *blendingMode = [windowStyle[@"titlebarMaterialBlendingMode"] isKindOfClass:NSString.class]
1030
+ ? windowStyle[@"titlebarMaterialBlendingMode"]
1031
+ : nil;
1032
+ NSString *state = [windowStyle[@"titlebarMaterialState"] isKindOfClass:NSString.class]
1033
+ ? windowStyle[@"titlebarMaterialState"]
1034
+ : nil;
1035
+ effectView.blendingMode = LegendVisualEffectBlendingModeForName(blendingMode);
1036
+ effectView.state = LegendVisualEffectStateForName(state);
1037
+ }
1038
+
1039
+ materialView.translatesAutoresizingMaskIntoConstraints = YES;
1040
+ materialView.autoresizingMask = NSViewWidthSizable | NSViewMinYMargin;
1041
+ [frameView addSubview:materialView positioned:NSWindowAbove relativeTo:window.contentView];
1042
+ self.titlebarMaterialViews[identifier] = materialView;
1043
+ }
1044
+
1045
+ - (void)removeTitlebarControlsForIdentifier:(NSString *)identifier fromWindow:(NSWindow *)window
1046
+ {
1047
+ NSArray<NSTitlebarAccessoryViewController *> *controllers = self.titlebarAccessoryControllers[identifier] ?: @[];
1048
+ for (NSTitlebarAccessoryViewController *controller in controllers) {
1049
+ NSUInteger index = [window.titlebarAccessoryViewControllers indexOfObjectIdenticalTo:controller];
1050
+ if (index != NSNotFound) {
1051
+ [window removeTitlebarAccessoryViewControllerAtIndex:index];
1052
+ }
1053
+ }
1054
+ [self.titlebarAccessoryControllers removeObjectForKey:identifier];
1055
+ }
1056
+
1057
+ - (void)applyTitlebarControlsFromOptions:(NSDictionary *)options toWindow:(NSWindow *)window identifier:(NSString *)identifier
1058
+ {
1059
+ NSDictionary *windowStyle = [options[@"windowStyle"] isKindOfClass:NSDictionary.class] ? options[@"windowStyle"] : @{};
1060
+ if (!LegendDictionaryHasKey(windowStyle, @"titlebarControls")) {
1061
+ return;
1062
+ }
1063
+
1064
+ [self removeTitlebarControlsForIdentifier:identifier fromWindow:window];
1065
+
1066
+ NSArray *titlebarControls = [windowStyle[@"titlebarControls"] isKindOfClass:NSArray.class]
1067
+ ? windowStyle[@"titlebarControls"]
1068
+ : @[];
1069
+ NSMutableArray<NSTitlebarAccessoryViewController *> *controllers = [NSMutableArray new];
1070
+
1071
+ for (id controlCandidate in titlebarControls) {
1072
+ if (![controlCandidate isKindOfClass:NSDictionary.class]) {
1073
+ continue;
1074
+ }
1075
+
1076
+ NSDictionary *control = (NSDictionary *)controlCandidate;
1077
+ NSString *type = [control[@"type"] isKindOfClass:NSString.class] ? control[@"type"] : nil;
1078
+ NSString *controlId = [control[@"id"] isKindOfClass:NSString.class] ? control[@"id"] : nil;
1079
+ if (![type isEqualToString:@"button"] || controlId.length == 0) {
1080
+ continue;
1081
+ }
1082
+
1083
+ NSString *label = [control[@"label"] isKindOfClass:NSString.class] ? control[@"label"] : controlId;
1084
+ NSString *systemImageName = [control[@"systemImageName"] isKindOfClass:NSString.class] ? control[@"systemImageName"] : nil;
1085
+ NSImage *image = nil;
1086
+ if (systemImageName.length > 0) {
1087
+ if (@available(macOS 11.0, *)) {
1088
+ image = [NSImage imageWithSystemSymbolName:systemImageName accessibilityDescription:label];
1089
+ }
1090
+ }
1091
+
1092
+ NSSegmentedControl *segmentedControl = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect];
1093
+ segmentedControl.segmentCount = 1;
1094
+ segmentedControl.segmentStyle = NSSegmentStyleSeparated;
1095
+ segmentedControl.trackingMode = NSSegmentSwitchTrackingSelectAny;
1096
+ [segmentedControl setSelected:[control[@"selected"] boolValue] forSegment:0];
1097
+ segmentedControl.target = self;
1098
+ segmentedControl.action = @selector(titlebarControlPressed:);
1099
+ segmentedControl.controlSize = NSControlSizeRegular;
1100
+ [segmentedControl setEnabled:LegendDictionaryHasKey(control, @"enabled") ? [control[@"enabled"] boolValue] : YES forSegment:0];
1101
+ if (image) {
1102
+ [segmentedControl setImage:image forSegment:0];
1103
+ [segmentedControl setImageScaling:NSImageScaleProportionallyDown forSegment:0];
1104
+ } else {
1105
+ [segmentedControl setLabel:label forSegment:0];
1106
+ }
1107
+ if (@available(macOS 10.13, *)) {
1108
+ [segmentedControl setToolTip:[control[@"tooltip"] isKindOfClass:NSString.class] ? control[@"tooltip"] : label forSegment:0];
1109
+ }
1110
+ CGFloat segmentWidth = image ? 36 : MAX(84, [label sizeWithAttributes:@{NSFontAttributeName: segmentedControl.font ?: [NSFont systemFontOfSize:NSFont.systemFontSize]}].width + 24);
1111
+ [segmentedControl setWidth:segmentWidth forSegment:0];
1112
+ segmentedControl.frame = NSMakeRect(0, 0, segmentWidth, MAX(28, segmentedControl.fittingSize.height));
1113
+ objc_setAssociatedObject(segmentedControl, &LegendTitlebarControlMetadataKey, @{
1114
+ @"controlId": controlId,
1115
+ @"windowIdentifier": identifier ?: @"",
1116
+ @"instanceId": objc_getAssociatedObject(window, NSSelectorFromString(@"sparkWindowInstanceId")) ?: @"",
1117
+ @"selected": control[@"selected"] ?: @NO,
1118
+ }, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1119
+
1120
+ NSTitlebarAccessoryViewController *controller = [NSTitlebarAccessoryViewController new];
1121
+ controller.view = segmentedControl;
1122
+ NSString *placement = [control[@"placement"] isKindOfClass:NSString.class] ? control[@"placement"] : @"left";
1123
+ controller.layoutAttribute = [placement isEqualToString:@"right"] ? NSLayoutAttributeRight : NSLayoutAttributeLeft;
1124
+ [window addTitlebarAccessoryViewController:controller];
1125
+ [controllers addObject:controller];
1126
+ }
1127
+
1128
+ self.titlebarAccessoryControllers[identifier] = controllers;
1129
+ }
1130
+
1131
+ - (void)titlebarControlPressed:(id)sender
1132
+ {
1133
+ id metadata = objc_getAssociatedObject(sender, &LegendTitlebarControlMetadataKey);
1134
+ NSDictionary *representedObject = [metadata isKindOfClass:NSDictionary.class]
1135
+ ? metadata
1136
+ : @{};
1137
+ NSString *identifier = [representedObject[@"windowIdentifier"] isKindOfClass:NSString.class]
1138
+ ? representedObject[@"windowIdentifier"]
1139
+ : @"";
1140
+ NSString *controlId = [representedObject[@"controlId"] isKindOfClass:NSString.class]
1141
+ ? representedObject[@"controlId"]
1142
+ : @"";
1143
+
1144
+ if ([sender isKindOfClass:NSSegmentedControl.class]) [(NSSegmentedControl *)sender setSelected:[representedObject[@"selected"] boolValue] forSegment:0];
1145
+ [self sendWindowEventWithName:@"onTitlebarControlPressed"
1146
+ body:@{@"identifier": identifier, @"controlId": controlId, @"instanceId": representedObject[@"instanceId"] ?: @""}];
1147
+ }
1148
+
1149
+ - (NSString *)toolbarItemIdentifierForConfig:(NSDictionary *)config
1150
+ {
1151
+ NSString *itemId = [config[@"id"] isKindOfClass:NSString.class] ? config[@"id"] : nil;
1152
+ return itemId.length > 0 ? [@"legend.toolbar." stringByAppendingString:itemId] : nil;
1153
+ }
1154
+
1155
+ - (NSDictionary *)toolbarItemConfigForIdentifier:(NSToolbarItemIdentifier)itemIdentifier toolbar:(NSToolbar *)toolbar
1156
+ {
1157
+ if (![itemIdentifier hasPrefix:@"legend.toolbar."]) {
1158
+ return nil;
1159
+ }
1160
+
1161
+ NSArray<NSDictionary *> *configs = self.toolbarItemConfigs[toolbar.identifier] ?: @[];
1162
+ for (NSDictionary *config in configs) {
1163
+ NSString *configIdentifier = [self toolbarItemIdentifierForConfig:config];
1164
+ if ([configIdentifier isEqualToString:itemIdentifier]) {
1165
+ return config;
1166
+ }
1167
+ }
1168
+ return nil;
1169
+ }
1170
+
1171
+ - (NSArray<NSToolbarItemIdentifier> *)toolbarItemIdentifiersForToolbar:(NSToolbar *)toolbar
1172
+ {
1173
+ NSMutableArray<NSToolbarItemIdentifier> *leadingIdentifiers = [NSMutableArray new];
1174
+ NSMutableArray<NSToolbarItemIdentifier> *trailingIdentifiers = [NSMutableArray new];
1175
+ NSArray<NSDictionary *> *configs = self.toolbarItemConfigs[toolbar.identifier] ?: @[];
1176
+ for (NSDictionary *config in configs) {
1177
+ NSString *itemIdentifier = [self toolbarItemIdentifierForConfig:config];
1178
+ if (itemIdentifier.length > 0) {
1179
+ NSString *placement = [config[@"placement"] isKindOfClass:NSString.class] ? config[@"placement"] : @"trailing";
1180
+ if ([placement isEqualToString:@"leading"]) {
1181
+ [leadingIdentifiers addObject:itemIdentifier];
1182
+ } else {
1183
+ [trailingIdentifiers addObject:itemIdentifier];
1184
+ }
1185
+ }
1186
+ }
1187
+ NSMutableArray<NSToolbarItemIdentifier> *identifiers = [NSMutableArray new];
1188
+ [identifiers addObjectsFromArray:leadingIdentifiers];
1189
+ [identifiers addObject:NSToolbarFlexibleSpaceItemIdentifier];
1190
+ [identifiers addObjectsFromArray:trailingIdentifiers];
1191
+ return identifiers;
1192
+ }
1193
+
1194
+ - (NSToolbar *)ensureToolbarForWindow:(NSWindow *)window identifier:(NSString *)identifier
1195
+ {
1196
+ NSToolbar *toolbar = window.toolbar;
1197
+ if (!toolbar || !objc_getAssociatedObject(toolbar, &LegendToolbarWindowIdentifierKey)) {
1198
+ toolbar = LegendCreateToolbar(identifier);
1199
+ window.toolbar = toolbar;
1200
+ }
1201
+ toolbar.delegate = self;
1202
+ toolbar.allowsUserCustomization = NO;
1203
+ toolbar.autosavesConfiguration = NO;
1204
+ return toolbar;
1205
+ }
1206
+
1207
+ - (void)applyToolbarItemsFromOptions:(NSDictionary *)options toWindow:(NSWindow *)window identifier:(NSString *)identifier
1208
+ {
1209
+ NSDictionary *windowStyle = [options[@"windowStyle"] isKindOfClass:NSDictionary.class] ? options[@"windowStyle"] : @{};
1210
+ if (!LegendDictionaryHasKey(windowStyle, @"toolbarItems")) {
1211
+ return;
1212
+ }
1213
+
1214
+ NSArray *toolbarItems = [windowStyle[@"toolbarItems"] isKindOfClass:NSArray.class] ? windowStyle[@"toolbarItems"] : @[];
1215
+ NSToolbar *toolbar = [self ensureToolbarForWindow:window identifier:identifier];
1216
+ NSMutableArray<NSDictionary *> *configs = [NSMutableArray new];
1217
+ for (id item in toolbarItems) {
1218
+ if ([item isKindOfClass:NSDictionary.class]) {
1219
+ NSMutableDictionary *config = [item mutableCopy]; config[@"instanceId"] = objc_getAssociatedObject(window, NSSelectorFromString(@"sparkWindowInstanceId")) ?: @""; [configs addObject:config];
1220
+ }
1221
+ }
1222
+ self.toolbarItemConfigs[toolbar.identifier] = configs;
1223
+
1224
+ while (toolbar.items.count > 0) {
1225
+ [toolbar removeItemAtIndex:toolbar.items.count - 1];
1226
+ }
1227
+ for (NSToolbarItemIdentifier itemIdentifier in [self toolbarDefaultItemIdentifiers:toolbar]) {
1228
+ [toolbar insertItemWithItemIdentifier:itemIdentifier atIndex:toolbar.items.count];
1229
+ }
1230
+ }
1231
+
1232
+ - (NSArray<NSToolbarItemIdentifier> *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar
1233
+ {
1234
+ NSMutableArray<NSToolbarItemIdentifier> *identifiers = [[self toolbarItemIdentifiersForToolbar:toolbar] mutableCopy];
1235
+ [identifiers addObject:NSToolbarSpaceItemIdentifier];
1236
+ return identifiers;
1237
+ }
1238
+
1239
+ - (NSArray<NSToolbarItemIdentifier> *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
1240
+ {
1241
+ return [self toolbarItemIdentifiersForToolbar:toolbar];
1242
+ }
1243
+
1244
+ - (NSArray<NSToolbarItemIdentifier> *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
1245
+ {
1246
+ return @[];
1247
+ }
1248
+
1249
+ - (NSImage *)toolbarSearchImageWithLabel:(NSString *)label
1250
+ {
1251
+ if (@available(macOS 11.0, *)) {
1252
+ return [NSImage imageWithSystemSymbolName:@"magnifyingglass" accessibilityDescription:label ?: @"Search"];
1253
+ }
1254
+ return [NSImage imageNamed:NSImageNameTouchBarSearchTemplate];
1255
+ }
1256
+
1257
+ - (void)focusToolbarSearchField:(NSSearchField *)searchField
1258
+ {
1259
+ id metadata = objc_getAssociatedObject(searchField, &LegendToolbarControlMetadataKey);
1260
+ NSDictionary *representedObject = [metadata isKindOfClass:NSDictionary.class]
1261
+ ? metadata
1262
+ : @{};
1263
+ NSString *identifier = [representedObject[@"windowIdentifier"] isKindOfClass:NSString.class]
1264
+ ? representedObject[@"windowIdentifier"]
1265
+ : @"";
1266
+ NSWindow *window = (NSWindow *)self.windows[identifier];
1267
+ [window makeKeyAndOrderFront:nil];
1268
+ [window makeFirstResponder:searchField];
1269
+ }
1270
+
1271
+ - (void)setToolbarSearchItem:(NSToolbarItem *)toolbarItem
1272
+ expanded:(BOOL)expanded
1273
+ value:(NSString *)value
1274
+ focus:(BOOL)focus
1275
+ sendChange:(BOOL)sendChange
1276
+ animated:(BOOL)animated
1277
+ {
1278
+ if (!toolbarItem) {
1279
+ return;
1280
+ }
1281
+
1282
+ id associatedField = objc_getAssociatedObject(toolbarItem, &LegendToolbarSearchFieldKey);
1283
+ id associatedButton = objc_getAssociatedObject(toolbarItem, &LegendToolbarSearchButtonKey);
1284
+ id associatedContainer = objc_getAssociatedObject(toolbarItem, &LegendToolbarSearchContainerKey);
1285
+ id associatedWidthConstraint = objc_getAssociatedObject(toolbarItem, &LegendToolbarSearchWidthConstraintKey);
1286
+ if (![associatedField isKindOfClass:NSSearchField.class]) {
1287
+ return;
1288
+ }
1289
+
1290
+ NSSearchField *searchField = (NSSearchField *)associatedField;
1291
+ NSButton *button = [associatedButton isKindOfClass:NSButton.class] ? (NSButton *)associatedButton : nil;
1292
+ NSView *container = [associatedContainer isKindOfClass:NSView.class] ? (NSView *)associatedContainer : toolbarItem.view;
1293
+ NSLayoutConstraint *widthConstraint = [associatedWidthConstraint isKindOfClass:NSLayoutConstraint.class]
1294
+ ? (NSLayoutConstraint *)associatedWidthConstraint
1295
+ : nil;
1296
+ NSNumber *widthNumber = objc_getAssociatedObject(toolbarItem, &LegendToolbarSearchWidthKey);
1297
+ CGFloat width = [widthNumber isKindOfClass:NSNumber.class] ? widthNumber.doubleValue : 240;
1298
+ CGFloat targetWidth = expanded ? width : LegendToolbarSearchCollapsedSize;
1299
+
1300
+ if (value) {
1301
+ searchField.stringValue = value;
1302
+ }
1303
+ if (expanded) {
1304
+ searchField.hidden = NO;
1305
+ button.hidden = NO;
1306
+ }
1307
+ if (focus) {
1308
+ [self focusToolbarSearchField:searchField];
1309
+ }
1310
+ if (container) {
1311
+ NSRect frame = container.frame;
1312
+ frame.size.width = targetWidth;
1313
+ if (animated) {
1314
+ [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
1315
+ context.duration = 0.18;
1316
+ context.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
1317
+ widthConstraint.animator.constant = targetWidth;
1318
+ container.animator.frame = frame;
1319
+ searchField.animator.alphaValue = expanded ? 1 : 0;
1320
+ button.animator.alphaValue = expanded ? 0 : 1;
1321
+ [container.superview.animator layoutSubtreeIfNeeded];
1322
+ [container.animator layoutSubtreeIfNeeded];
1323
+ } completionHandler:^{
1324
+ button.hidden = expanded;
1325
+ searchField.hidden = !expanded;
1326
+ }];
1327
+ } else {
1328
+ widthConstraint.constant = targetWidth;
1329
+ container.frame = frame;
1330
+ searchField.alphaValue = expanded ? 1 : 0;
1331
+ button.alphaValue = expanded ? 0 : 1;
1332
+ button.hidden = expanded;
1333
+ searchField.hidden = !expanded;
1334
+ [container layoutSubtreeIfNeeded];
1335
+ }
1336
+ }
1337
+ if (sendChange) {
1338
+ [self sendToolbarSearchEventForField:searchField submitted:NO shiftKey:NO];
1339
+ }
1340
+ }
1341
+
1342
+ - (void)collapseToolbarSearchItem:(NSToolbarItem *)toolbarItem animated:(BOOL)animated
1343
+ {
1344
+ [self setToolbarSearchItem:toolbarItem
1345
+ expanded:NO
1346
+ value:nil
1347
+ focus:NO
1348
+ sendChange:NO
1349
+ animated:animated];
1350
+ }
1351
+
1352
+ - (void)expandToolbarSearchItem:(NSToolbarItem *)toolbarItem
1353
+ value:(NSString *)value
1354
+ focus:(BOOL)focus
1355
+ sendChange:(BOOL)sendChange
1356
+ animated:(BOOL)animated
1357
+ {
1358
+ [self setToolbarSearchItem:toolbarItem
1359
+ expanded:YES
1360
+ value:value
1361
+ focus:focus
1362
+ sendChange:sendChange
1363
+ animated:animated];
1364
+ }
1365
+
1366
+ - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
1367
+ itemForItemIdentifier:(NSToolbarItemIdentifier)itemIdentifier
1368
+ willBeInsertedIntoToolbar:(BOOL)flag
1369
+ {
1370
+ NSDictionary *config = [self toolbarItemConfigForIdentifier:itemIdentifier toolbar:toolbar];
1371
+ if (!config) {
1372
+ return nil;
1373
+ }
1374
+
1375
+ NSString *type = [config[@"type"] isKindOfClass:NSString.class] ? config[@"type"] : nil;
1376
+ if ([type isEqualToString:@"button"] || [type isEqualToString:@"menuButton"]) {
1377
+ NSString *itemId = [config[@"id"] isKindOfClass:NSString.class] ? config[@"id"] : @"";
1378
+ NSString *label = [config[@"label"] isKindOfClass:NSString.class] ? config[@"label"] : itemId;
1379
+ NSString *tooltip = [config[@"tooltip"] isKindOfClass:NSString.class] ? config[@"tooltip"] : label;
1380
+ NSString *systemImageName = [config[@"systemImageName"] isKindOfClass:NSString.class] ? config[@"systemImageName"] : nil;
1381
+ NSImage *image = nil;
1382
+ if (systemImageName.length > 0) {
1383
+ if (@available(macOS 11.0, *)) {
1384
+ image = [NSImage imageWithSystemSymbolName:systemImageName accessibilityDescription:label];
1385
+ }
1386
+ }
1387
+ NSArray *menuItems = [config[@"menuItems"] isKindOfClass:NSArray.class] ? config[@"menuItems"] : @[];
1388
+ NSNumber *widthNumber = [config[@"width"] isKindOfClass:NSNumber.class] ? config[@"width"] : nil;
1389
+ BOOL isMenuButton = [type isEqualToString:@"menuButton"] || menuItems.count > 0;
1390
+
1391
+ NSToolbarItem *toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
1392
+ toolbarItem.label = label;
1393
+ toolbarItem.paletteLabel = label;
1394
+ toolbarItem.toolTip = tooltip;
1395
+ toolbarItem.enabled = LegendDictionaryHasKey(config, @"enabled") ? [config[@"enabled"] boolValue] : YES;
1396
+ NSString *placement = [config[@"placement"] isKindOfClass:NSString.class] ? config[@"placement"] : @"trailing";
1397
+ if (@available(macOS 10.15, *)) {
1398
+ toolbarItem.bordered = LegendDictionaryHasKey(config, @"bordered") ? [config[@"bordered"] boolValue] : YES;
1399
+ }
1400
+ if (@available(macOS 11.0, *)) {
1401
+ toolbarItem.navigational = [placement isEqualToString:@"leading"];
1402
+ }
1403
+ NSDictionary *metadata = @{
1404
+ @"itemId": itemId,
1405
+ @"menuItems": menuItems,
1406
+ @"value": [config[@"value"] isKindOfClass:NSString.class] ? config[@"value"] : @"",
1407
+ @"windowIdentifier": LegendToolbarWindowIdentifier(toolbar),
1408
+ @"instanceId": config[@"instanceId"] ?: @"",
1409
+ };
1410
+ if (isMenuButton) {
1411
+ // The fallback name identifies the item; it is never what the control displays, so an
1412
+ // item without a label stays a bare icon instead of printing that identifier.
1413
+ NSString *title = [config[@"label"] isKindOfClass:NSString.class] ? config[@"label"] : @"";
1414
+ NSButton *button = [NSButton buttonWithTitle:title target:self action:@selector(toolbarButtonItemPressed:)];
1415
+ button.bezelStyle = NSBezelStyleRounded;
1416
+ button.controlSize = NSControlSizeRegular;
1417
+ button.enabled = toolbarItem.enabled;
1418
+ button.toolTip = tooltip;
1419
+ if (LegendDictionaryHasKey(config, @"monospacedDigits") && [config[@"monospacedDigits"] boolValue]) {
1420
+ button.font = [NSFont monospacedDigitSystemFontOfSize:NSFont.systemFontSize weight:NSFontWeightSemibold];
1421
+ }
1422
+ if (image) {
1423
+ button.image = image;
1424
+ button.imagePosition = title.length > 0 ? NSImageLeft : NSImageOnly;
1425
+ }
1426
+ NSFont *font = button.font ?: [NSFont systemFontOfSize:NSFont.systemFontSize];
1427
+ CGFloat textWidth = [title sizeWithAttributes:@{NSFontAttributeName: font}].width + (title.length > 0 ? (image ? 64 : 52) : 0);
1428
+ CGFloat buttonWidth = widthNumber ? widthNumber.doubleValue
1429
+ : MAX(title.length > 0 ? 148 : LegendToolbarIconControlWidth, textWidth);
1430
+ button.frame = NSMakeRect(0, 0, buttonWidth, MAX(28, button.fittingSize.height));
1431
+ objc_setAssociatedObject(button, &LegendToolbarControlMetadataKey, metadata, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1432
+ toolbarItem.view = button;
1433
+ } else {
1434
+ toolbarItem.image = image;
1435
+ toolbarItem.target = self;
1436
+ toolbarItem.action = @selector(toolbarButtonItemPressed:);
1437
+ objc_setAssociatedObject(toolbarItem, &LegendToolbarControlMetadataKey, metadata, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1438
+ }
1439
+ return toolbarItem;
1440
+ }
1441
+
1442
+ if ([type isEqualToString:@"label"]) {
1443
+ NSString *itemId = [config[@"id"] isKindOfClass:NSString.class] ? config[@"id"] : @"";
1444
+ NSString *label = [config[@"label"] isKindOfClass:NSString.class] ? config[@"label"] : itemId;
1445
+ NSString *text = [config[@"text"] isKindOfClass:NSString.class] ? config[@"text"] : @"";
1446
+ NSNumber *widthNumber = [config[@"width"] isKindOfClass:NSNumber.class] ? config[@"width"] : nil;
1447
+ CGFloat width = widthNumber ? widthNumber.doubleValue : MAX(44, [text sizeWithAttributes:nil].width + 12);
1448
+
1449
+ NSTextField *textField = [NSTextField labelWithString:text];
1450
+ textField.translatesAutoresizingMaskIntoConstraints = NO;
1451
+ textField.alignment = NSTextAlignmentRight;
1452
+ textField.font = [NSFont monospacedDigitSystemFontOfSize:NSFont.systemFontSize weight:NSFontWeightSemibold];
1453
+ textField.textColor = NSColor.labelColor;
1454
+ textField.toolTip = label;
1455
+ [textField.widthAnchor constraintEqualToConstant:width].active = YES;
1456
+
1457
+ NSToolbarItem *toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
1458
+ toolbarItem.view = textField;
1459
+ toolbarItem.label = label;
1460
+ toolbarItem.paletteLabel = label;
1461
+ toolbarItem.toolTip = label;
1462
+ return toolbarItem;
1463
+ }
1464
+
1465
+ if ([type isEqualToString:@"search"]) {
1466
+ NSString *itemId = [config[@"id"] isKindOfClass:NSString.class] ? config[@"id"] : @"";
1467
+ NSString *label = [config[@"label"] isKindOfClass:NSString.class] ? config[@"label"] : itemId;
1468
+ NSString *placeholder = [config[@"placeholder"] isKindOfClass:NSString.class] ? config[@"placeholder"] : @"Search";
1469
+ NSString *value = [config[@"value"] isKindOfClass:NSString.class] ? config[@"value"] : @"";
1470
+ NSNumber *widthNumber = [config[@"width"] isKindOfClass:NSNumber.class] ? config[@"width"] : nil;
1471
+ CGFloat width = widthNumber ? widthNumber.doubleValue : 240;
1472
+ BOOL collapses = LegendDictionaryHasKey(config, @"collapses") && [config[@"collapses"] boolValue];
1473
+ NSDictionary *metadata = @{
1474
+ @"itemId": itemId,
1475
+ @"windowIdentifier": LegendToolbarWindowIdentifier(toolbar),
1476
+ @"instanceId": config[@"instanceId"] ?: @"",
1477
+ };
1478
+
1479
+ NSSearchField *searchField = nil;
1480
+ NSToolbarItem *toolbarItem = nil;
1481
+ if (collapses) {
1482
+ CGFloat initialWidth = value.length > 0 ? width : LegendToolbarSearchCollapsedSize;
1483
+ NSView *container = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, initialWidth, LegendToolbarSearchCollapsedSize)];
1484
+ container.translatesAutoresizingMaskIntoConstraints = NO;
1485
+ NSLayoutConstraint *containerWidthConstraint = [container.widthAnchor constraintEqualToConstant:initialWidth];
1486
+ containerWidthConstraint.active = YES;
1487
+ [container.heightAnchor constraintEqualToConstant:LegendToolbarSearchCollapsedSize].active = YES;
1488
+
1489
+ NSButton *searchButton = [NSButton buttonWithImage:[self toolbarSearchImageWithLabel:label]
1490
+ target:self
1491
+ action:@selector(toolbarSearchButtonPressed:)];
1492
+ searchButton.translatesAutoresizingMaskIntoConstraints = NO;
1493
+ searchButton.bordered = YES;
1494
+ searchButton.bezelStyle = NSBezelStyleCircular;
1495
+ searchButton.controlSize = NSControlSizeRegular;
1496
+ searchButton.imagePosition = NSImageOnly;
1497
+ searchButton.toolTip = placeholder;
1498
+ searchButton.enabled = LegendDictionaryHasKey(config, @"enabled") ? [config[@"enabled"] boolValue] : YES;
1499
+ [container addSubview:searchButton];
1500
+ [NSLayoutConstraint activateConstraints:@[
1501
+ [searchButton.centerXAnchor constraintEqualToAnchor:container.centerXAnchor],
1502
+ [searchButton.centerYAnchor constraintEqualToAnchor:container.centerYAnchor],
1503
+ [searchButton.widthAnchor constraintEqualToConstant:LegendToolbarSearchCollapsedSize],
1504
+ [searchButton.heightAnchor constraintEqualToConstant:LegendToolbarSearchCollapsedSize],
1505
+ ]];
1506
+
1507
+ searchField = [[NSSearchField alloc] initWithFrame:NSMakeRect(0, 0, width, 28)];
1508
+ searchField.translatesAutoresizingMaskIntoConstraints = NO;
1509
+ searchField.hidden = value.length == 0;
1510
+ searchField.alphaValue = value.length > 0 ? 1 : 0;
1511
+ searchButton.hidden = value.length > 0;
1512
+ searchButton.alphaValue = value.length > 0 ? 0 : 1;
1513
+ [container addSubview:searchField];
1514
+ [NSLayoutConstraint activateConstraints:@[
1515
+ [searchField.leadingAnchor constraintEqualToAnchor:container.leadingAnchor],
1516
+ [searchField.trailingAnchor constraintEqualToAnchor:container.trailingAnchor],
1517
+ [searchField.topAnchor constraintEqualToAnchor:container.topAnchor],
1518
+ [searchField.bottomAnchor constraintEqualToAnchor:container.bottomAnchor],
1519
+ ]];
1520
+ toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
1521
+ toolbarItem.view = container;
1522
+ objc_setAssociatedObject(toolbarItem, &LegendToolbarSearchFieldKey, searchField, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1523
+ objc_setAssociatedObject(toolbarItem, &LegendToolbarSearchWidthKey, @(width), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1524
+ objc_setAssociatedObject(toolbarItem, &LegendToolbarSearchButtonKey, searchButton, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1525
+ objc_setAssociatedObject(toolbarItem, &LegendToolbarSearchContainerKey, container, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1526
+ objc_setAssociatedObject(toolbarItem, &LegendToolbarSearchWidthConstraintKey, containerWidthConstraint, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1527
+ objc_setAssociatedObject(searchButton, &LegendToolbarSearchItemKey, toolbarItem, OBJC_ASSOCIATION_ASSIGN);
1528
+ objc_setAssociatedObject(searchField, &LegendToolbarSearchItemKey, toolbarItem, OBJC_ASSOCIATION_ASSIGN);
1529
+ } else if (@available(macOS 11.0, *)) {
1530
+ NSSearchToolbarItem *searchToolbarItem = [[NSSearchToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
1531
+ searchField = searchToolbarItem.searchField;
1532
+ toolbarItem = searchToolbarItem;
1533
+ } else {
1534
+ searchField = [[NSSearchField alloc] initWithFrame:NSMakeRect(0, 0, width, 28)];
1535
+ toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
1536
+ toolbarItem.view = searchField;
1537
+ }
1538
+
1539
+ searchField.placeholderString = placeholder;
1540
+ searchField.stringValue = value;
1541
+ searchField.delegate = self;
1542
+ searchField.target = self;
1543
+ searchField.action = @selector(toolbarSearchSubmitted:);
1544
+ searchField.enabled = LegendDictionaryHasKey(config, @"enabled") ? [config[@"enabled"] boolValue] : YES;
1545
+ searchField.controlSize = NSControlSizeRegular;
1546
+ objc_setAssociatedObject(searchField, &LegendToolbarControlMetadataKey, metadata, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1547
+
1548
+ toolbarItem.label = label;
1549
+ toolbarItem.paletteLabel = label;
1550
+ toolbarItem.toolTip = placeholder;
1551
+ toolbarItem.enabled = searchField.enabled;
1552
+ if (collapses) {
1553
+ searchField.enabled = toolbarItem.enabled;
1554
+ } else {
1555
+ toolbarItem.minSize = NSMakeSize(MIN(width, 120), 28);
1556
+ toolbarItem.maxSize = NSMakeSize(width, 28);
1557
+ }
1558
+ return toolbarItem;
1559
+ }
1560
+
1561
+ if (![type isEqualToString:@"segmented"]) {
1562
+ return nil;
1563
+ }
1564
+
1565
+ NSArray *segments = [config[@"segments"] isKindOfClass:NSArray.class] ? config[@"segments"] : @[];
1566
+ NSMutableArray<NSString *> *labels = [NSMutableArray new];
1567
+ NSMutableArray<NSString *> *systemImageNames = [NSMutableArray new];
1568
+ NSMutableArray<NSString *> *values = [NSMutableArray new];
1569
+ NSInteger selectedSegment = -1;
1570
+ NSString *selectedValue = [config[@"selectedValue"] isKindOfClass:NSString.class] ? config[@"selectedValue"] : nil;
1571
+
1572
+ for (id segment in segments) {
1573
+ if ([segment isKindOfClass:NSDictionary.class]) {
1574
+ NSDictionary *segmentConfig = (NSDictionary *)segment;
1575
+ NSString *label = [segmentConfig[@"label"] isKindOfClass:NSString.class] ? segmentConfig[@"label"] : @"";
1576
+ NSString *value = [segmentConfig[@"value"] isKindOfClass:NSString.class] ? segmentConfig[@"value"] : label;
1577
+ if (label.length > 0 && value != nil) {
1578
+ NSString *systemImageName = [segmentConfig[@"systemImageName"] isKindOfClass:NSString.class]
1579
+ ? segmentConfig[@"systemImageName"]
1580
+ : @"";
1581
+ if ([value isEqualToString:selectedValue]) {
1582
+ selectedSegment = values.count;
1583
+ }
1584
+ [labels addObject:label];
1585
+ [systemImageNames addObject:systemImageName];
1586
+ [values addObject:value];
1587
+ }
1588
+ }
1589
+ }
1590
+
1591
+ if (labels.count == 0) {
1592
+ return nil;
1593
+ }
1594
+
1595
+ NSSegmentedControl *control = [[NSSegmentedControl alloc] initWithFrame:NSZeroRect];
1596
+ control.segmentCount = labels.count;
1597
+ control.trackingMode = NSSegmentSwitchTrackingSelectOne;
1598
+ control.target = self;
1599
+ control.action = @selector(toolbarSegmentedControlChanged:);
1600
+ control.controlSize = NSControlSizeRegular;
1601
+ control.segmentStyle = NSSegmentStyleAutomatic;
1602
+ control.selectedSegment = selectedSegment;
1603
+
1604
+ CGFloat totalWidth = 0;
1605
+ for (NSInteger index = 0; index < labels.count; index += 1) {
1606
+ NSString *systemImageName = systemImageNames[index];
1607
+ NSImage *image = nil;
1608
+ if (systemImageName.length > 0) {
1609
+ if (@available(macOS 11.0, *)) {
1610
+ image = [NSImage imageWithSystemSymbolName:systemImageName accessibilityDescription:labels[index]];
1611
+ }
1612
+ }
1613
+ if (image) {
1614
+ [control setImage:image forSegment:index];
1615
+ [control setToolTip:labels[index] forSegment:index];
1616
+ } else {
1617
+ [control setLabel:labels[index] forSegment:index];
1618
+ }
1619
+
1620
+ NSFont *font = control.font ?: [NSFont systemFontOfSize:NSFont.systemFontSize];
1621
+ CGFloat width = image ? 38 : MAX(76, [labels[index] sizeWithAttributes:@{NSFontAttributeName: font}].width + 28);
1622
+ [control setWidth:width forSegment:index];
1623
+ totalWidth += width;
1624
+ }
1625
+
1626
+ NSString *itemId = [config[@"id"] isKindOfClass:NSString.class] ? config[@"id"] : @"";
1627
+ objc_setAssociatedObject(control, &LegendToolbarControlMetadataKey, @{
1628
+ @"itemId": itemId,
1629
+ @"values": values,
1630
+ @"windowIdentifier": LegendToolbarWindowIdentifier(toolbar),
1631
+ @"instanceId": config[@"instanceId"] ?: @"",
1632
+ }, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1633
+ control.frame = NSMakeRect(0, 0, totalWidth, control.fittingSize.height);
1634
+
1635
+ NSToolbarItem *toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
1636
+ toolbarItem.view = control;
1637
+ toolbarItem.label = [config[@"label"] isKindOfClass:NSString.class] ? config[@"label"] : itemId;
1638
+ toolbarItem.paletteLabel = toolbarItem.label;
1639
+ return toolbarItem;
1640
+ }
1641
+
1642
+ - (void)toolbarMenuSliderChanged:(NSSlider *)sender
1643
+ {
1644
+ NSDictionary *metadata = objc_getAssociatedObject(sender, &LegendToolbarControlMetadataKey);
1645
+ double value = sender.doubleValue;
1646
+ NSTextField *readout = [sender.superview viewWithTag:731];
1647
+ readout.stringValue = [NSString stringWithFormat:@"%g%@", value, metadata[@"suffix"] ?: @""];
1648
+ [self sendWindowEventWithName:@"onToolbarItemSelected" body:@{
1649
+ @"identifier": metadata[@"windowIdentifier"], @"itemId": metadata[@"itemId"],
1650
+ @"instanceId": metadata[@"instanceId"] ?: @"",
1651
+ @"action": @{ @"type": @"valueChanged", @"itemId": metadata[@"value"], @"value": @(value) },
1652
+ }];
1653
+ }
1654
+
1655
+ - (void)toolbarMenuItemSelected:(NSMenuItem *)sender
1656
+ {
1657
+ NSDictionary *representedObject = [sender.representedObject isKindOfClass:NSDictionary.class]
1658
+ ? sender.representedObject
1659
+ : @{};
1660
+ NSString *identifier = [representedObject[@"windowIdentifier"] isKindOfClass:NSString.class]
1661
+ ? representedObject[@"windowIdentifier"]
1662
+ : @"";
1663
+ NSString *itemId = [representedObject[@"itemId"] isKindOfClass:NSString.class]
1664
+ ? representedObject[@"itemId"]
1665
+ : @"";
1666
+ NSString *value = [representedObject[@"value"] isKindOfClass:NSString.class]
1667
+ ? representedObject[@"value"]
1668
+ : @"";
1669
+
1670
+ [self sendWindowEventWithName:@"onToolbarItemSelected"
1671
+ body:@{@"identifier": identifier, @"itemId": itemId, @"action": @{ @"type": @"action", @"itemId": value }, @"instanceId": representedObject[@"instanceId"] ?: @""}];
1672
+ }
1673
+
1674
+ - (void)toolbarSearchButtonPressed:(id)sender
1675
+ {
1676
+ NSToolbarItem *toolbarItem = [sender isKindOfClass:NSToolbarItem.class]
1677
+ ? (NSToolbarItem *)sender
1678
+ : nil;
1679
+ if (!toolbarItem) {
1680
+ id associatedItem = objc_getAssociatedObject(sender, &LegendToolbarSearchItemKey);
1681
+ if ([associatedItem isKindOfClass:NSToolbarItem.class]) {
1682
+ toolbarItem = (NSToolbarItem *)associatedItem;
1683
+ }
1684
+ }
1685
+
1686
+ [self expandToolbarSearchItem:toolbarItem value:nil focus:YES sendChange:YES animated:YES];
1687
+ }
1688
+
1689
+ - (void)toolbarButtonItemPressed:(id)sender
1690
+ {
1691
+ id metadata = objc_getAssociatedObject(sender, &LegendToolbarControlMetadataKey);
1692
+ NSDictionary *representedObject = [metadata isKindOfClass:NSDictionary.class]
1693
+ ? metadata
1694
+ : @{};
1695
+ NSString *identifier = [representedObject[@"windowIdentifier"] isKindOfClass:NSString.class]
1696
+ ? representedObject[@"windowIdentifier"]
1697
+ : @"";
1698
+ NSString *itemId = [representedObject[@"itemId"] isKindOfClass:NSString.class]
1699
+ ? representedObject[@"itemId"]
1700
+ : @"";
1701
+ NSString *value = [representedObject[@"value"] isKindOfClass:NSString.class]
1702
+ ? representedObject[@"value"]
1703
+ : @"";
1704
+ NSArray *menuItems = [representedObject[@"menuItems"] isKindOfClass:NSArray.class]
1705
+ ? representedObject[@"menuItems"]
1706
+ : @[];
1707
+
1708
+ if (menuItems.count > 0) {
1709
+ NSMenu *menu = [NSMenu new];
1710
+ menu.autoenablesItems = NO;
1711
+ for (id itemCandidate in menuItems) {
1712
+ if (![itemCandidate isKindOfClass:NSDictionary.class]) {
1713
+ continue;
1714
+ }
1715
+ NSDictionary *itemConfig = (NSDictionary *)itemCandidate;
1716
+ if (LegendDictionaryHasKey(itemConfig, @"separator") && [itemConfig[@"separator"] boolValue]) {
1717
+ [menu addItem:NSMenuItem.separatorItem];
1718
+ continue;
1719
+ }
1720
+ NSString *itemLabel = [itemConfig[@"title"] isKindOfClass:NSString.class] ? itemConfig[@"title"] : @"";
1721
+ NSString *itemValue = [itemConfig[@"id"] isKindOfClass:NSString.class] ? itemConfig[@"id"] : @"";
1722
+ if (itemLabel.length == 0 || itemValue.length == 0) {
1723
+ continue;
1724
+ }
1725
+ NSDictionary *sliderConfig = [itemConfig[@"slider"] isKindOfClass:NSDictionary.class] ? itemConfig[@"slider"] : nil;
1726
+ if (sliderConfig) {
1727
+ double minimum = [sliderConfig[@"min"] doubleValue];
1728
+ double maximum = [sliderConfig[@"max"] doubleValue];
1729
+ double initial = [sliderConfig[@"value"] doubleValue];
1730
+ if (!isfinite(minimum) || !isfinite(maximum) || !isfinite(initial) || maximum <= minimum) continue;
1731
+ NSString *suffix = [sliderConfig[@"suffix"] isKindOfClass:NSString.class] ? sliderConfig[@"suffix"] : @"";
1732
+ NSView *container = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 280, 68)];
1733
+ NSTextField *label = [NSTextField labelWithString:itemLabel];
1734
+ label.frame = NSMakeRect(16, 42, 190, 18);
1735
+ [container addSubview:label];
1736
+ NSTextField *readout = [NSTextField labelWithString:@""];
1737
+ readout.tag = 731;
1738
+ readout.alignment = NSTextAlignmentRight;
1739
+ readout.frame = NSMakeRect(210, 42, 54, 18);
1740
+ [container addSubview:readout];
1741
+ NSSlider *slider = [NSSlider sliderWithValue:MAX(minimum, MIN(maximum, initial)) minValue:minimum maxValue:maximum target:self action:@selector(toolbarMenuSliderChanged:)];
1742
+ slider.frame = NSMakeRect(16, 12, 248, 24);
1743
+ slider.continuous = YES;
1744
+ slider.accessibilityLabel = itemLabel;
1745
+ slider.enabled = LegendDictionaryHasKey(itemConfig, @"enabled") ? [itemConfig[@"enabled"] boolValue] : YES;
1746
+ readout.stringValue = [NSString stringWithFormat:@"%g%@", slider.doubleValue, suffix];
1747
+ objc_setAssociatedObject(slider, &LegendToolbarControlMetadataKey, @{
1748
+ @"windowIdentifier": identifier, @"instanceId": representedObject[@"instanceId"] ?: @"", @"itemId": itemId, @"value": itemValue, @"suffix": suffix,
1749
+ }, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
1750
+ [container addSubview:slider];
1751
+ NSMenuItem *sliderItem = [NSMenuItem new];
1752
+ sliderItem.view = container;
1753
+ [menu addItem:sliderItem];
1754
+ continue;
1755
+ }
1756
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:itemLabel action:@selector(toolbarMenuItemSelected:) keyEquivalent:@""];
1757
+ item.target = self;
1758
+ item.enabled = LegendDictionaryHasKey(itemConfig, @"enabled") ? [itemConfig[@"enabled"] boolValue] : YES;
1759
+ item.state = LegendDictionaryHasKey(itemConfig, @"checked") && [itemConfig[@"checked"] boolValue] ? NSControlStateValueOn : NSControlStateValueOff;
1760
+ NSString *itemSystemImageName = [itemConfig[@"systemImageName"] isKindOfClass:NSString.class] ? itemConfig[@"systemImageName"] : nil;
1761
+ if (itemSystemImageName.length > 0) {
1762
+ if (@available(macOS 11.0, *)) {
1763
+ item.image = [NSImage imageWithSystemSymbolName:itemSystemImageName accessibilityDescription:itemLabel];
1764
+ }
1765
+ }
1766
+ item.representedObject = @{
1767
+ @"itemId": itemId,
1768
+ @"value": itemValue,
1769
+ @"windowIdentifier": identifier, @"instanceId": representedObject[@"instanceId"] ?: @"",
1770
+ };
1771
+ [menu addItem:item];
1772
+ }
1773
+
1774
+ if (menu.numberOfItems > 0) {
1775
+ if ([sender isKindOfClass:NSView.class]) {
1776
+ NSView *senderView = (NSView *)sender;
1777
+ [menu popUpMenuPositioningItem:nil atLocation:NSMakePoint(0, NSHeight(senderView.bounds) + 4) inView:senderView];
1778
+ } else {
1779
+ NSEvent *event = NSApp.currentEvent;
1780
+ NSWindow *window = (NSWindow *)self.windows[identifier];
1781
+ if (event && window.contentView) {
1782
+ [NSMenu popUpContextMenu:menu withEvent:event forView:window.contentView];
1783
+ } else {
1784
+ [menu popUpMenuPositioningItem:nil atLocation:NSZeroPoint inView:nil];
1785
+ }
1786
+ }
1787
+ return;
1788
+ }
1789
+ }
1790
+
1791
+ [self sendWindowEventWithName:@"onToolbarItemSelected"
1792
+ body:@{@"identifier": identifier, @"itemId": itemId, @"value": value, @"instanceId": representedObject[@"instanceId"] ?: @""}];
1793
+ }
1794
+
1795
+ - (void)toolbarSegmentedControlChanged:(NSSegmentedControl *)sender
1796
+ {
1797
+ id metadata = objc_getAssociatedObject(sender, &LegendToolbarControlMetadataKey);
1798
+ NSDictionary *representedObject = [metadata isKindOfClass:NSDictionary.class]
1799
+ ? metadata
1800
+ : @{};
1801
+ NSArray *values = [representedObject[@"values"] isKindOfClass:NSArray.class] ? representedObject[@"values"] : @[];
1802
+ NSInteger selectedSegment = sender.selectedSegment;
1803
+ NSString *value = selectedSegment >= 0 && selectedSegment < values.count && [values[selectedSegment] isKindOfClass:NSString.class]
1804
+ ? values[selectedSegment]
1805
+ : @"";
1806
+ NSString *identifier = [representedObject[@"windowIdentifier"] isKindOfClass:NSString.class]
1807
+ ? representedObject[@"windowIdentifier"]
1808
+ : @"";
1809
+ NSString *itemId = [representedObject[@"itemId"] isKindOfClass:NSString.class]
1810
+ ? representedObject[@"itemId"]
1811
+ : @"";
1812
+
1813
+ [self sendWindowEventWithName:@"onToolbarItemSelected"
1814
+ body:@{@"identifier": identifier, @"itemId": itemId, @"value": value, @"kind": @"selection", @"instanceId": representedObject[@"instanceId"] ?: @""}];
1815
+ }
1816
+
1817
+ - (void)sendToolbarSearchEventForField:(NSSearchField *)searchField
1818
+ submitted:(BOOL)submitted
1819
+ shiftKey:(BOOL)shiftKey
1820
+ {
1821
+ id metadata = objc_getAssociatedObject(searchField, &LegendToolbarControlMetadataKey);
1822
+ NSDictionary *representedObject = [metadata isKindOfClass:NSDictionary.class]
1823
+ ? metadata
1824
+ : @{};
1825
+ NSString *identifier = [representedObject[@"windowIdentifier"] isKindOfClass:NSString.class]
1826
+ ? representedObject[@"windowIdentifier"]
1827
+ : @"";
1828
+ NSString *itemId = [representedObject[@"itemId"] isKindOfClass:NSString.class]
1829
+ ? representedObject[@"itemId"]
1830
+ : @"";
1831
+
1832
+ [self sendWindowEventWithName:@"onToolbarSearch"
1833
+ body:@{
1834
+ @"identifier": identifier,
1835
+ @"itemId": itemId,
1836
+ @"shiftKey": @(shiftKey),
1837
+ @"submitted": @(submitted),
1838
+ @"instanceId": representedObject[@"instanceId"] ?: @"",
1839
+ @"value": searchField.stringValue ?: @"",
1840
+ }];
1841
+ }
1842
+
1843
+ - (void)controlTextDidChange:(NSNotification *)notification
1844
+ {
1845
+ if ([notification.object isKindOfClass:NSSearchField.class]) {
1846
+ [self sendToolbarSearchEventForField:(NSSearchField *)notification.object submitted:NO shiftKey:NO];
1847
+ }
1848
+ }
1849
+
1850
+ - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
1851
+ {
1852
+ if ([control isKindOfClass:NSSearchField.class] && commandSelector == @selector(cancelOperation:)) {
1853
+ [control.window makeFirstResponder:nil];
1854
+ return YES;
1855
+ }
1856
+ return NO;
1857
+ }
1858
+
1859
+ - (void)controlTextDidEndEditing:(NSNotification *)notification
1860
+ {
1861
+ if ([notification.object isKindOfClass:NSSearchField.class]) {
1862
+ NSSearchField *searchField = (NSSearchField *)notification.object;
1863
+ if (searchField.stringValue.length == 0) {
1864
+ id associatedItem = objc_getAssociatedObject(searchField, &LegendToolbarSearchItemKey);
1865
+ if ([associatedItem isKindOfClass:NSToolbarItem.class]) {
1866
+ [self collapseToolbarSearchItem:(NSToolbarItem *)associatedItem animated:YES];
1867
+ }
1868
+ }
1869
+ }
1870
+ }
1871
+
1872
+ - (void)toolbarSearchSubmitted:(NSSearchField *)sender
1873
+ {
1874
+ NSEvent *currentEvent = NSApp.currentEvent;
1875
+ BOOL shiftKey = currentEvent && ((currentEvent.modifierFlags & NSEventModifierFlagShift) != 0);
1876
+ [self sendToolbarSearchEventForField:sender submitted:YES shiftKey:shiftKey];
1877
+ }
1878
+ #endif
1879
+
1880
+ - (NSString *)successJson
1881
+ {
1882
+ return [self jsonStringFromObject:@{@"success": @YES}];
1883
+ }
1884
+
1885
+ - (NSString *)failureJson:(NSString *)message
1886
+ {
1887
+ return [self jsonStringFromObject:@{@"success": @NO, @"message": message ?: @""}];
1888
+ }
1889
+
1890
+ - (NSString *)getConstantsJson
1891
+ {
1892
+ #if TARGET_OS_OSX
1893
+ return [self jsonStringFromObject:@{
1894
+ @"STYLE_MASK_BORDERLESS": @(NSWindowStyleMaskBorderless),
1895
+ @"STYLE_MASK_TITLED": @(NSWindowStyleMaskTitled),
1896
+ @"STYLE_MASK_CLOSABLE": @(NSWindowStyleMaskClosable),
1897
+ @"STYLE_MASK_MINIATURIZABLE": @(NSWindowStyleMaskMiniaturizable),
1898
+ @"STYLE_MASK_RESIZABLE": @(NSWindowStyleMaskResizable),
1899
+ @"STYLE_MASK_UNIFIED_TITLE_AND_TOOLBAR": @(NSWindowStyleMaskUnifiedTitleAndToolbar),
1900
+ @"STYLE_MASK_FULL_SCREEN": @(NSWindowStyleMaskFullScreen),
1901
+ @"STYLE_MASK_FULL_SIZE_CONTENT_VIEW": @(NSWindowStyleMaskFullSizeContentView),
1902
+ @"STYLE_MASK_UTILITY_WINDOW": @(NSWindowStyleMaskUtilityWindow),
1903
+ @"STYLE_MASK_DOC_MODAL_WINDOW": @(NSWindowStyleMaskDocModalWindow),
1904
+ @"STYLE_MASK_NONACTIVATING_PANEL": @(NSWindowStyleMaskNonactivatingPanel),
1905
+ @"WINDOW_LEVEL_NORMAL": @(NSNormalWindowLevel),
1906
+ @"WINDOW_LEVEL_FLOATING": @(NSFloatingWindowLevel),
1907
+ @"WINDOW_LEVEL_MODAL_PANEL": @(NSModalPanelWindowLevel),
1908
+ @"WINDOW_LEVEL_MAIN_MENU": @(NSMainMenuWindowLevel),
1909
+ @"WINDOW_LEVEL_STATUS": @(NSStatusWindowLevel),
1910
+ @"WINDOW_LEVEL_SCREEN_SAVER": @(NSScreenSaverWindowLevel),
1911
+ }];
1912
+ #else
1913
+ return @"{}";
1914
+ #endif
1915
+ }
1916
+
1917
+ - (NSString *)getReactNativeStartupTimingJson
1918
+ {
1919
+ facebook::react::ReactMarker::StartupLogger& startupLogger =
1920
+ facebook::react::ReactMarker::StartupLogger::getInstance();
1921
+ NSMutableDictionary<NSString *, NSNumber *> *timing = [NSMutableDictionary new];
1922
+
1923
+ #if TARGET_OS_OSX
1924
+ timing[@"clockOffsetMs"] = @(
1925
+ NSDate.date.timeIntervalSince1970 * 1000 - CACurrentMediaTime() * 1000
1926
+ );
1927
+ #if __has_include(<RNDesktopApp/SparkDesktop.h>)
1928
+ [timing addEntriesFromDictionary:SparkStartupTiming()];
1929
+ #else
1930
+ if (LegendMainWindowFirstVisibleTimeMs > 0) {
1931
+ timing[@"mainWindowFirstVisibleTime"] = @(LegendMainWindowFirstVisibleTimeMs);
1932
+ }
1933
+ if (LegendMainWindowReactRootAttachedTimeMs > 0) {
1934
+ timing[@"mainWindowReactRootAttachedTime"] = @(LegendMainWindowReactRootAttachedTimeMs);
1935
+ }
1936
+ #endif
1937
+ #endif
1938
+
1939
+ double startTime = startupLogger.getAppStartupStartTime();
1940
+ if (std::isnan(startTime)) {
1941
+ startTime = startupLogger.getInitReactRuntimeStartTime();
1942
+ }
1943
+ if (!std::isnan(startTime)) {
1944
+ timing[@"startTime"] = @(startTime);
1945
+ }
1946
+
1947
+ const struct {
1948
+ NSString *key;
1949
+ double value;
1950
+ } markers[] = {
1951
+ {@"initializeRuntimeStart", startupLogger.getInitReactRuntimeStartTime()},
1952
+ {@"executeJavaScriptBundleEntryPointStart", startupLogger.getRunJSBundleStartTime()},
1953
+ {@"executeJavaScriptBundleEntryPointEnd", startupLogger.getRunJSBundleEndTime()},
1954
+ {@"initializeRuntimeEnd", startupLogger.getInitReactRuntimeEndTime()},
1955
+ {@"endTime", startupLogger.getAppStartupEndTime()},
1956
+ };
1957
+ for (const auto& marker : markers) {
1958
+ if (!std::isnan(marker.value)) {
1959
+ timing[marker.key] = @(marker.value);
1960
+ }
1961
+ }
1962
+
1963
+ return [self jsonStringFromObject:timing];
1964
+ }
1965
+
1966
+ - (NSArray<NSDictionary *> *)displayDictionaries
1967
+ {
1968
+ #if TARGET_OS_OSX
1969
+ NSMutableArray<NSDictionary *> *displays = [NSMutableArray new];
1970
+ NSScreen *mainScreen = NSScreen.mainScreen;
1971
+ for (NSScreen *screen in NSScreen.screens) {
1972
+ NSNumber *screenNumber = screen.deviceDescription[@"NSScreenNumber"];
1973
+ NSRect frame = screen.frame;
1974
+ NSRect visibleFrame = screen.visibleFrame;
1975
+ NSString *name = @"Display";
1976
+ if (@available(macOS 10.15, *)) {
1977
+ name = screen.localizedName ?: name;
1978
+ }
1979
+ CFUUIDRef uuid = screenNumber ? CGDisplayCreateUUIDFromDisplayID(screenNumber.unsignedIntValue) : nullptr;
1980
+ NSString *persistentId = uuid ? CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuid)) : screenNumber.stringValue;
1981
+ if (uuid) CFRelease(uuid);
1982
+ [displays addObject:@{
1983
+ @"persistentId": persistentId ?: @"",
1984
+ @"id": screenNumber.stringValue ?: @"",
1985
+ @"name": name,
1986
+ @"isMain": @(screen == mainScreen),
1987
+ @"scaleFactor": @(screen.backingScaleFactor),
1988
+ @"frame": @{ @"x": @(frame.origin.x), @"y": @(frame.origin.y), @"width": @(frame.size.width), @"height": @(frame.size.height) },
1989
+ @"visibleFrame": @{ @"x": @(visibleFrame.origin.x), @"y": @(visibleFrame.origin.y), @"width": @(visibleFrame.size.width), @"height": @(visibleFrame.size.height) },
1990
+ }];
1991
+ }
1992
+ return displays;
1993
+ #else
1994
+ return @[];
1995
+ #endif
1996
+ }
1997
+
1998
+ - (void)getDisplays:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
1999
+ {
2000
+ resolve([self jsonStringFromObject:[self displayDictionaries]]);
2001
+ }
2002
+
2003
+ #if TARGET_OS_OSX
2004
+ - (RCTUIView *)createReactRootViewWithModuleName:(NSString *)moduleName initialProperties:(NSDictionary *)initialProps
2005
+ {
2006
+
2007
+ id appDelegate = NSApplication.sharedApplication.delegate;
2008
+ RCTRootViewFactory *rootViewFactory = nil;
2009
+
2010
+ if ([appDelegate respondsToSelector:@selector(rootViewFactory)]) {
2011
+ rootViewFactory = [(id<RNWindowManagerRootViewFactoryProvider>)appDelegate rootViewFactory];
2012
+ }
2013
+
2014
+ if (rootViewFactory) {
2015
+ RCTUIView *rootView = (RCTUIView *)[rootViewFactory viewWithModuleName:moduleName initialProperties:initialProps];
2016
+ return rootView;
2017
+ }
2018
+
2019
+ RCTBridge *bridge = self.bridge;
2020
+ if (!bridge) {
2021
+ return nil;
2022
+ }
2023
+
2024
+ RCTUIView *rootView = [[RCTRootView alloc] initWithBridge:bridge moduleName:moduleName initialProperties:initialProps];
2025
+ return rootView;
2026
+ }
2027
+ #endif
2028
+
2029
+ - (void)finishWindowRestoration:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2030
+ {
2031
+ #if TARGET_OS_OSX
2032
+ RCTExecuteOnMainQueue(^{
2033
+ // Adopted windows are removed from this collection by LegendTakePrecreatedWindow.
2034
+ // Close only unclaimed native shells; preserve saved records and document data.
2035
+ for (NSString *identifier in LegendPrecreatedWindows.allKeys) {
2036
+ LegendRemovePrecreatedWindow(identifier);
2037
+ }
2038
+ resolve([self successJson]);
2039
+ });
2040
+ #else
2041
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2042
+ #endif
2043
+ }
2044
+
2045
+ - (void)openWindow:(NSString *)optionsJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2046
+ {
2047
+ #if TARGET_OS_OSX
2048
+ RCTExecuteOnMainQueue(^{
2049
+ NSDictionary *options = [self parseObjectJSON:optionsJson];
2050
+ NSString *identifier = [options[@"identifier"] isKindOfClass:NSString.class] ? options[@"identifier"] : nil;
2051
+ NSString *moduleName = [options[@"moduleName"] isKindOfClass:NSString.class] ? options[@"moduleName"] : nil;
2052
+ NSAppearance *darkAppearance = LegendDarkAppearance();
2053
+
2054
+ if (moduleName.length == 0) {
2055
+ moduleName = identifier;
2056
+ }
2057
+ if (identifier.length == 0) {
2058
+ identifier = moduleName ?: @"default";
2059
+ }
2060
+
2061
+ // A caller can turn off restoration before the window has been adopted by
2062
+ // React, so remove a native startup shell immediately when requested.
2063
+ if (LegendDictionaryHasKey(options, @"restoreOnLaunch") && ![options[@"restoreOnLaunch"] boolValue]) {
2064
+ LegendSetRestorableWindowOptions(identifier, nil);
2065
+ LegendRemovePrecreatedWindow(identifier);
2066
+ }
2067
+
2068
+ NSString *title = [options[@"title"] isKindOfClass:NSString.class] ? options[@"title"] : nil;
2069
+ title = title ?: moduleName ?: @"New Window";
2070
+ BOOL hasRepresentedURL = LegendDictionaryHasKey(options, @"representedURL");
2071
+ id representedURLValue = hasRepresentedURL ? options[@"representedURL"] : nil;
2072
+
2073
+ NSDictionary *windowStyle = [options[@"windowStyle"] isKindOfClass:NSDictionary.class] ? options[@"windowStyle"] : @{};
2074
+ NSNumber *maskNumber = [windowStyle[@"mask"] isKindOfClass:NSNumber.class] ? windowStyle[@"mask"] : nil;
2075
+ NSNumber *transparentTitlebar = [windowStyle[@"titlebarAppearsTransparent"] isKindOfClass:NSNumber.class]
2076
+ ? windowStyle[@"titlebarAppearsTransparent"]
2077
+ : nil;
2078
+ NSString *titleVisibility = [windowStyle[@"titleVisibility"] isKindOfClass:NSString.class]
2079
+ ? windowStyle[@"titleVisibility"]
2080
+ : nil;
2081
+ NSString *toolbarStyle = [windowStyle[@"toolbarStyle"] isKindOfClass:NSString.class] ? windowStyle[@"toolbarStyle"] : nil;
2082
+ NSString *contentLayoutMode = [windowStyle[@"contentLayoutMode"] isKindOfClass:NSString.class]
2083
+ ? windowStyle[@"contentLayoutMode"]
2084
+ : nil;
2085
+ NSString *titlebarSeparatorStyle = [windowStyle[@"titlebarSeparatorStyle"] isKindOfClass:NSString.class]
2086
+ ? windowStyle[@"titlebarSeparatorStyle"]
2087
+ : nil;
2088
+ NSString *backgroundColor = [windowStyle[@"backgroundColor"] isKindOfClass:NSString.class]
2089
+ ? windowStyle[@"backgroundColor"]
2090
+ : nil;
2091
+ NSString *appearance = [windowStyle[@"appearance"] isKindOfClass:NSString.class]
2092
+ ? windowStyle[@"appearance"]
2093
+ : nil;
2094
+ BOOL usesTitlebarBackground = transparentTitlebar.boolValue && backgroundColor.length > 0;
2095
+ NSNumber *levelNumber = [options[@"level"] isKindOfClass:NSNumber.class] ? options[@"level"] : nil;
2096
+ BOOL transparentBackground = [options[@"transparentBackground"] boolValue];
2097
+ BOOL interceptClose = [options[@"interceptClose"] boolValue];
2098
+ BOOL deferOrderFront = [options[@"deferOrderFront"] boolValue];
2099
+ NSNumber *hasShadowNumber = [options[@"hasShadow"] isKindOfClass:NSNumber.class] ? options[@"hasShadow"] : nil;
2100
+ BOOL shouldApplyHasShadow = hasShadowNumber != nil;
2101
+ BOOL hasShadow = shouldApplyHasShadow ? hasShadowNumber.boolValue : NO;
2102
+ NSNumber *animateFrameChangeNumber = [options[@"animateFrameChange"] isKindOfClass:NSNumber.class]
2103
+ ? options[@"animateFrameChange"]
2104
+ : nil;
2105
+ BOOL animateFrameChange = animateFrameChangeNumber ? animateFrameChangeNumber.boolValue : NO;
2106
+ NSNumber *frameAnimationDurationNumber = [options[@"frameAnimationDurationMs"] isKindOfClass:NSNumber.class]
2107
+ ? options[@"frameAnimationDurationMs"]
2108
+ : nil;
2109
+ NSTimeInterval frameAnimationDuration = frameAnimationDurationNumber ? frameAnimationDurationNumber.doubleValue / 1000.0 : 0;
2110
+ NSNumber *widthNumber = [windowStyle[@"width"] isKindOfClass:NSNumber.class]
2111
+ ? windowStyle[@"width"]
2112
+ : ([options[@"width"] isKindOfClass:NSNumber.class] ? options[@"width"] : nil);
2113
+ NSNumber *heightNumber = [windowStyle[@"height"] isKindOfClass:NSNumber.class]
2114
+ ? windowStyle[@"height"]
2115
+ : ([options[@"height"] isKindOfClass:NSNumber.class] ? options[@"height"] : nil);
2116
+ CGFloat width = widthNumber ? widthNumber.doubleValue : 400;
2117
+ CGFloat height = heightNumber ? heightNumber.doubleValue : 300;
2118
+ NSNumber *minWidthNumber = [windowStyle[@"minWidth"] isKindOfClass:NSNumber.class]
2119
+ ? windowStyle[@"minWidth"]
2120
+ : ([options[@"minWidth"] isKindOfClass:NSNumber.class] ? options[@"minWidth"] : nil);
2121
+ NSNumber *minHeightNumber = [windowStyle[@"minHeight"] isKindOfClass:NSNumber.class]
2122
+ ? windowStyle[@"minHeight"]
2123
+ : ([options[@"minHeight"] isKindOfClass:NSNumber.class] ? options[@"minHeight"] : nil);
2124
+ BOOL hasMinWidth = minWidthNumber != nil;
2125
+ BOOL hasMinHeight = minHeightNumber != nil;
2126
+ CGFloat minWidth = hasMinWidth ? minWidthNumber.doubleValue : 0;
2127
+ CGFloat minHeight = hasMinHeight ? minHeightNumber.doubleValue : 0;
2128
+
2129
+ if (hasMinWidth && width < minWidth) {
2130
+ width = minWidth;
2131
+ }
2132
+ if (hasMinHeight && height < minHeight) {
2133
+ height = minHeight;
2134
+ }
2135
+
2136
+ NSNumber *originX = [options[@"x"] isKindOfClass:NSNumber.class] ? options[@"x"] : nil;
2137
+ NSNumber *originY = [options[@"y"] isKindOfClass:NSNumber.class] ? options[@"y"] : nil;
2138
+ BOOL hasToolbar = [windowStyle[@"hasToolbar"] boolValue];
2139
+ NSWindow *existingWindow = (NSWindow *)self.windows[identifier];
2140
+ if (existingWindow) { reject(@"E_ALREADY_EXISTS", @"Window ID is already open", nil); return; }
2141
+
2142
+ NSUInteger styleMask = maskNumber
2143
+ ? maskNumber.unsignedIntegerValue
2144
+ : (NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable);
2145
+ NSWindow *precreatedWindow = LegendTakePrecreatedWindow ? LegendTakePrecreatedWindow(identifier) : nil;
2146
+ NSWindow *window = precreatedWindow ?: [[([options[@"sparkOverlay"] boolValue] || windowStyle[@"panelStyle"] ? NSPanel.class : NSWindow.class) alloc] initWithContentRect:NSMakeRect(0, 0, width, height)
2147
+ styleMask:styleMask
2148
+ backing:NSBackingStoreBuffered
2149
+ defer:NO];
2150
+ objc_setAssociatedObject(window, NSSelectorFromString(@"sparkWindowInstanceId"), NSUUID.UUID.UUIDString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
2151
+ if ([options[@"sparkOverlay"] boolValue]) {
2152
+ ((NSPanel *)window).hidesOnDeactivate = NO;
2153
+ window.styleMask |= NSWindowStyleMaskNonactivatingPanel;
2154
+ window.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces | NSWindowCollectionBehaviorFullScreenAuxiliary;
2155
+ }
2156
+ objc_setAssociatedObject(window, NSSelectorFromString(@"sparkRestoredShell"), @(precreatedWindow != nil), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
2157
+ LegendApplyWindowOptions(window, options);
2158
+ LegendApplyContentLayoutModeOption(window, contentLayoutMode);
2159
+ LegendApplyContentLayoutMode(window, maskNumber, usesTitlebarBackground);
2160
+
2161
+ if (appearance) {
2162
+ LegendApplyWindowAppearance(window, appearance);
2163
+ } else if (darkAppearance) {
2164
+ window.appearance = darkAppearance;
2165
+ }
2166
+ window.releasedWhenClosed = NO;
2167
+ LegendApplyWindowTitleAndRepresentedURL(window, title, hasRepresentedURL, representedURLValue, title);
2168
+ if (transparentTitlebar != nil) {
2169
+ window.titlebarAppearsTransparent = transparentTitlebar.boolValue;
2170
+ }
2171
+ LegendApplyTitleVisibility(window, titleVisibility);
2172
+ if (hasToolbar) {
2173
+ NSToolbar *toolbar = LegendCreateToolbar(identifier);
2174
+ window.toolbar = toolbar;
2175
+ }
2176
+ LegendApplyToolbarStyle(window, toolbarStyle);
2177
+ LegendApplyTitlebarSeparatorStyle(window, titlebarSeparatorStyle);
2178
+ [self applyTitlebarControlsFromOptions:options toWindow:window identifier:identifier];
2179
+ [self applyToolbarItemsFromOptions:options toWindow:window identifier:identifier];
2180
+ [self applyTitlebarMaterialFromOptions:options toWindow:window identifier:identifier];
2181
+ LegendApplyWindowBackgroundColor(window, backgroundColor);
2182
+ if (levelNumber) {
2183
+ window.level = levelNumber.integerValue;
2184
+ }
2185
+ if (shouldApplyHasShadow) {
2186
+ window.hasShadow = hasShadow;
2187
+ if (hasShadow) {
2188
+ [window invalidateShadow];
2189
+ }
2190
+ }
2191
+ if (transparentBackground) {
2192
+ window.opaque = NO;
2193
+ if (!hasToolbar) {
2194
+ window.backgroundColor = NSColor.clearColor;
2195
+ }
2196
+ window.contentView.wantsLayer = YES;
2197
+ window.contentView.layer.masksToBounds = NO;
2198
+ }
2199
+
2200
+ if (precreatedWindow) {
2201
+ // Native startup already restored this window to its final AppKit frame.
2202
+ } else if (originX || originY) {
2203
+ NSPoint origin = window.frame.origin;
2204
+ if (originX) {
2205
+ origin.x = originX.doubleValue;
2206
+ }
2207
+ if (originY) {
2208
+ origin.y = originY.doubleValue;
2209
+ }
2210
+ [window setFrameOrigin:origin];
2211
+ } else {
2212
+ [window center];
2213
+ }
2214
+
2215
+ if (hasMinWidth || hasMinHeight) {
2216
+ NSSize currentMinSize = window.minSize;
2217
+ [window setMinSize:NSMakeSize(hasMinWidth ? minWidth : currentMinSize.width,
2218
+ hasMinHeight ? minHeight : currentMinSize.height)];
2219
+ }
2220
+
2221
+ NSColor *startupBackgroundColor = transparentBackground
2222
+ ? NSColor.clearColor
2223
+ : LegendWindowStartupBackgroundColor(window, backgroundColor);
2224
+ if (!transparentBackground && backgroundColor.length == 0) {
2225
+ window.backgroundColor = startupBackgroundColor;
2226
+ window.opaque = startupBackgroundColor.alphaComponent >= 1;
2227
+ LegendApplyBackgroundColorToView(window.contentView, startupBackgroundColor);
2228
+ }
2229
+
2230
+ if (!precreatedWindow && [options[@"restoreOnLaunch"] boolValue]) {
2231
+ [window setFrameAutosaveName:LegendManagedWindowFrameAutosaveName(identifier)];
2232
+ }
2233
+ if (!precreatedWindow && LegendPrepareSidebarSplitViewStartup) {
2234
+ LegendPrepareSidebarSplitViewStartup(window, windowStyle[@"startupSplitView"]);
2235
+ }
2236
+ BOOL presentedBeforeReactRoot = precreatedWindow != nil || !deferOrderFront;
2237
+ if (!deferOrderFront && !window.visible) {
2238
+ [window.contentView displayIfNeeded];
2239
+ [window displayIfNeeded];
2240
+ [window makeKeyAndOrderFront:nil];
2241
+ if (levelNumber) {
2242
+ [window orderFrontRegardless];
2243
+ }
2244
+ }
2245
+
2246
+ NSDictionary *initialProps = [self initialPropsFromOptions:options];
2247
+ RCTUIView *rootView = [self createReactRootViewWithModuleName:moduleName initialProperties:initialProps];
2248
+ if (!rootView) {
2249
+ if (presentedBeforeReactRoot) {
2250
+ [window close];
2251
+ }
2252
+ reject(@"no_root_view", @"React root view could not be created", nil);
2253
+ return;
2254
+ }
2255
+
2256
+ rootView.backgroundColor = startupBackgroundColor;
2257
+ __weak NSWindow *weakWindow = window;
2258
+ void (^installRoot)(void) = ^{
2259
+ NSWindow *targetWindow = weakWindow;
2260
+ if (!targetWindow) {
2261
+ return;
2262
+ }
2263
+ [rootView removeFromSuperview];
2264
+ targetWindow.contentView = rootView;
2265
+ if (usesTitlebarBackground) {
2266
+ LegendEnsureRootViewContainer(targetWindow, rootView);
2267
+ }
2268
+ LegendApplyWindowBackgroundColor(targetWindow, backgroundColor);
2269
+ LegendSizeRootViewToWindow(rootView, targetWindow);
2270
+ };
2271
+ if (!LegendAttachSidebarSplitViewStartupRoot ||
2272
+ !LegendAttachSidebarSplitViewStartupRoot(window, rootView, installRoot)) {
2273
+ installRoot();
2274
+ }
2275
+ objc_setAssociatedObject(window, &LegendManagedRootViewKey, rootView, OBJC_ASSOCIATION_ASSIGN);
2276
+ LegendSizeRootViewToWindow(rootView, window);
2277
+ if (transparentBackground) {
2278
+ rootView.backgroundColor = NSColor.clearColor;
2279
+ window.contentView.wantsLayer = YES;
2280
+ window.contentView.layer.backgroundColor = NSColor.clearColor.CGColor;
2281
+ window.contentView.layer.masksToBounds = NO;
2282
+ }
2283
+ rootView.wantsLayer = YES;
2284
+ rootView.layerUsesCoreImageFilters = YES;
2285
+ if (!rootView.layer) {
2286
+ rootView.layer = [CALayer layer];
2287
+ }
2288
+ rootView.layer.masksToBounds = NO;
2289
+ LegendPrepareWindowForDisplay(window, backgroundColor);
2290
+ window.delegate = self;
2291
+
2292
+ window.identifier = identifier;
2293
+ self.windows[identifier] = window;
2294
+ self.rootViews[identifier] = rootView;
2295
+ self.moduleNames[identifier] = moduleName ?: @"";
2296
+ [self recordWindowOptions:options identifier:identifier moduleName:moduleName];
2297
+ if (interceptClose) {
2298
+ [self.closeRequestIdentifiers addObject:identifier];
2299
+ } else {
2300
+ [self.closeRequestIdentifiers removeObject:identifier];
2301
+ }
2302
+
2303
+ [self finishOpeningWindow:window options:windowStyle resolve:resolve reject:reject];
2304
+ });
2305
+ #else
2306
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2307
+ #endif
2308
+ }
2309
+
2310
+ #if TARGET_OS_OSX
2311
+ // Opening resolves only after AppKit enters its fullscreen Space, so callers
2312
+ // can safely focus a presenter on another display afterwards.
2313
+ - (void)finishOpeningWindow:(NSWindow *)window options:(NSDictionary *)windowStyle
2314
+ resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2315
+ {
2316
+ if (![windowStyle[@"fullscreen"] boolValue] || (window.styleMask & NSWindowStyleMaskFullScreen)) {
2317
+ resolve([self successJson]);
2318
+ return;
2319
+ }
2320
+ objc_setAssociatedObject(window, &LegendManagedFullscreenPresentationKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
2321
+ window.collectionBehavior = (window.collectionBehavior & ~NSWindowCollectionBehaviorFullScreenAuxiliary)
2322
+ | NSWindowCollectionBehaviorFullScreenPrimary;
2323
+ __block id entered = nil;
2324
+ __block id closed = nil;
2325
+ __block BOOL finished = NO;
2326
+ void (^finish)(BOOL) = ^(BOOL success) {
2327
+ if (finished) return;
2328
+ finished = YES;
2329
+ [NSNotificationCenter.defaultCenter removeObserver:entered];
2330
+ [NSNotificationCenter.defaultCenter removeObserver:closed];
2331
+ entered = nil;
2332
+ closed = nil;
2333
+ if (success) resolve([self successJson]);
2334
+ else {
2335
+ [window close];
2336
+ reject(@"fullscreen_failed", @"Fullscreen did not complete. Check the selected display and try again.", nil);
2337
+ }
2338
+ };
2339
+ entered = [NSNotificationCenter.defaultCenter addObserverForName:NSWindowDidEnterFullScreenNotification
2340
+ object:window queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *note) { finish(YES); }];
2341
+ closed = [NSNotificationCenter.defaultCenter addObserverForName:NSWindowWillCloseNotification
2342
+ object:window queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *note) { finish(NO); }];
2343
+ [window makeKeyAndOrderFront:nil];
2344
+ [window toggleFullScreen:nil];
2345
+ // Bound failures where AppKit never sends the completion notification.
2346
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ finish(NO); });
2347
+ }
2348
+
2349
+ - (NSApplicationPresentationOptions)window:(NSWindow *)window
2350
+ willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)options
2351
+ {
2352
+ if (![objc_getAssociatedObject(window, &LegendManagedFullscreenPresentationKey) boolValue]) return options;
2353
+ return options | NSApplicationPresentationAutoHideMenuBar | NSApplicationPresentationAutoHideDock
2354
+ | NSApplicationPresentationAutoHideToolbar;
2355
+ }
2356
+ #endif
2357
+
2358
+ - (void)setWindowTitle:(NSString *)identifier
2359
+ title:(NSString *)title
2360
+ resolve:(RCTPromiseResolveBlock)resolve
2361
+ reject:(RCTPromiseRejectBlock)reject
2362
+ {
2363
+ #if TARGET_OS_OSX
2364
+ RCTExecuteOnMainQueue(^{
2365
+ NSString *targetIdentifier = [self normalizeIdentifier:identifier];
2366
+ NSWindow *window = [targetIdentifier isEqualToString:@"main"]
2367
+ ? [RNWindowManager getMainWindow]
2368
+ : (NSWindow *)self.windows[targetIdentifier];
2369
+ if (!window) {
2370
+ reject(@"E_NOT_FOUND", @"Window not found", nil);
2371
+ return;
2372
+ }
2373
+ window.title = title ?: @"";
2374
+ resolve([self successJson]);
2375
+ });
2376
+ #else
2377
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2378
+ #endif
2379
+ }
2380
+
2381
+ - (void)setWindowToolbarItemText:(NSString *)identifier
2382
+ itemId:(NSString *)itemId
2383
+ text:(NSString *)text
2384
+ resolve:(RCTPromiseResolveBlock)resolve
2385
+ reject:(RCTPromiseRejectBlock)reject
2386
+ {
2387
+ #if TARGET_OS_OSX
2388
+ RCTExecuteOnMainQueue(^{
2389
+ NSString *targetIdentifier = [self normalizeIdentifier:identifier];
2390
+ NSWindow *window = (NSWindow *)self.windows[targetIdentifier];
2391
+ if (!window) {
2392
+ resolve([self failureJson:@"Window not found"]);
2393
+ return;
2394
+ }
2395
+
2396
+ NSString *toolbarIdentifier = [NSString stringWithFormat:@"legend.toolbar.%@", itemId ?: @""];
2397
+ NSToolbarItem *toolbarItem = nil;
2398
+ for (NSToolbarItem *candidate in window.toolbar.items) {
2399
+ if ([candidate.itemIdentifier isEqualToString:toolbarIdentifier]) {
2400
+ toolbarItem = candidate;
2401
+ break;
2402
+ }
2403
+ }
2404
+ if (!toolbarItem) {
2405
+ resolve([self failureJson:@"Toolbar item not found"]);
2406
+ return;
2407
+ }
2408
+
2409
+ if ([toolbarItem.view isKindOfClass:NSTextField.class]) {
2410
+ ((NSTextField *)toolbarItem.view).stringValue = text ?: @"";
2411
+ } else if ([toolbarItem.view isKindOfClass:NSButton.class]) {
2412
+ ((NSButton *)toolbarItem.view).title = text ?: @"";
2413
+ toolbarItem.label = text ?: @"";
2414
+ toolbarItem.paletteLabel = toolbarItem.label;
2415
+ } else {
2416
+ resolve([self failureJson:@"Toolbar item does not support text"]);
2417
+ return;
2418
+ }
2419
+ resolve([self successJson]);
2420
+ });
2421
+ #else
2422
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2423
+ #endif
2424
+ }
2425
+
2426
+ - (void)setPreventDisplaySleep:(BOOL)enabled
2427
+ resolve:(RCTPromiseResolveBlock)resolve
2428
+ reject:(__unused RCTPromiseRejectBlock)reject
2429
+ {
2430
+ #if TARGET_OS_OSX
2431
+ RCTExecuteOnMainQueue(^{
2432
+ if (enabled && !self.displaySleepActivity) {
2433
+ self.displaySleepActivity = [NSProcessInfo.processInfo
2434
+ beginActivityWithOptions:NSActivityIdleDisplaySleepDisabled
2435
+ reason:@"Legend Slides presentation in progress"];
2436
+ } else if (!enabled && self.displaySleepActivity) {
2437
+ [NSProcessInfo.processInfo endActivity:self.displaySleepActivity];
2438
+ self.displaySleepActivity = nil;
2439
+ }
2440
+ resolve([self successJson]);
2441
+ });
2442
+ #else
2443
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2444
+ #endif
2445
+ }
2446
+
2447
+ - (void)focusToolbarSearchItem:(NSString *)identifier
2448
+ itemId:(NSString *)itemId
2449
+ value:(NSString *)value
2450
+ resolve:(RCTPromiseResolveBlock)resolve
2451
+ reject:(RCTPromiseRejectBlock)reject
2452
+ {
2453
+ #if TARGET_OS_OSX
2454
+ RCTExecuteOnMainQueue(^{
2455
+ NSString *targetIdentifier = [self normalizeIdentifier:identifier];
2456
+ NSWindow *window = [targetIdentifier isEqualToString:@"main"]
2457
+ ? [RNWindowManager getMainWindow]
2458
+ : (NSWindow *)self.windows[targetIdentifier];
2459
+ if (!window) {
2460
+ reject(@"E_NOT_FOUND", @"Window not found", nil);
2461
+ return;
2462
+ }
2463
+
2464
+ NSString *toolbarIdentifier = [NSString stringWithFormat:@"legend.toolbar.%@", itemId ?: @""];
2465
+ NSToolbarItem *toolbarItem = nil;
2466
+ for (NSToolbarItem *candidate in window.toolbar.items) {
2467
+ if ([candidate.itemIdentifier isEqualToString:toolbarIdentifier]) {
2468
+ toolbarItem = candidate;
2469
+ break;
2470
+ }
2471
+ }
2472
+ if (!toolbarItem) {
2473
+ reject(@"E_NOT_FOUND", @"Toolbar search item not found", nil);
2474
+ return;
2475
+ }
2476
+
2477
+ NSSearchField *searchField = nil;
2478
+ if (@available(macOS 11.0, *)) {
2479
+ if ([toolbarItem isKindOfClass:NSSearchToolbarItem.class]) {
2480
+ searchField = ((NSSearchToolbarItem *)toolbarItem).searchField;
2481
+ }
2482
+ }
2483
+ if (!searchField && [toolbarItem.view isKindOfClass:NSSearchField.class]) {
2484
+ searchField = (NSSearchField *)toolbarItem.view;
2485
+ }
2486
+ if (!searchField) {
2487
+ id associatedField = objc_getAssociatedObject(toolbarItem, &LegendToolbarSearchFieldKey);
2488
+ if ([associatedField isKindOfClass:NSSearchField.class]) {
2489
+ [self expandToolbarSearchItem:toolbarItem value:value focus:YES sendChange:YES animated:YES];
2490
+ resolve([self successJson]);
2491
+ return;
2492
+ }
2493
+ }
2494
+ if (!searchField) {
2495
+ reject(@"E_NOT_FOUND", @"Toolbar search field not found", nil);
2496
+ return;
2497
+ }
2498
+
2499
+ if (value) searchField.stringValue = value;
2500
+ [self sendToolbarSearchEventForField:searchField submitted:NO shiftKey:NO];
2501
+ [window makeKeyAndOrderFront:nil];
2502
+ [window makeFirstResponder:searchField];
2503
+ resolve([self successJson]);
2504
+ });
2505
+ #else
2506
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2507
+ #endif
2508
+ }
2509
+
2510
+ - (void)closeWindow:(NSString *)identifier resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2511
+ {
2512
+ #if TARGET_OS_OSX
2513
+ RCTExecuteOnMainQueue(^{
2514
+ NSString *targetIdentifier = [self normalizeIdentifier:identifier];
2515
+ NSWindow *window = (NSWindow *)self.windows[targetIdentifier];
2516
+ if (!window) {
2517
+ resolve([self failureJson:@"No window to close"]);
2518
+ return;
2519
+ }
2520
+ [self handleWindowClosedForIdentifier:targetIdentifier closeWindow:YES];
2521
+ resolve([self successJson]);
2522
+ });
2523
+ #else
2524
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2525
+ #endif
2526
+ }
2527
+
2528
+ - (void)closeFrontmostWindow:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2529
+ {
2530
+ #if TARGET_OS_OSX
2531
+ RCTExecuteOnMainQueue(^{
2532
+ NSWindow *window = NSApp.keyWindow ?: NSApp.mainWindow;
2533
+ if (!window) {
2534
+ resolve([self failureJson:@"No window to close"]);
2535
+ return;
2536
+ }
2537
+ NSString *identifier = [self identifierForWindow:window];
2538
+ if (identifier) {
2539
+ [self handleWindowClosedForIdentifier:identifier closeWindow:YES];
2540
+ } else {
2541
+ [window performClose:nil];
2542
+ }
2543
+ resolve([self successJson]);
2544
+ });
2545
+ #else
2546
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2547
+ #endif
2548
+ }
2549
+
2550
+ - (void)showMainWindow:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2551
+ {
2552
+ #if TARGET_OS_OSX
2553
+ RCTExecuteOnMainQueue(^{
2554
+ NSWindow *mainWindow = [RNWindowManager getMainWindow];
2555
+ if (!mainWindow) {
2556
+ resolve([self failureJson:@"Main window not found"]);
2557
+ return;
2558
+ }
2559
+ [NSApp activateIgnoringOtherApps:YES];
2560
+ [mainWindow makeKeyAndOrderFront:nil];
2561
+ resolve([self successJson]);
2562
+ });
2563
+ #else
2564
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2565
+ #endif
2566
+ }
2567
+
2568
+ - (void)hideMainWindow:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2569
+ {
2570
+ #if TARGET_OS_OSX
2571
+ RCTExecuteOnMainQueue(^{
2572
+ NSWindow *mainWindow = [RNWindowManager getMainWindow];
2573
+ if (!mainWindow) {
2574
+ resolve([self failureJson:@"Main window not found"]);
2575
+ return;
2576
+ }
2577
+ [mainWindow orderOut:nil];
2578
+ [NSUserDefaults.standardUserDefaults removeObjectForKey:LegendMainWindowStartupSplitViewKey];
2579
+ [self sendWindowEventWithName:@"onWindowClosed"
2580
+ body:@{@"identifier": @"main", @"moduleName": @"main"}];
2581
+ resolve([self successJson]);
2582
+ });
2583
+ #else
2584
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2585
+ #endif
2586
+ }
2587
+
2588
+ - (void)showWindow:(NSString *)identifier resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2589
+ {
2590
+ #if TARGET_OS_OSX
2591
+ RCTExecuteOnMainQueue(^{
2592
+ NSString *targetIdentifier = [self normalizeIdentifier:identifier];
2593
+ NSWindow *window = (NSWindow *)self.windows[targetIdentifier];
2594
+ if (!window) {
2595
+ reject(@"E_NOT_FOUND", @"Window not found", nil);
2596
+ return;
2597
+ }
2598
+
2599
+ [NSApp activateIgnoringOtherApps:YES];
2600
+ [window makeKeyAndOrderFront:nil];
2601
+ resolve([self successJson]);
2602
+ });
2603
+ #else
2604
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2605
+ #endif
2606
+ }
2607
+
2608
+ - (void)setMainWindowOptions:(NSString *)optionsJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2609
+ {
2610
+ #if TARGET_OS_OSX
2611
+ RCTExecuteOnMainQueue(^{
2612
+ NSWindow *mainWindow = [RNWindowManager getMainWindow];
2613
+ if (!mainWindow) {
2614
+ reject(@"E_NOT_FOUND", @"Main window not found", nil);
2615
+ return;
2616
+ }
2617
+
2618
+ NSDictionary *options = [self parseObjectJSON:optionsJson];
2619
+ id startupSplitView = options[@"windowStyle"][@"startupSplitView"];
2620
+ if ([startupSplitView isKindOfClass:NSDictionary.class]) {
2621
+ // Persist window chrome only, just like AppKit's saved frame. No document data.
2622
+ if (![startupSplitView isEqual:[NSUserDefaults.standardUserDefaults dictionaryForKey:LegendMainWindowStartupSplitViewKey]]) {
2623
+ [NSUserDefaults.standardUserDefaults setObject:startupSplitView forKey:LegendMainWindowStartupSplitViewKey];
2624
+ }
2625
+ } else if (startupSplitView == NSNull.null) {
2626
+ [NSUserDefaults.standardUserDefaults removeObjectForKey:LegendMainWindowStartupSplitViewKey];
2627
+ }
2628
+ LegendApplyWindowOptions(mainWindow, options);
2629
+ [self applyTitlebarControlsFromOptions:options toWindow:mainWindow identifier:@"main"];
2630
+ [self applyToolbarItemsFromOptions:options toWindow:mainWindow identifier:@"main"];
2631
+ [self applyTitlebarMaterialFromOptions:options toWindow:mainWindow identifier:@"main"];
2632
+ resolve([self successJson]);
2633
+ });
2634
+ #else
2635
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2636
+ #endif
2637
+ }
2638
+
2639
+ - (void)setWindowOptions:(NSString *)identifier
2640
+ optionsJson:(NSString *)optionsJson
2641
+ resolve:(RCTPromiseResolveBlock)resolve
2642
+ reject:(RCTPromiseRejectBlock)reject
2643
+ {
2644
+ #if TARGET_OS_OSX
2645
+ RCTExecuteOnMainQueue(^{
2646
+ NSString *targetIdentifier = [self normalizeIdentifier:identifier];
2647
+ NSDictionary *options = [self parseObjectJSON:optionsJson];
2648
+ NSWindow *window = (NSWindow *)self.windows[targetIdentifier];
2649
+ if (!window) {
2650
+ reject(@"E_NOT_FOUND", @"Window not found", nil);
2651
+ return;
2652
+ }
2653
+ [self recordWindowOptions:options
2654
+ identifier:targetIdentifier
2655
+ moduleName:self.moduleNames[targetIdentifier] ?: @""];
2656
+ LegendApplyWindowOptions(window, options);
2657
+ [self applyTitlebarControlsFromOptions:options toWindow:window identifier:targetIdentifier];
2658
+ [self applyToolbarItemsFromOptions:options toWindow:window identifier:targetIdentifier];
2659
+ [self applyTitlebarMaterialFromOptions:options toWindow:window identifier:targetIdentifier];
2660
+ RCTUIView *rootView = self.rootViews[targetIdentifier];
2661
+ LegendSizeRootViewToWindow(rootView, window);
2662
+ resolve([self successJson]);
2663
+ });
2664
+ #else
2665
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2666
+ #endif
2667
+ }
2668
+
2669
+ - (void)getMainWindowFrame:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2670
+ {
2671
+ #if TARGET_OS_OSX
2672
+ RCTExecuteOnMainQueue(^{
2673
+ NSWindow *mainWindow = [RNWindowManager getMainWindow];
2674
+ if (!mainWindow) {
2675
+ reject(@"no_main_window", @"Main window not found", nil);
2676
+ return;
2677
+ }
2678
+ resolve([self jsonStringFromObject:[self frameDictionary:mainWindow.frame]]);
2679
+ });
2680
+ #else
2681
+ resolve([self jsonStringFromObject:@{@"x": @0, @"y": @0, @"width": @0, @"height": @0}]);
2682
+ #endif
2683
+ }
2684
+
2685
+ - (void)setMainWindowFrame:(NSString *)frameJson resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
2686
+ {
2687
+ #if TARGET_OS_OSX
2688
+ RCTExecuteOnMainQueue(^{
2689
+ NSWindow *mainWindow = [RNWindowManager getMainWindow];
2690
+ if (!mainWindow) {
2691
+ reject(@"no_main_window", @"Main window not found", nil);
2692
+ return;
2693
+ }
2694
+ NSDictionary *frameDict = [self parseObjectJSON:frameJson];
2695
+ NSRect newFrame = NSMakeRect([frameDict[@"x"] doubleValue],
2696
+ [frameDict[@"y"] doubleValue],
2697
+ [frameDict[@"width"] doubleValue],
2698
+ [frameDict[@"height"] doubleValue]);
2699
+ [mainWindow setFrame:newFrame display:YES animate:NO];
2700
+ resolve([self successJson]);
2701
+ });
2702
+ #else
2703
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2704
+ #endif
2705
+ }
2706
+
2707
+ - (void)setWindowBlur:(NSString *)identifier
2708
+ radius:(double)radius
2709
+ durationMs:(double)durationMs
2710
+ resolve:(RCTPromiseResolveBlock)resolve
2711
+ reject:(RCTPromiseRejectBlock)reject
2712
+ {
2713
+ #if TARGET_OS_OSX
2714
+ RCTExecuteOnMainQueue(^{
2715
+ NSString *targetIdentifier = [self normalizeIdentifier:identifier];
2716
+ NSWindow *window = (NSWindow *)self.windows[targetIdentifier];
2717
+ if (!window) {
2718
+ reject(@"E_NOT_FOUND", @"Target window not found for blur animation", nil);
2719
+ return;
2720
+ }
2721
+
2722
+ RCTUIView *rootView = self.rootViews[targetIdentifier];
2723
+ NSView *contentView = window.contentView ?: rootView;
2724
+ if (!contentView) {
2725
+ reject(@"no_content_view", @"Window does not have a content view to blur", nil);
2726
+ return;
2727
+ }
2728
+
2729
+ contentView.wantsLayer = YES;
2730
+ contentView.layerUsesCoreImageFilters = YES;
2731
+ if (!contentView.layer) {
2732
+ contentView.layer = [CALayer layer];
2733
+ }
2734
+ contentView.layer.masksToBounds = NO;
2735
+
2736
+ CIFilter *blurFilter = self.windowBlurFilters[targetIdentifier];
2737
+ if (!blurFilter) {
2738
+ blurFilter = [CIFilter filterWithName:@"CIGaussianBlur"];
2739
+ if (!blurFilter) {
2740
+ reject(@"filter_unavailable", @"CIGaussianBlur filter could not be created", nil);
2741
+ return;
2742
+ }
2743
+ blurFilter.name = @"legendOverlayBlur";
2744
+ [blurFilter setDefaults];
2745
+ [blurFilter setValue:@0 forKey:kCIInputRadiusKey];
2746
+ self.windowBlurFilters[targetIdentifier] = blurFilter;
2747
+ }
2748
+
2749
+ BOOL filterAttached = NO;
2750
+ for (id existingFilter in contentView.layer.filters ?: @[]) {
2751
+ if ([existingFilter isKindOfClass:CIFilter.class] && [[existingFilter name] isEqualToString:@"legendOverlayBlur"]) {
2752
+ filterAttached = YES;
2753
+ break;
2754
+ }
2755
+ }
2756
+ if (!filterAttached) {
2757
+ NSMutableArray *filters = [NSMutableArray arrayWithArray:contentView.layer.filters ?: @[]];
2758
+ [filters addObject:blurFilter];
2759
+ contentView.layer.filters = filters;
2760
+ }
2761
+
2762
+ CGFloat targetRadius = radius;
2763
+ CGFloat currentRadius = [[blurFilter valueForKey:kCIInputRadiusKey] doubleValue];
2764
+ NSTimeInterval durationSeconds = durationMs / 1000.0;
2765
+ if (durationSeconds <= 0) {
2766
+ [CATransaction begin];
2767
+ [CATransaction setDisableActions:YES];
2768
+ [blurFilter setValue:@(targetRadius) forKey:kCIInputRadiusKey];
2769
+ [CATransaction commit];
2770
+ [contentView.layer removeAnimationForKey:@"legendOverlayBlurAnimation"];
2771
+ resolve([self successJson]);
2772
+ return;
2773
+ }
2774
+
2775
+ [contentView.layer removeAnimationForKey:@"legendOverlayBlurAnimation"];
2776
+ CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"filters.legendOverlayBlur.inputRadius"];
2777
+ animation.fromValue = @(currentRadius);
2778
+ animation.toValue = @(targetRadius);
2779
+ animation.duration = durationSeconds;
2780
+ animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
2781
+ animation.fillMode = kCAFillModeForwards;
2782
+ animation.removedOnCompletion = NO;
2783
+
2784
+ [CATransaction begin];
2785
+ [CATransaction setCompletionBlock:^{
2786
+ [CATransaction begin];
2787
+ [CATransaction setDisableActions:YES];
2788
+ [blurFilter setValue:@(targetRadius) forKey:kCIInputRadiusKey];
2789
+ [CATransaction commit];
2790
+ resolve([self successJson]);
2791
+ }];
2792
+ [contentView.layer addAnimation:animation forKey:@"legendOverlayBlurAnimation"];
2793
+ [CATransaction commit];
2794
+ });
2795
+ #else
2796
+ resolve([self failureJson:@"WindowManager is only available on macOS"]);
2797
+ #endif
2798
+ }
2799
+
2800
+ #if TARGET_OS_OSX
2801
+ + (NSWindow *)getMainWindow
2802
+ {
2803
+ // AppKit's window list includes precreated/restored document windows and has
2804
+ // no stable ordering. Both supported hosts own the main window on the delegate.
2805
+ id<RNWindowManagerRootViewFactoryProvider> delegate = (id)NSApplication.sharedApplication.delegate;
2806
+ return [delegate respondsToSelector:@selector(window)] ? delegate.window : nil;
2807
+ }
2808
+
2809
+ - (void)setupMainWindowObservers
2810
+ {
2811
+ if (self.mainWindowObserversInstalled) {
2812
+ return;
2813
+ }
2814
+ NSWindow *mainWindow = [RNWindowManager getMainWindow];
2815
+ if (!mainWindow) {
2816
+ return;
2817
+ }
2818
+ self.mainWindowObserversInstalled = YES;
2819
+ NSNotificationCenter *center = NSNotificationCenter.defaultCenter;
2820
+ [center addObserver:self selector:@selector(mainWindowDidMove:) name:NSWindowDidMoveNotification object:mainWindow];
2821
+ [center addObserver:self selector:@selector(mainWindowDidResize:) name:NSWindowDidResizeNotification object:mainWindow];
2822
+ [center addObserver:self selector:@selector(mainWindowDidBecomeKey:) name:NSWindowDidBecomeKeyNotification object:mainWindow];
2823
+ }
2824
+
2825
+ - (void)mainWindowDidMove:(NSNotification *)notification
2826
+ {
2827
+ NSWindow *window = notification.object;
2828
+ [self sendWindowEventWithName:@"onMainWindowMoved" body:[self frameDictionary:window.frame]];
2829
+ }
2830
+
2831
+ - (void)mainWindowDidResize:(NSNotification *)notification
2832
+ {
2833
+ NSWindow *window = notification.object;
2834
+ RCTUIView *rootView = LegendManagedRootView(window);
2835
+ if (rootView) {
2836
+ LegendSizeRootViewToWindow(rootView, window);
2837
+ }
2838
+ [self sendWindowEventWithName:@"onMainWindowResized" body:[self frameDictionary:window.frame]];
2839
+ }
2840
+
2841
+ - (void)mainWindowDidBecomeKey:(NSNotification *)notification
2842
+ {
2843
+ [self sendWindowEventWithName:@"onWindowFocused" body:@{@"identifier": @"main", @"moduleName": @"main"}];
2844
+ }
2845
+
2846
+ - (void)windowDidBecomeKey:(NSNotification *)notification
2847
+ {
2848
+ NSString *identifier = [self identifierForWindow:notification.object];
2849
+ if (!identifier) {
2850
+ return;
2851
+ }
2852
+ [self sendWindowEventWithName:@"onWindowFocused"
2853
+ body:@{@"identifier": identifier, @"moduleName": self.moduleNames[identifier] ?: @""}];
2854
+ }
2855
+
2856
+ - (void)windowDidMove:(NSNotification *)notification
2857
+ {
2858
+ NSWindow *window = notification.object;
2859
+ NSString *identifier = [self identifierForWindow:window];
2860
+ if (!identifier) {
2861
+ return;
2862
+ }
2863
+ [self sendWindowEventWithName:@"onWindowMoved"
2864
+ body:@{
2865
+ @"identifier": identifier,
2866
+ @"moduleName": self.moduleNames[identifier] ?: @"",
2867
+ @"frame": [self frameDictionary:window.frame],
2868
+ }];
2869
+ }
2870
+
2871
+ - (void)windowDidResize:(NSNotification *)notification
2872
+ {
2873
+ NSWindow *window = notification.object;
2874
+ NSString *identifier = [self identifierForWindow:window];
2875
+ if (!identifier) {
2876
+ return;
2877
+ }
2878
+ RCTUIView *rootView = LegendManagedRootView(window);
2879
+ if (rootView) {
2880
+ LegendSizeRootViewToWindow(rootView, window);
2881
+ }
2882
+ [self sendWindowEventWithName:@"onWindowResized"
2883
+ body:@{
2884
+ @"identifier": identifier,
2885
+ @"moduleName": self.moduleNames[identifier] ?: @"",
2886
+ @"frame": [self frameDictionary:window.frame],
2887
+ }];
2888
+ }
2889
+
2890
+ - (void)windowWillClose:(NSNotification *)notification
2891
+ {
2892
+ NSString *identifier = [self identifierForWindow:notification.object];
2893
+ if (identifier) {
2894
+ [self handleWindowClosedForIdentifier:identifier closeWindow:NO];
2895
+ }
2896
+ }
2897
+
2898
+ - (void)sendWindowEventWithName:(NSString *)eventName body:(id)body
2899
+ {
2900
+ #if __has_include(<RNDesktopApp/SparkDesktop.h>)
2901
+ NSString *type = nil;
2902
+ if ([eventName isEqual:@"onTitlebarControlPressed"]) type = @"titleBarAction";
2903
+ else if ([eventName isEqual:@"onToolbarItemSelected"]) type = body[@"action"] ? @"toolbarMenuAction" : [body[@"kind"] isEqual:@"selection"] ? @"toolbarSelectionChanged" : @"toolbarAction";
2904
+ else if ([eventName isEqual:@"onToolbarSearch"]) type = [body[@"submitted"] boolValue] ? @"toolbarSearchSubmitted" : @"toolbarSearchChanged";
2905
+ if (type) {
2906
+ NSMutableDictionary *event = [body mutableCopy]; event[@"type"] = type; event[@"windowId"] = body[@"identifier"] ?: @"";
2907
+ SparkEmit(event); return;
2908
+ }
2909
+ #endif
2910
+
2911
+ if (!self.hasListeners) {
2912
+ return;
2913
+ }
2914
+ [self sendEventWithName:eventName body:body];
2915
+ }
2916
+
2917
+ #if __has_include(<RNDesktopApp/SparkDesktop.h>)
2918
+ - (void)sparkDesktopEvent:(NSNotification *)notification
2919
+ {
2920
+ NSDictionary *event = notification.userInfo;
2921
+ if ([event[@"type"] isEqual:@"reopen"]) {
2922
+ // A visible host can have been hidden while other managed windows remained.
2923
+ BOOL mainHidden = [SparkLifecycleConfiguration()[@"mainWindow"][@"hidden"] boolValue] || [SparkLifecycleConfiguration()[@"mainWindow"][@"reopenBehavior"] isEqual:@"visibleWindows"];
2924
+ NSNumber *visible = mainHidden ? event[@"hasVisibleWindows"] : event[@"mainWindowWasVisible"];
2925
+ [self sendWindowEventWithName:@"onApplicationReopenRequested" body:@{@"hasVisibleWindows": visible ?: @NO}];
2926
+ } else if ([event[@"type"] isEqual:@"closeRequested"] && [event[@"windowId"] isEqual:@"main"]) {
2927
+ [self mainWindowCloseRequested:notification];
2928
+ }
2929
+ }
2930
+ #endif
2931
+
2932
+ - (void)applicationReopenRequested:(NSNotification *)notification
2933
+ {
2934
+ NSNumber *hasVisibleWindows = notification.userInfo[@"hasVisibleWindows"];
2935
+ [self sendWindowEventWithName:@"onApplicationReopenRequested"
2936
+ body:@{@"hasVisibleWindows": hasVisibleWindows ?: @NO}];
2937
+ }
2938
+
2939
+ - (void)applicationWillTerminate:(NSNotification *)notification
2940
+ {
2941
+ LegendApplicationIsTerminating = YES;
2942
+ }
2943
+
2944
+ - (void)mainWindowCloseRequested:(NSNotification *)notification
2945
+ {
2946
+ [self sendWindowEventWithName:@"onWindowCloseRequested"
2947
+ body:@{@"identifier": @"main", @"moduleName": @"main"}];
2948
+ }
2949
+
2950
+ - (void)screensChanged:(NSNotification *)notification
2951
+ {
2952
+ [self sendWindowEventWithName:@"onDisplaysChanged" body:[self displayDictionaries]];
2953
+ }
2954
+
2955
+ - (BOOL)windowShouldClose:(NSWindow *)window
2956
+ {
2957
+ NSString *identifier = [self identifierForWindow:window];
2958
+ if (!identifier) {
2959
+ return NO;
2960
+ }
2961
+
2962
+ if ([self.closeRequestIdentifiers containsObject:identifier]) {
2963
+ [self sendWindowEventWithName:@"onWindowCloseRequested"
2964
+ body:@{@"identifier": identifier, @"moduleName": self.moduleNames[identifier] ?: @""}];
2965
+ return NO;
2966
+ }
2967
+
2968
+ return YES;
2969
+ }
2970
+
2971
+ - (NSDictionary *)frameDictionary:(NSRect)frame
2972
+ {
2973
+ return @{
2974
+ @"x": @(frame.origin.x),
2975
+ @"y": @(frame.origin.y),
2976
+ @"width": @(frame.size.width),
2977
+ @"height": @(frame.size.height),
2978
+ };
2979
+ }
2980
+
2981
+ - (NSString *)identifierForWindow:(NSWindow *)window
2982
+ {
2983
+ if (!window) {
2984
+ return nil;
2985
+ }
2986
+ for (NSString *identifier in self.windows.allKeys) {
2987
+ if ((NSWindow *)self.windows[identifier] == window) {
2988
+ return identifier;
2989
+ }
2990
+ }
2991
+ return nil;
2992
+ }
2993
+
2994
+ - (void)handleWindowClosedForIdentifier:(NSString *)identifier closeWindow:(BOOL)shouldCloseWindow
2995
+ {
2996
+ NSString *moduleName = self.moduleNames[identifier] ?: @"";
2997
+ NSWindow *window = (NSWindow *)self.windows[identifier];
2998
+ RCTUIView *rootView = self.rootViews[identifier];
2999
+ CIFilter *blurFilter = self.windowBlurFilters[identifier];
3000
+ if (blurFilter) {
3001
+ NSView *contentView = window.contentView ?: rootView;
3002
+ if (contentView.layer) {
3003
+ [contentView.layer removeAnimationForKey:@"legendOverlayBlurAnimation"];
3004
+ NSMutableArray *remainingFilters = [NSMutableArray array];
3005
+ for (id filter in contentView.layer.filters ?: @[]) {
3006
+ if ([filter isKindOfClass:CIFilter.class] && [[filter name] isEqualToString:@"legendOverlayBlur"]) {
3007
+ continue;
3008
+ }
3009
+ [remainingFilters addObject:filter];
3010
+ }
3011
+ contentView.layer.filters = remainingFilters;
3012
+ }
3013
+ }
3014
+ [self.windowBlurFilters removeObjectForKey:identifier];
3015
+ if (window) {
3016
+ [self removeTitlebarMaterialForIdentifier:identifier];
3017
+ [self removeTitlebarControlsForIdentifier:identifier fromWindow:window];
3018
+ NSToolbar *toolbar = window.toolbar;
3019
+ if (toolbar) {
3020
+ if (toolbar.identifier) {
3021
+ [self.toolbarItemConfigs removeObjectForKey:toolbar.identifier];
3022
+ }
3023
+ toolbar.delegate = nil;
3024
+ while (toolbar.items.count > 0) {
3025
+ [toolbar removeItemAtIndex:toolbar.items.count - 1];
3026
+ }
3027
+ window.toolbar = nil;
3028
+ }
3029
+ window.delegate = nil;
3030
+ if (rootView) {
3031
+ [rootView removeFromSuperview];
3032
+ objc_setAssociatedObject(window, &LegendManagedRootViewKey, nil, OBJC_ASSOCIATION_ASSIGN);
3033
+ }
3034
+ NSView *emptyContentView = [[NSView alloc] initWithFrame:window.contentView ? window.contentView.frame : NSZeroRect];
3035
+ emptyContentView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
3036
+ window.contentView = emptyContentView;
3037
+ } else {
3038
+ [self.titlebarMaterialViews removeObjectForKey:identifier];
3039
+ [self.titlebarAccessoryControllers removeObjectForKey:identifier];
3040
+ }
3041
+ [self.closeRequestIdentifiers removeObject:identifier];
3042
+ [self.toolbarItemConfigs removeObjectForKey:identifier];
3043
+ NSDictionary *windowOptions = self.windowOptions[identifier];
3044
+ if (!LegendApplicationIsTerminating && [windowOptions[@"restoreOnLaunch"] boolValue]) {
3045
+ LegendSetRestorableWindowOptions(identifier, nil);
3046
+ }
3047
+ [self.windows removeObjectForKey:identifier];
3048
+ [self.rootViews removeObjectForKey:identifier];
3049
+ [self.moduleNames removeObjectForKey:identifier];
3050
+ [self.windowOptions removeObjectForKey:identifier];
3051
+ if (shouldCloseWindow && window) {
3052
+ [window close];
3053
+ }
3054
+ [self sendWindowEventWithName:@"onWindowClosed" body:@{@"identifier": identifier ?: @"", @"moduleName": moduleName ?: @""}];
3055
+ }
3056
+
3057
+ - (void)recordWindowOptions:(NSDictionary *)options identifier:(NSString *)identifier moduleName:(NSString *)moduleName
3058
+ {
3059
+ if (identifier.length == 0 || ![options isKindOfClass:NSDictionary.class]) {
3060
+ return;
3061
+ }
3062
+
3063
+ NSDictionary *persistedOptions = LegendFindRestorableWindowOptions(identifier);
3064
+ NSDictionary *previousOptions = self.windowOptions[identifier] ?: persistedOptions ?: @{};
3065
+ NSMutableDictionary *mergedOptions = [previousOptions mutableCopy];
3066
+ [mergedOptions addEntriesFromDictionary:options];
3067
+ NSDictionary *previousWindowStyle = [previousOptions[@"windowStyle"] isKindOfClass:NSDictionary.class]
3068
+ ? previousOptions[@"windowStyle"]
3069
+ : @{};
3070
+ NSDictionary *nextWindowStyle = [options[@"windowStyle"] isKindOfClass:NSDictionary.class]
3071
+ ? options[@"windowStyle"]
3072
+ : nil;
3073
+ if (nextWindowStyle) {
3074
+ NSMutableDictionary *mergedWindowStyle = [previousWindowStyle mutableCopy];
3075
+ [mergedWindowStyle addEntriesFromDictionary:nextWindowStyle];
3076
+ mergedOptions[@"windowStyle"] = mergedWindowStyle;
3077
+ }
3078
+ mergedOptions[@"identifier"] = identifier;
3079
+ if (moduleName.length > 0) {
3080
+ mergedOptions[@"moduleName"] = moduleName;
3081
+ }
3082
+ self.windowOptions[identifier] = mergedOptions;
3083
+
3084
+ if (LegendDictionaryHasKey(options, @"restoreOnLaunch")) {
3085
+ if ([options[@"restoreOnLaunch"] boolValue]) {
3086
+ LegendSetRestorableWindowOptions(identifier, mergedOptions);
3087
+ } else {
3088
+ LegendSetRestorableWindowOptions(identifier, nil);
3089
+ LegendRemovePrecreatedWindow(identifier);
3090
+ }
3091
+ } else if ([mergedOptions[@"restoreOnLaunch"] boolValue]) {
3092
+ LegendSetRestorableWindowOptions(identifier, mergedOptions);
3093
+ }
3094
+ }
3095
+
3096
+ - (NSDictionary *)initialPropsFromOptions:(NSDictionary *)options
3097
+ {
3098
+ id initialPropsCandidate = options[@"initialProperties"];
3099
+ return [initialPropsCandidate isKindOfClass:NSDictionary.class] ? initialPropsCandidate : nil;
3100
+ }
3101
+
3102
+ - (NSString *)normalizeIdentifier:(NSString *)identifier
3103
+ {
3104
+ return [identifier isKindOfClass:NSString.class] && identifier.length > 0 ? identifier : @"default";
3105
+ }
3106
+ #endif
3107
+
3108
+ - (void)invalidate
3109
+ {
3110
+ // React's host restarts the existing surfaces on reload. Keep their native
3111
+ // registry, but retire this runtime's observers and event delivery.
3112
+ self.hasListeners = NO;
3113
+ #if TARGET_OS_OSX
3114
+ [NSNotificationCenter.defaultCenter removeObserver:self];
3115
+ #endif
3116
+ [super invalidate];
3117
+ }
3118
+
3119
+ - (void)dealloc
3120
+ {
3121
+ #if TARGET_OS_OSX
3122
+ if (self.displaySleepActivity) {
3123
+ [NSProcessInfo.processInfo endActivity:self.displaySleepActivity];
3124
+ self.displaySleepActivity = nil;
3125
+ }
3126
+ [NSNotificationCenter.defaultCenter removeObserver:self];
3127
+ #endif
3128
+ }
3129
+
3130
+ @end