pythonnative 0.35.0__py3-none-any.whl → 0.37.0__py3-none-any.whl
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.
- pythonnative/__init__.py +6 -4
- pythonnative/_ios_log.py +1 -1
- pythonnative/alerts.py +34 -88
- pythonnative/bootstrap.py +90 -0
- pythonnative/bridge/__init__.py +306 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +164 -0
- pythonnative/bridge/fake.py +236 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/cli/pn.py +24 -6
- pythonnative/components/layout.py +65 -3
- pythonnative/gestures.py +13 -10
- pythonnative/hooks.py +41 -26
- pythonnative/hosts/__init__.py +13 -36
- pythonnative/hosts/native.py +348 -0
- pythonnative/images.py +6 -5
- pythonnative/mutations.py +21 -0
- pythonnative/native_modules/__init__.py +30 -29
- pythonnative/native_modules/app_state.py +10 -6
- pythonnative/native_modules/battery.py +20 -84
- pythonnative/native_modules/biometrics.py +11 -124
- pythonnative/native_modules/camera.py +15 -291
- pythonnative/native_modules/clipboard.py +14 -109
- pythonnative/native_modules/desktop.py +322 -0
- pythonnative/native_modules/file_system.py +15 -21
- pythonnative/native_modules/haptics.py +19 -129
- pythonnative/native_modules/linking.py +24 -137
- pythonnative/native_modules/location.py +28 -180
- pythonnative/native_modules/net_info.py +25 -135
- pythonnative/native_modules/notifications.py +36 -257
- pythonnative/native_modules/permissions.py +20 -184
- pythonnative/native_modules/registry.py +513 -0
- pythonnative/native_modules/secure_store.py +18 -155
- pythonnative/native_modules/share.py +8 -113
- pythonnative/native_views/__init__.py +67 -86
- pythonnative/native_views/bridge_backend.py +264 -0
- pythonnative/platform.py +19 -15
- pythonnative/platform_metrics.py +3 -3
- pythonnative/project/android.py +0 -3
- pythonnative/project/builder.py +38 -16
- pythonnative/project/config.py +12 -2
- pythonnative/project/plugins.py +366 -0
- pythonnative/py.typed +0 -0
- pythonnative/runtime.py +35 -203
- pythonnative/sdk/__init__.py +48 -33
- pythonnative/sdk/_components.py +57 -76
- pythonnative/storage.py +18 -243
- pythonnative/templates/android_template/app/build.gradle +6 -9
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +34 -166
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -151
- pythonnative/templates/android_template/pythonnative/build.gradle +48 -0
- pythonnative/templates/android_template/pythonnative/src/main/AndroidManifest.xml +11 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PNBridge.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +249 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +32 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +115 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +35 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +33 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +156 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +87 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +245 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +179 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +97 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +135 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +88 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/Navigator.kt +116 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +223 -0
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNAccessibilityDelegate.kt +5 -4
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNBorderDrawable.kt +12 -3
- pythonnative/templates/android_template/pythonnative/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNFrameLayout.kt +9 -10
- pythonnative/templates/android_template/{app/src/main/java/com/pythonnative/android_template → pythonnative/src/main/java/com/pythonnative/runtime/views}/PNVirtualListView.java +9 -12
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/templates/android_template/pythonnative/src/test/java/com/pythonnative/runtime/PromiseTest.kt +75 -0
- pythonnative/templates/android_template/settings.gradle +1 -0
- pythonnative/templates/ios_template/PythonNativeKit/Package.swift +21 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Animation/PNAnimator.swift +301 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNBridge.swift +191 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNRegistry.swift +140 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNTransaction.swift +165 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNButtonManagers.swift +206 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNImageManager.swift +221 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextInputManager.swift +289 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +257 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/HostModule.swift +158 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PNNativeModule.swift +125 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/PermissionModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Screens/PNViewController.swift +251 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/templates/ios_template/PythonNativeKit/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNManagerTests.swift +124 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNModuleTests.swift +98 -0
- pythonnative/templates/ios_template/PythonNativeKit/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +8 -50
- pythonnative/templates/ios_template/ios_template/Info.plist +4 -4
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +38 -46
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +11 -16
- pythonnative/templates/ios_template/ios_template/ViewController.swift +12 -142
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +47 -1
- pythonnative/utils.py +4 -82
- pythonnative/virtual_rows.py +3 -3
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/METADATA +3 -5
- pythonnative-0.37.0.dist-info/RECORD +263 -0
- pythonnative/hosts/android.py +0 -269
- pythonnative/hosts/ios.py +0 -404
- pythonnative/native_views/android.py +0 -3702
- pythonnative/native_views/ios.py +0 -5253
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/Navigator.kt +0 -43
- pythonnative-0.35.0.dist-info/RECORD +0 -155
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/WHEEL +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/entry_points.txt +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/licenses/LICENSE +0 -0
- {pythonnative-0.35.0.dist-info → pythonnative-0.37.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.view.ViewGroup
|
|
7
|
+
import android.widget.FrameLayout
|
|
8
|
+
import com.pythonnative.runtime.PNBridge
|
|
9
|
+
import com.pythonnative.runtime.animation.PNAnimator
|
|
10
|
+
import com.pythonnative.runtime.bridge.JsonUtil
|
|
11
|
+
import com.pythonnative.runtime.bridge.ViewRecord
|
|
12
|
+
import com.pythonnative.runtime.bridge.present
|
|
13
|
+
import org.json.JSONArray
|
|
14
|
+
import org.json.JSONObject
|
|
15
|
+
import kotlin.math.max
|
|
16
|
+
import kotlin.math.min
|
|
17
|
+
import kotlin.math.roundToInt
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Base class for one element type's native implementation.
|
|
21
|
+
*
|
|
22
|
+
* Subclasses implement [createView] (construct the widget) and
|
|
23
|
+
* [applyProps] (apply visual props); the base class owns frame
|
|
24
|
+
* application, intrinsic measurement, child management, and the
|
|
25
|
+
* animation hooks. All geometry crossing this API is in dp.
|
|
26
|
+
*/
|
|
27
|
+
abstract class ComponentManager {
|
|
28
|
+
/** Construct the widget for `tag`; `props` may be consulted for creation-time choices. */
|
|
29
|
+
abstract fun createView(context: Context, tag: Long, props: JSONObject): View
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Apply `props` to `view`. `initial` is `true` for the create op;
|
|
33
|
+
* on updates `props` holds only the changed keys (JSON null means
|
|
34
|
+
* removed). The merged props are always available via [propsOf].
|
|
35
|
+
*/
|
|
36
|
+
open fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
37
|
+
ViewStyler.apply(view, props)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Apply changed props (the `u` op). */
|
|
41
|
+
fun update(view: View, changed: JSONObject) = applyProps(view, changed, false)
|
|
42
|
+
|
|
43
|
+
/** Ensure `child` sits at `index` under `parent` (move-aware, clamped). */
|
|
44
|
+
open fun insertChild(parent: View, child: View, index: Int) {
|
|
45
|
+
val group = parent as? ViewGroup ?: return
|
|
46
|
+
ViewChildren.insert(group, child, index)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Detach `child` from `parent` without destroying it. */
|
|
50
|
+
open fun removeChild(parent: View, child: View) {
|
|
51
|
+
(parent as? ViewGroup)?.removeView(child)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Release resources and detach from the parent. */
|
|
55
|
+
open fun destroy(view: View) {
|
|
56
|
+
try {
|
|
57
|
+
teardown(view)
|
|
58
|
+
} catch (e: Exception) {
|
|
59
|
+
Log.d(PNBridge.TAG, "teardown failed for ${view.javaClass.simpleName}", e)
|
|
60
|
+
}
|
|
61
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Subclass hook for extra cleanup before the view is released. */
|
|
65
|
+
protected open fun teardown(view: View) {}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Position `view` inside its parent's `FrameLayout` via margin layout
|
|
69
|
+
* params. Non-finite values are clamped; sizes are never negative.
|
|
70
|
+
*/
|
|
71
|
+
open fun setFrame(view: View, x: Double, y: Double, width: Double, height: Double) {
|
|
72
|
+
val pxX = px(finite(x))
|
|
73
|
+
val pxY = px(finite(y))
|
|
74
|
+
val pxW = max(0, px(finite(width)))
|
|
75
|
+
val pxH = max(0, px(finite(height)))
|
|
76
|
+
val lp = view.layoutParams
|
|
77
|
+
val params: ViewGroup.LayoutParams = if (lp == null) {
|
|
78
|
+
FrameLayout.LayoutParams(pxW, pxH)
|
|
79
|
+
} else {
|
|
80
|
+
lp.width = pxW
|
|
81
|
+
lp.height = pxH
|
|
82
|
+
lp
|
|
83
|
+
}
|
|
84
|
+
if (params is ViewGroup.MarginLayoutParams) {
|
|
85
|
+
params.leftMargin = pxX
|
|
86
|
+
params.topMargin = pxY
|
|
87
|
+
params.rightMargin = 0
|
|
88
|
+
params.bottomMargin = 0
|
|
89
|
+
}
|
|
90
|
+
view.layoutParams = params
|
|
91
|
+
val record = recordOf(view)
|
|
92
|
+
if (record != null) {
|
|
93
|
+
record.frame = doubleArrayOf(x, y, width, height)
|
|
94
|
+
if (record.props.present("hit_slop") || record.state["hit_slop_active"] == true) {
|
|
95
|
+
ViewStyler.updateHitSlop(view)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Natural size of `view` under the constraints, in dp. Either
|
|
102
|
+
* constraint may be `1e6` or infinite meaning unconstrained.
|
|
103
|
+
*/
|
|
104
|
+
open fun measure(view: View, maxWidth: Double, maxHeight: Double): FloatArray {
|
|
105
|
+
val density = PNBridge.density()
|
|
106
|
+
val wSpec = measureSpec(maxWidth, density)
|
|
107
|
+
val hSpec = measureSpec(maxHeight, density)
|
|
108
|
+
view.measure(wSpec, hSpec)
|
|
109
|
+
return floatArrayOf(view.measuredWidth / density, view.measuredHeight / density)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Run an imperative command; unknown names return `null`. */
|
|
113
|
+
open fun command(view: View, name: String, args: JSONObject): Any? = null
|
|
114
|
+
|
|
115
|
+
/** Apply one Python-driven animation frame immediately. */
|
|
116
|
+
open fun setAnimatedProperty(view: View, prop: String, value: Any?) {
|
|
117
|
+
PNAnimator.setProperty(view, prop, value)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Start a natively driven animation; `false` asks Python to tick it instead. */
|
|
121
|
+
open fun startAnimation(view: View, id: Long, prop: String, spec: JSONObject): Boolean =
|
|
122
|
+
PNAnimator.start(view, id, prop, spec)
|
|
123
|
+
|
|
124
|
+
/** Cancel a native animation and return the presentation value, if readable. */
|
|
125
|
+
open fun cancelAnimation(view: View, id: Long): Any? = PNAnimator.cancel(id)
|
|
126
|
+
|
|
127
|
+
// ------------------------------------------------------------------
|
|
128
|
+
// Helpers for subclasses
|
|
129
|
+
// ------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/** The registry record for `view`, or `null` for foreign views. */
|
|
132
|
+
protected fun recordOf(view: View): ViewRecord? = PNBridge.registry.recordFor(view)
|
|
133
|
+
|
|
134
|
+
/** Merged props for `view` (empty for foreign views). */
|
|
135
|
+
protected fun propsOf(view: View): JSONObject = recordOf(view)?.props ?: JSONObject()
|
|
136
|
+
|
|
137
|
+
/** Manager-private state map for `view`. */
|
|
138
|
+
protected fun stateOf(view: View): MutableMap<String, Any?> = recordOf(view)?.state ?: HashMap()
|
|
139
|
+
|
|
140
|
+
/** Dispatch event `name` with positional `args` for `view`'s tag. */
|
|
141
|
+
protected fun fire(view: View, name: String, vararg args: Any?): Boolean = PNEvents.fire(view, name, *args)
|
|
142
|
+
|
|
143
|
+
/** Dispatch event `name` and return Python's (JSON) reply. */
|
|
144
|
+
protected fun fireForResult(view: View, name: String, vararg args: Any?): String? =
|
|
145
|
+
PNEvents.fireForResult(view, name, *args)
|
|
146
|
+
|
|
147
|
+
/** Whether the element wired a callback named `name`. */
|
|
148
|
+
protected fun hasEvent(view: View, name: String): Boolean = recordOf(view)?.hasEvent(name) ?: false
|
|
149
|
+
|
|
150
|
+
/** dp to whole pixels. */
|
|
151
|
+
protected fun px(dp: Double): Int = (dp * PNBridge.density()).roundToInt()
|
|
152
|
+
|
|
153
|
+
/** dp to pixels as a float. */
|
|
154
|
+
protected fun pxF(dp: Double): Float = (dp * PNBridge.density()).toFloat()
|
|
155
|
+
|
|
156
|
+
/** pixels to dp. */
|
|
157
|
+
protected fun dp(px: Number): Double = px.toDouble() / PNBridge.density()
|
|
158
|
+
|
|
159
|
+
private fun finite(v: Double): Double = if (v.isNaN() || v.isInfinite()) 0.0 else v
|
|
160
|
+
|
|
161
|
+
private fun measureSpec(limit: Double, density: Float): Int {
|
|
162
|
+
if (limit.isNaN() || limit.isInfinite() || limit >= 1e6) {
|
|
163
|
+
return View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
|
|
164
|
+
}
|
|
165
|
+
val px = min((max(0.0, limit) * density).toInt(), (1 shl 29))
|
|
166
|
+
return View.MeasureSpec.makeMeasureSpec(px, View.MeasureSpec.AT_MOST)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Move-aware indexed insertion into any `ViewGroup`. */
|
|
171
|
+
object ViewChildren {
|
|
172
|
+
fun insert(parent: ViewGroup, child: View, index: Int) {
|
|
173
|
+
val current = child.parent as? ViewGroup
|
|
174
|
+
if (current === parent) {
|
|
175
|
+
val currentIndex = parent.indexOfChild(child)
|
|
176
|
+
val target = max(0, min(index, parent.childCount - 1))
|
|
177
|
+
if (currentIndex == target) return
|
|
178
|
+
parent.removeView(child)
|
|
179
|
+
parent.addView(child, max(0, min(target, parent.childCount)))
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
current?.removeView(child)
|
|
183
|
+
if (child.layoutParams == null) {
|
|
184
|
+
child.layoutParams = FrameLayout.LayoutParams(0, 0)
|
|
185
|
+
}
|
|
186
|
+
parent.addView(child, max(0, min(index, parent.childCount)))
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Event dispatch keyed by view identity. */
|
|
191
|
+
object PNEvents {
|
|
192
|
+
/** Dispatch `name` for `view` with positional `args`; `false` when the view is unknown. */
|
|
193
|
+
fun fire(view: View, name: String, vararg args: Any?): Boolean {
|
|
194
|
+
val tag = PNBridge.registry.tagOf(view) ?: return false
|
|
195
|
+
fireTag(tag, name, JsonUtil.args(*args))
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Dispatch `name` for `view` and return Python's JSON reply. */
|
|
200
|
+
fun fireForResult(view: View, name: String, vararg args: Any?): String? {
|
|
201
|
+
val tag = PNBridge.registry.tagOf(view) ?: return null
|
|
202
|
+
return fireTag(tag, name, JsonUtil.args(*args))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Dispatch `name` for `tag` with an already built argument array. */
|
|
206
|
+
fun fireTag(tag: Long, name: String, args: JSONArray): String? =
|
|
207
|
+
PNBridge.callPython("event", tag, name, args.toString())
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Fallback for unknown element types: an empty container that hosts children. */
|
|
211
|
+
class PlaceholderManager : ComponentManager() {
|
|
212
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View = FrameLayout(context)
|
|
213
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.View
|
|
5
|
+
import androidx.core.view.ViewCompat
|
|
6
|
+
import androidx.core.view.WindowInsetsCompat
|
|
7
|
+
import com.pythonnative.runtime.PNBridge
|
|
8
|
+
import com.pythonnative.runtime.modules.ModuleEvents
|
|
9
|
+
import com.pythonnative.runtime.views.PNFrameLayout
|
|
10
|
+
import org.json.JSONObject
|
|
11
|
+
import kotlin.math.max
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Flex container (shared by `View`, `Column`, and `Row`): a bare
|
|
15
|
+
* [PNFrameLayout]. All flex semantics are computed by the Python layout
|
|
16
|
+
* engine and applied through `setFrame`; the container is just a
|
|
17
|
+
* positioning surface with `pointer_events` interception.
|
|
18
|
+
*/
|
|
19
|
+
open class ViewManager : ComponentManager() {
|
|
20
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View = PNFrameLayout(context)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Empty layout placeholder used as a flexible gap. */
|
|
24
|
+
class SpacerManager : ComponentManager() {
|
|
25
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View = View(context)
|
|
26
|
+
|
|
27
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
28
|
+
// Spacers carry no visual props of their own.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Safe-area container: a flex container with `fitsSystemWindows`. */
|
|
33
|
+
class SafeAreaViewManager : ViewManager() {
|
|
34
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
35
|
+
val view = PNFrameLayout(context)
|
|
36
|
+
view.fitsSystemWindows = true
|
|
37
|
+
return view
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Vanilla container that publishes the keyboard height (from window
|
|
43
|
+
* insets) as a `Host` module `keyboard` event so the Python-side
|
|
44
|
+
* component can compute its offset.
|
|
45
|
+
*/
|
|
46
|
+
class KeyboardAvoidingViewManager : ViewManager() {
|
|
47
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
48
|
+
val view = PNFrameLayout(context)
|
|
49
|
+
ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
|
|
50
|
+
publishKeyboard(v, insets)
|
|
51
|
+
insets
|
|
52
|
+
}
|
|
53
|
+
view.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ ->
|
|
54
|
+
ViewCompat.getRootWindowInsets(v)?.let { publishKeyboard(v, it) }
|
|
55
|
+
}
|
|
56
|
+
return view
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private fun publishKeyboard(view: View, insets: WindowInsetsCompat) {
|
|
60
|
+
val density = PNBridge.density()
|
|
61
|
+
val ime = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
62
|
+
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
|
63
|
+
val height = max(0, ime - bars) / density
|
|
64
|
+
val last = stateOf(view)["keyboard_height"] as? Double
|
|
65
|
+
if (last != null && last == height.toDouble()) return
|
|
66
|
+
stateOf(view)["keyboard_height"] = height.toDouble()
|
|
67
|
+
ModuleEvents.emit("Host", "keyboard", JSONObject().put("height", height.toDouble()))
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.res.ColorStateList
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.widget.CheckBox
|
|
7
|
+
import android.widget.CompoundButton
|
|
8
|
+
import android.widget.ProgressBar
|
|
9
|
+
import android.widget.SeekBar
|
|
10
|
+
import android.widget.Switch
|
|
11
|
+
import com.pythonnative.runtime.bridge.JsonUtil
|
|
12
|
+
import com.pythonnative.runtime.bridge.num
|
|
13
|
+
import com.pythonnative.runtime.bridge.str
|
|
14
|
+
import com.pythonnative.runtime.bridge.value
|
|
15
|
+
import org.json.JSONObject
|
|
16
|
+
|
|
17
|
+
/** Base for `CompoundButton` widgets: `on_change` wiring with a suppress guard. */
|
|
18
|
+
abstract class CheckedManager : ComponentManager() {
|
|
19
|
+
protected fun bindChecked(button: CompoundButton) {
|
|
20
|
+
button.setOnCheckedChangeListener { b, checked ->
|
|
21
|
+
if (stateOf(b)["suppress"] == true) return@setOnCheckedChangeListener
|
|
22
|
+
fire(b, "on_change", checked)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `Switch` element. */
|
|
28
|
+
class SwitchManager : CheckedManager() {
|
|
29
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
30
|
+
val sw = Switch(context)
|
|
31
|
+
bindChecked(sw)
|
|
32
|
+
return sw
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
36
|
+
val sw = view as Switch
|
|
37
|
+
if (props.has("value")) {
|
|
38
|
+
val state = stateOf(sw)
|
|
39
|
+
state["suppress"] = true
|
|
40
|
+
try {
|
|
41
|
+
sw.isChecked = JsonUtil.truthy(props.value("value"))
|
|
42
|
+
} finally {
|
|
43
|
+
state["suppress"] = false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (props.has("disabled")) sw.isEnabled = !JsonUtil.truthy(props.value("disabled"))
|
|
47
|
+
if (props.has("enabled")) sw.isEnabled = JsonUtil.truthy(props.value("enabled"))
|
|
48
|
+
PNColor.parse(props.value("on_tint_color") ?: props.value("tint_color"))?.let {
|
|
49
|
+
sw.thumbTintList = ColorStateList(
|
|
50
|
+
arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
|
|
51
|
+
intArrayOf(it, 0xFFFAFAFA.toInt()),
|
|
52
|
+
)
|
|
53
|
+
sw.trackTintList = ColorStateList(
|
|
54
|
+
arrayOf(intArrayOf(android.R.attr.state_checked), intArrayOf()),
|
|
55
|
+
intArrayOf(PNColor.withAlpha(it, 0.5), 0x61000000),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
PNColor.parse(props.value("thumb_color"))?.let { sw.thumbTintList = ColorStateList.valueOf(it) }
|
|
59
|
+
ViewStyler.applyAccessibility(sw, props)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `Checkbox` element with an optional inline label. */
|
|
64
|
+
class CheckboxManager : CheckedManager() {
|
|
65
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
66
|
+
val cb = CheckBox(context)
|
|
67
|
+
bindChecked(cb)
|
|
68
|
+
return cb
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
72
|
+
val cb = view as CheckBox
|
|
73
|
+
if (props.has("label")) cb.text = props.str("label") ?: ""
|
|
74
|
+
if (props.has("value")) {
|
|
75
|
+
val state = stateOf(cb)
|
|
76
|
+
state["suppress"] = true
|
|
77
|
+
try {
|
|
78
|
+
cb.isChecked = JsonUtil.truthy(props.value("value"))
|
|
79
|
+
} finally {
|
|
80
|
+
state["suppress"] = false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (props.has("disabled")) cb.isEnabled = !JsonUtil.truthy(props.value("disabled"))
|
|
84
|
+
PNColor.parse(props.value("color"))?.let { cb.buttonTintList = ColorStateList.valueOf(it) }
|
|
85
|
+
ViewStyler.applyAccessibility(cb, props)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `ProgressBar` element: a horizontal determinate bar with a 0..1 `value`. */
|
|
90
|
+
class ProgressBarManager : ComponentManager() {
|
|
91
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
92
|
+
val pb = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal)
|
|
93
|
+
pb.max = 1000
|
|
94
|
+
return pb
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
98
|
+
val pb = view as ProgressBar
|
|
99
|
+
props.num("value")?.let { pb.progress = (it.coerceIn(0.0, 1.0) * 1000).toInt() }
|
|
100
|
+
PNColor.parse(props.value("color"))?.let { pb.progressTintList = ColorStateList.valueOf(it) }
|
|
101
|
+
PNColor.parse(props.value("track_color"))?.let {
|
|
102
|
+
val track = ColorStateList.valueOf(it)
|
|
103
|
+
pb.progressBackgroundTintList = track
|
|
104
|
+
pb.secondaryProgressTintList = track
|
|
105
|
+
}
|
|
106
|
+
if (props.has("indeterminate")) pb.isIndeterminate = JsonUtil.truthy(props.value("indeterminate"))
|
|
107
|
+
ViewStyler.apply(pb, props)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `ActivityIndicator` element: an indeterminate spinner. */
|
|
112
|
+
class ActivityIndicatorManager : ComponentManager() {
|
|
113
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View = ProgressBar(context)
|
|
114
|
+
|
|
115
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
116
|
+
val pb = view as ProgressBar
|
|
117
|
+
if (props.has("animating")) {
|
|
118
|
+
pb.visibility = if (JsonUtil.truthy(props.value("animating"))) View.VISIBLE else View.GONE
|
|
119
|
+
}
|
|
120
|
+
if (props.has("hides_when_stopped") && !JsonUtil.truthy(props.value("hides_when_stopped"))) {
|
|
121
|
+
pb.visibility = View.VISIBLE
|
|
122
|
+
}
|
|
123
|
+
PNColor.parse(props.value("color"))?.let { pb.indeterminateTintList = ColorStateList.valueOf(it) }
|
|
124
|
+
props.str("size")?.let {
|
|
125
|
+
// The framework ProgressBar has no runtime size switch; scale "large".
|
|
126
|
+
val scale = if (it == "large") 1.5f else 1f
|
|
127
|
+
pb.scaleX = scale
|
|
128
|
+
pb.scaleY = scale
|
|
129
|
+
}
|
|
130
|
+
ViewStyler.apply(pb, props)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `Slider` element: a `SeekBar` mapped onto `min_value..max_value`. */
|
|
135
|
+
class SliderManager : ComponentManager() {
|
|
136
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
137
|
+
val sb = SeekBar(context)
|
|
138
|
+
sb.max = 1000
|
|
139
|
+
sb.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
140
|
+
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
141
|
+
if (!fromUser) return
|
|
142
|
+
fire(seekBar, "on_change", valueFor(seekBar, progress))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
|
146
|
+
fire(seekBar, "on_sliding_start", valueFor(seekBar, seekBar.progress))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
|
150
|
+
fire(seekBar, "on_sliding_complete", valueFor(seekBar, seekBar.progress))
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
return sb
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private fun range(view: View): Pair<Double, Double> {
|
|
157
|
+
val merged = propsOf(view)
|
|
158
|
+
val mn = merged.num("min_value") ?: merged.num("minimum_value") ?: 0.0
|
|
159
|
+
val mx = merged.num("max_value") ?: merged.num("maximum_value") ?: 1.0
|
|
160
|
+
return Pair(mn, mx)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fun valueFor(view: View, progress: Int): Double {
|
|
164
|
+
val (mn, mx) = range(view)
|
|
165
|
+
val span = if (mx != mn) mx - mn else 1.0
|
|
166
|
+
var value = mn + (progress / 1000.0) * span
|
|
167
|
+
propsOf(view).num("step")?.takeIf { it > 0 }?.let { step ->
|
|
168
|
+
value = mn + Math.round((value - mn) / step) * step
|
|
169
|
+
}
|
|
170
|
+
return value
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
174
|
+
val sb = view as SeekBar
|
|
175
|
+
val (mn, mx) = range(sb)
|
|
176
|
+
val span = if (mx != mn) mx - mn else 1.0
|
|
177
|
+
props.num("value")?.let { sb.progress = (((it - mn) / span) * 1000).toInt().coerceIn(0, 1000) }
|
|
178
|
+
if (props.has("disabled")) sb.isEnabled = !JsonUtil.truthy(props.value("disabled"))
|
|
179
|
+
PNColor.parse(props.value("minimum_track_tint_color") ?: props.value("tint_color"))?.let {
|
|
180
|
+
sb.progressTintList = ColorStateList.valueOf(it)
|
|
181
|
+
}
|
|
182
|
+
PNColor.parse(props.value("maximum_track_tint_color"))?.let { sb.progressBackgroundTintList = ColorStateList.valueOf(it) }
|
|
183
|
+
PNColor.parse(props.value("thumb_tint_color"))?.let { sb.thumbTintList = ColorStateList.valueOf(it) }
|
|
184
|
+
ViewStyler.applyAccessibility(sb, props)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
package com.pythonnative.runtime.components
|
|
2
|
+
|
|
3
|
+
import android.app.DatePickerDialog
|
|
4
|
+
import android.app.TimePickerDialog
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.view.View
|
|
7
|
+
import android.widget.Button
|
|
8
|
+
import com.pythonnative.runtime.bridge.PNLog
|
|
9
|
+
import com.pythonnative.runtime.bridge.str
|
|
10
|
+
import com.pythonnative.runtime.bridge.value
|
|
11
|
+
import org.json.JSONObject
|
|
12
|
+
import java.text.SimpleDateFormat
|
|
13
|
+
import java.util.Calendar
|
|
14
|
+
import java.util.Locale
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* `DatePicker` element: a trigger `Button` opening the native
|
|
18
|
+
* `DatePickerDialog` (`mode` `"date"`), `TimePickerDialog` (`"time"`),
|
|
19
|
+
* or a chained date-then-time flow (`"datetime"`). Values are ISO
|
|
20
|
+
* strings; the confirmed value is reported through `on_change`.
|
|
21
|
+
*/
|
|
22
|
+
class DatePickerManager : ComponentManager() {
|
|
23
|
+
override fun createView(context: Context, tag: Long, props: JSONObject): View {
|
|
24
|
+
val btn = Button(context)
|
|
25
|
+
btn.isAllCaps = false
|
|
26
|
+
btn.setOnClickListener { v ->
|
|
27
|
+
if (propsOf(v).value("enabled") == false) return@setOnClickListener
|
|
28
|
+
openDialog(v as Button)
|
|
29
|
+
}
|
|
30
|
+
return btn
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override fun applyProps(view: View, props: JSONObject, initial: Boolean) {
|
|
34
|
+
val btn = view as Button
|
|
35
|
+
if (props.has("enabled")) btn.isEnabled = props.value("enabled") != false
|
|
36
|
+
if (props.has("value") || props.has("mode") || initial) refreshLabel(btn)
|
|
37
|
+
ViewStyler.applyAccessibility(btn, props)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private fun mode(btn: Button): String = propsOf(btn).str("mode") ?: "date"
|
|
41
|
+
|
|
42
|
+
private fun refreshLabel(btn: Button) {
|
|
43
|
+
val value = propsOf(btn).str("value")
|
|
44
|
+
btn.text = if (!value.isNullOrEmpty()) value else PLACEHOLDERS[mode(btn)] ?: "Select"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private fun openDialog(btn: Button) {
|
|
48
|
+
val mode = mode(btn)
|
|
49
|
+
val cal = parse(propsOf(btn).str("value"), mode)
|
|
50
|
+
when (mode) {
|
|
51
|
+
"time" -> openTime(btn, cal)
|
|
52
|
+
"datetime" -> openDate(btn, cal, thenTime = true)
|
|
53
|
+
else -> openDate(btn, cal, thenTime = false)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private fun openDate(btn: Button, cal: Calendar, thenTime: Boolean) {
|
|
58
|
+
val dialog = DatePickerDialog(
|
|
59
|
+
btn.context,
|
|
60
|
+
{ _, year, month, day ->
|
|
61
|
+
cal.set(Calendar.YEAR, year)
|
|
62
|
+
cal.set(Calendar.MONTH, month)
|
|
63
|
+
cal.set(Calendar.DAY_OF_MONTH, day)
|
|
64
|
+
if (thenTime) openTime(btn, cal) else commit(btn, cal)
|
|
65
|
+
},
|
|
66
|
+
cal.get(Calendar.YEAR),
|
|
67
|
+
cal.get(Calendar.MONTH),
|
|
68
|
+
cal.get(Calendar.DAY_OF_MONTH),
|
|
69
|
+
)
|
|
70
|
+
try {
|
|
71
|
+
val merged = propsOf(btn)
|
|
72
|
+
merged.str("minimum")?.takeIf { it.isNotEmpty() }?.let { dialog.datePicker.minDate = parse(it, mode(btn)).timeInMillis }
|
|
73
|
+
merged.str("maximum")?.takeIf { it.isNotEmpty() }?.let { dialog.datePicker.maxDate = parse(it, mode(btn)).timeInMillis }
|
|
74
|
+
} catch (e: Exception) {
|
|
75
|
+
PNLog.swallowed("DatePickerManager.minMax", e)
|
|
76
|
+
}
|
|
77
|
+
dialog.show()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private fun openTime(btn: Button, cal: Calendar) {
|
|
81
|
+
TimePickerDialog(
|
|
82
|
+
btn.context,
|
|
83
|
+
{ _, hour, minute ->
|
|
84
|
+
cal.set(Calendar.HOUR_OF_DAY, hour)
|
|
85
|
+
cal.set(Calendar.MINUTE, minute)
|
|
86
|
+
commit(btn, cal)
|
|
87
|
+
},
|
|
88
|
+
cal.get(Calendar.HOUR_OF_DAY),
|
|
89
|
+
cal.get(Calendar.MINUTE),
|
|
90
|
+
true,
|
|
91
|
+
).show()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private fun format(mode: String): SimpleDateFormat = SimpleDateFormat(PATTERNS[mode] ?: PATTERNS["date"]!!, Locale.US)
|
|
95
|
+
|
|
96
|
+
private fun parse(value: String?, mode: String): Calendar {
|
|
97
|
+
val cal = Calendar.getInstance()
|
|
98
|
+
if (!value.isNullOrEmpty()) {
|
|
99
|
+
try {
|
|
100
|
+
format(mode).parse(value)?.let { cal.time = it }
|
|
101
|
+
} catch (e: Exception) {
|
|
102
|
+
PNLog.swallowed("DatePickerManager.parse", e)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return cal
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private fun commit(btn: Button, cal: Calendar) {
|
|
109
|
+
val iso = try {
|
|
110
|
+
format(mode(btn)).format(cal.time)
|
|
111
|
+
} catch (e: Exception) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
propsOf(btn).put("value", iso)
|
|
115
|
+
btn.text = iso
|
|
116
|
+
fire(btn, "on_change", iso)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private companion object {
|
|
120
|
+
val PATTERNS = mapOf("date" to "yyyy-MM-dd", "time" to "HH:mm", "datetime" to "yyyy-MM-dd'T'HH:mm")
|
|
121
|
+
val PLACEHOLDERS = mapOf("date" to "Select date", "time" to "Select time", "datetime" to "Select date & time")
|
|
122
|
+
}
|
|
123
|
+
}
|