pythonnative 0.22.1__py3-none-any.whl → 0.24.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.
Files changed (35) hide show
  1. pythonnative/__init__.py +43 -50
  2. pythonnative/animated.py +1 -1
  3. pythonnative/appearance.py +124 -0
  4. pythonnative/cli/pn.py +3 -3
  5. pythonnative/components.py +873 -466
  6. pythonnative/diagnostics.py +214 -0
  7. pythonnative/element.py +5 -2
  8. pythonnative/events.py +13 -8
  9. pythonnative/hooks.py +454 -49
  10. pythonnative/hot_reload.py +9 -1
  11. pythonnative/images.py +196 -0
  12. pythonnative/mutations.py +3 -12
  13. pythonnative/native_views/__init__.py +1 -7
  14. pythonnative/native_views/android.py +651 -46
  15. pythonnative/native_views/desktop.py +116 -20
  16. pythonnative/native_views/ios.py +974 -52
  17. pythonnative/navigation.py +41 -17
  18. pythonnative/preview.py +74 -7
  19. pythonnative/project/config.py +1 -9
  20. pythonnative/reconciler.py +863 -441
  21. pythonnative/screen.py +409 -27
  22. pythonnative/storage.py +3 -3
  23. pythonnative/style.py +148 -6
  24. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +2 -1
  25. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNAccessibilityDelegate.kt +47 -0
  26. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNBorderDrawable.kt +67 -0
  27. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/PNVirtualListView.java +172 -0
  28. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +22 -0
  29. pythonnative/virtual_rows.py +137 -0
  30. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/METADATA +5 -4
  31. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/RECORD +35 -28
  32. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/WHEEL +1 -1
  33. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/entry_points.txt +0 -0
  34. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/licenses/LICENSE +0 -0
  35. {pythonnative-0.22.1.dist-info → pythonnative-0.24.0.dist-info}/top_level.txt +0 -0
pythonnative/style.py CHANGED
@@ -34,9 +34,11 @@ Example:
34
34
  ```
35
35
  """
36
36
 
37
+ import difflib
37
38
  from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
38
39
 
39
- from .hooks import Context, create_context
40
+ from . import diagnostics
41
+ from .hooks import Context, create_context, use_color_scheme, use_context
40
42
 
41
43
  # ======================================================================
42
44
  # Atomic value types
@@ -74,6 +76,30 @@ EdgeValue = Union[Dimension, EdgeInsets]
74
76
  [`EdgeInsets`][pythonnative.EdgeInsets] dict."""
75
77
 
76
78
 
79
+ class AccessibilityState(TypedDict, total=False):
80
+ """Current widget state exposed to assistive technology.
81
+
82
+ Passed via the ``accessibility_state=`` prop on interactive
83
+ components. All keys are optional; omitted keys are treated as
84
+ unset (not ``False``).
85
+
86
+ Attributes:
87
+ disabled: The widget is visible but not interactive.
88
+ selected: The widget is currently selected (e.g. the active
89
+ tab).
90
+ checked: Toggle/checkbox state. ``"mixed"`` represents a
91
+ tri-state checkbox.
92
+ busy: The widget is loading or otherwise temporarily busy.
93
+ expanded: A disclosure/accordion is currently expanded.
94
+ """
95
+
96
+ disabled: bool
97
+ selected: bool
98
+ checked: Union[bool, Literal["mixed"]]
99
+ busy: bool
100
+ expanded: bool
101
+
102
+
77
103
  class ShadowOffset(TypedDict):
78
104
  """Shadow displacement in points."""
79
105
 
@@ -136,6 +162,17 @@ TransformSpec = Union[TransformEntry, List[TransformEntry]]
136
162
  # ----------------------------------------------------------------------
137
163
 
138
164
  FlexDirection = Literal["row", "column", "row_reverse", "column_reverse"]
165
+ FlexWrap = Literal["nowrap", "wrap", "wrap_reverse"]
166
+ AlignContent = Literal[
167
+ "flex_start",
168
+ "center",
169
+ "flex_end",
170
+ "stretch",
171
+ "space_between",
172
+ "space_around",
173
+ "space_evenly",
174
+ ]
175
+ LayoutDirection = Literal["ltr", "rtl"]
139
176
  JustifyContent = Literal[
140
177
  "flex_start",
141
178
  "center",
@@ -241,9 +278,12 @@ class Style(TypedDict, total=False):
241
278
  flex_shrink: float
242
279
  flex_basis: Dimension
243
280
  flex_direction: FlexDirection
281
+ flex_wrap: FlexWrap
244
282
  justify_content: JustifyContent
245
283
  align_items: AlignItems
246
284
  align_self: AlignSelf
285
+ align_content: AlignContent
286
+ direction: LayoutDirection
247
287
 
248
288
  # --- Layout: position ---
249
289
  position: Position
@@ -251,6 +291,8 @@ class Style(TypedDict, total=False):
251
291
  right: Dimension
252
292
  bottom: Dimension
253
293
  left: Dimension
294
+ start: Dimension
295
+ end: Dimension
254
296
 
255
297
  # --- Layout: spacing ---
256
298
  padding: EdgeValue
@@ -258,6 +300,8 @@ class Style(TypedDict, total=False):
258
300
  padding_bottom: Dimension
259
301
  padding_left: Dimension
260
302
  padding_right: Dimension
303
+ padding_start: Dimension
304
+ padding_end: Dimension
261
305
  padding_horizontal: Dimension
262
306
  padding_vertical: Dimension
263
307
  margin: EdgeValue
@@ -265,10 +309,14 @@ class Style(TypedDict, total=False):
265
309
  margin_bottom: Dimension
266
310
  margin_left: Dimension
267
311
  margin_right: Dimension
312
+ margin_start: Dimension
313
+ margin_end: Dimension
268
314
  margin_horizontal: Dimension
269
315
  margin_vertical: Dimension
270
316
  spacing: float
271
317
  gap: float
318
+ row_gap: float
319
+ column_gap: float
272
320
 
273
321
  # --- Visual: clipping & overflow ---
274
322
  overflow: Overflow
@@ -283,6 +331,14 @@ class Style(TypedDict, total=False):
283
331
  # --- Visual: borders ---
284
332
  border_width: float
285
333
  border_radius: float
334
+ border_top_width: float
335
+ border_right_width: float
336
+ border_bottom_width: float
337
+ border_left_width: float
338
+ border_top_color: Color
339
+ border_right_color: Color
340
+ border_bottom_color: Color
341
+ border_left_color: Color
286
342
 
287
343
  # --- Visual: typography ---
288
344
  font_size: float
@@ -370,6 +426,40 @@ def resolve_style(value: StyleProp) -> Dict[str, Any]:
370
426
  return result
371
427
 
372
428
 
429
+ _KNOWN_STYLE_KEYS = frozenset(Style.__annotations__)
430
+ """Every key declared on the [`Style`][pythonnative.Style] TypedDict."""
431
+
432
+
433
+ def validate_style_keys(style_dict: Dict[str, Any], owner: str = "") -> None:
434
+ """Warn (once per key) about style keys no built-in handler reads.
435
+
436
+ Dev-mode only; production skips the scan entirely. Typos get a
437
+ "did you mean" suggestion via fuzzy matching against the declared
438
+ [`Style`][pythonnative.Style] keys. Unknown keys still flow through
439
+ to handlers untouched, so custom components that read extra keys
440
+ keep working (at the cost of one dev warning).
441
+
442
+ Args:
443
+ style_dict: The flattened style dict about to be merged into an
444
+ element's props.
445
+ owner: Element type name for the warning message (e.g.
446
+ ``"Text"``).
447
+ """
448
+ if not diagnostics.is_dev() or not style_dict:
449
+ return
450
+ for key in style_dict:
451
+ if key in _KNOWN_STYLE_KEYS:
452
+ continue
453
+ matches = difflib.get_close_matches(str(key), _KNOWN_STYLE_KEYS, n=1)
454
+ hint = f" Did you mean {matches[0]!r}?" if matches else ""
455
+ diagnostics.warn_once(
456
+ f"Unknown style key {key!r}"
457
+ + (f" on {owner}" if owner else "")
458
+ + f".{hint} Built-in components ignore keys they don't recognize.",
459
+ key=f"style:{owner}:{key}",
460
+ )
461
+
462
+
373
463
  # ======================================================================
374
464
  # StyleSheet
375
465
  # ======================================================================
@@ -481,6 +571,7 @@ DEFAULT_LIGHT_THEME: Dict[str, Any] = {
481
571
  "spacing_large": 16,
482
572
  "border_radius": 8,
483
573
  }
574
+ """Built-in light theme selected by [`use_theme`][pythonnative.use_theme]."""
484
575
 
485
576
  DEFAULT_DARK_THEME: Dict[str, Any] = {
486
577
  "primary_color": "#0A84FF",
@@ -500,17 +591,65 @@ DEFAULT_DARK_THEME: Dict[str, Any] = {
500
591
  "spacing_large": 16,
501
592
  "border_radius": 8,
502
593
  }
594
+ """Built-in dark theme selected by [`use_theme`][pythonnative.use_theme]."""
595
+
596
+ _FOLLOW_SYSTEM_THEME = object()
597
+ """Sentinel default for `ThemeContext`: resolve from the color scheme."""
503
598
 
504
- ThemeContext: Context = create_context(DEFAULT_LIGHT_THEME)
505
- """Default theme context populated with `DEFAULT_LIGHT_THEME`.
599
+ ThemeContext: Context = create_context(_FOLLOW_SYSTEM_THEME)
600
+ """Theme context that follows the system color scheme by default.
506
601
 
507
- Wrap a subtree in
602
+ Without a provider, [`use_theme`][pythonnative.use_theme] resolves to
603
+ [`DEFAULT_LIGHT_THEME`][pythonnative.style.DEFAULT_LIGHT_THEME] or
604
+ [`DEFAULT_DARK_THEME`][pythonnative.style.DEFAULT_DARK_THEME] based on
605
+ the current appearance. Wrap a subtree in
508
606
  [`Provider(ThemeContext, my_theme, ...)`][pythonnative.Provider] to
509
- override the theme for that subtree, then read it inside descendants
510
- via [`use_context(ThemeContext)`][pythonnative.use_context].
607
+ pin an explicit theme for that subtree, then read it inside
608
+ descendants via [`use_theme`][pythonnative.use_theme] (or
609
+ [`use_context(ThemeContext)`][pythonnative.use_context]).
511
610
  """
512
611
 
513
612
 
613
+ def default_theme(scheme: str) -> Dict[str, Any]:
614
+ """Return the built-in theme dict for ``scheme`` (``"light"`` / ``"dark"``)."""
615
+ return DEFAULT_DARK_THEME if scheme == "dark" else DEFAULT_LIGHT_THEME
616
+
617
+
618
+ def use_theme() -> Dict[str, Any]:
619
+ """Return the active theme, following the system appearance by default.
620
+
621
+ If an ancestor mounted a ``Provider(ThemeContext, ...)``, that
622
+ theme is returned as-is. Otherwise the built-in light or dark
623
+ theme is selected from the effective color scheme (via
624
+ [`use_color_scheme`][pythonnative.use_color_scheme], so the
625
+ component re-renders when the system appearance flips).
626
+
627
+ Returns:
628
+ The active theme dict.
629
+
630
+ Raises:
631
+ RuntimeError: If called outside a `@component` function.
632
+
633
+ Example:
634
+ ```python
635
+ import pythonnative as pn
636
+
637
+ @pn.component
638
+ def Card():
639
+ theme = pn.use_theme()
640
+ return pn.View(
641
+ pn.Text("Hello", style=pn.style(color=theme["text_color"])),
642
+ style=pn.style(background_color=theme["surface_color"]),
643
+ )
644
+ ```
645
+ """
646
+ scheme = use_color_scheme()
647
+ theme = use_context(ThemeContext)
648
+ if theme is _FOLLOW_SYSTEM_THEME:
649
+ return default_theme(scheme)
650
+ return theme
651
+
652
+
514
653
  __all__ = [
515
654
  "AlignItems",
516
655
  "AlignSelf",
@@ -543,6 +682,9 @@ __all__ = [
543
682
  "TransformScaleY",
544
683
  "TransformSpec",
545
684
  "TransformTranslate",
685
+ "default_theme",
546
686
  "resolve_style",
547
687
  "style",
688
+ "use_theme",
689
+ "validate_style_keys",
548
690
  ]
@@ -14,7 +14,8 @@
14
14
  tools:targetApi="31">
15
15
  <activity
16
16
  android:name=".MainActivity"
17
- android:exported="true">
17
+ android:exported="true"
18
+ android:windowSoftInputMode="adjustResize">
18
19
  <intent-filter>
19
20
  <action android:name="android.intent.action.MAIN" />
20
21
 
@@ -0,0 +1,47 @@
1
+ package com.pythonnative.android_template
2
+
3
+ import android.os.Build
4
+ import android.view.View
5
+ import android.view.accessibility.AccessibilityNodeInfo
6
+
7
+ /**
8
+ * Accessibility delegate that exposes PythonNative's `test_id` and
9
+ * `accessibility_state` props through the Android accessibility tree.
10
+ *
11
+ * `test_id` is surfaced as the node's view-id resource name, which
12
+ * UI Automator-based tools (Maestro, UiAutomator2, Appium) match as
13
+ * `resource-id`. State flags map onto the closest
14
+ * [AccessibilityNodeInfo] equivalents so TalkBack announces them.
15
+ *
16
+ * Instances are created and configured from Python (the Android view
17
+ * handlers); fields left null are not applied.
18
+ */
19
+ class PNAccessibilityDelegate : View.AccessibilityDelegate() {
20
+ var testId: String? = null
21
+ var stateDisabled: Boolean? = null
22
+ var stateSelected: Boolean? = null
23
+ var stateChecked: Boolean? = null
24
+ var stateBusy: Boolean? = null
25
+ var stateExpanded: Boolean? = null
26
+
27
+ override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {
28
+ super.onInitializeAccessibilityNodeInfo(host, info)
29
+ testId?.let { info.viewIdResourceName = it }
30
+ stateDisabled?.let { info.isEnabled = !it }
31
+ stateSelected?.let { info.isSelected = it }
32
+ stateChecked?.let {
33
+ info.isCheckable = true
34
+ info.isChecked = it
35
+ }
36
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
37
+ val states = mutableListOf<String>()
38
+ if (stateBusy == true) states.add("busy")
39
+ when (stateExpanded) {
40
+ true -> states.add("expanded")
41
+ false -> states.add("collapsed")
42
+ null -> {}
43
+ }
44
+ if (states.isNotEmpty()) info.stateDescription = states.joinToString(", ")
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,67 @@
1
+ package com.pythonnative.android_template
2
+
3
+ import android.graphics.Canvas
4
+ import android.graphics.ColorFilter
5
+ import android.graphics.Paint
6
+ import android.graphics.PixelFormat
7
+ import android.graphics.RectF
8
+ import android.graphics.drawable.Drawable
9
+
10
+ /**
11
+ * Background drawable that renders an optional rounded fill plus four
12
+ * independent border strips. Used for PythonNative's
13
+ * `border_<side>_width` / `border_<side>_color` style props, which
14
+ * `GradientDrawable`'s single uniform stroke cannot express.
15
+ *
16
+ * Widths are in pixels; sides are ordered left, top, right, bottom.
17
+ */
18
+ class PNBorderDrawable(
19
+ private val hasBackground: Boolean,
20
+ private val backgroundColor: Int,
21
+ private val cornerRadius: Float,
22
+ private val widths: FloatArray,
23
+ private val colors: IntArray,
24
+ ) : Drawable() {
25
+ private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
26
+
27
+ override fun draw(canvas: Canvas) {
28
+ val b = RectF(bounds)
29
+ if (hasBackground) {
30
+ paint.style = Paint.Style.FILL
31
+ paint.color = backgroundColor
32
+ if (cornerRadius > 0f) {
33
+ canvas.drawRoundRect(b, cornerRadius, cornerRadius, paint)
34
+ } else {
35
+ canvas.drawRect(b, paint)
36
+ }
37
+ }
38
+ paint.style = Paint.Style.FILL
39
+ if (widths[0] > 0f) {
40
+ paint.color = colors[0]
41
+ canvas.drawRect(b.left, b.top, b.left + widths[0], b.bottom, paint)
42
+ }
43
+ if (widths[1] > 0f) {
44
+ paint.color = colors[1]
45
+ canvas.drawRect(b.left, b.top, b.right, b.top + widths[1], paint)
46
+ }
47
+ if (widths[2] > 0f) {
48
+ paint.color = colors[2]
49
+ canvas.drawRect(b.right - widths[2], b.top, b.right, b.bottom, paint)
50
+ }
51
+ if (widths[3] > 0f) {
52
+ paint.color = colors[3]
53
+ canvas.drawRect(b.left, b.bottom - widths[3], b.right, b.bottom, paint)
54
+ }
55
+ }
56
+
57
+ override fun setAlpha(alpha: Int) {
58
+ paint.alpha = alpha
59
+ }
60
+
61
+ override fun setColorFilter(colorFilter: ColorFilter?) {
62
+ paint.colorFilter = colorFilter
63
+ }
64
+
65
+ @Deprecated("Deprecated in Java")
66
+ override fun getOpacity(): Int = PixelFormat.TRANSLUCENT
67
+ }
@@ -0,0 +1,172 @@
1
+ package com.pythonnative.android_template;
2
+
3
+ import android.content.Context;
4
+ import android.util.TypedValue;
5
+ import android.view.ViewGroup;
6
+ import android.widget.FrameLayout;
7
+
8
+ import androidx.annotation.NonNull;
9
+ import androidx.recyclerview.widget.LinearLayoutManager;
10
+ import androidx.recyclerview.widget.RecyclerView;
11
+
12
+ /**
13
+ * RecyclerView-backed virtualized list used by PythonNative's Android bridge.
14
+ *
15
+ * <p>Chaquopy can proxy Java interfaces from Python, but it can't subclass Java
16
+ * abstract classes such as RecyclerView.Adapter or RecyclerView.ViewHolder. This
17
+ * helper owns those abstract-class implementations in Java and delegates row
18
+ * content back to Python through the small Delegate interface below.</p>
19
+ *
20
+ * <p>Rows may have per-position heights (section lists interleave headers and
21
+ * items); the delegate reports each row's height in dp. Scroll changes are
22
+ * forwarded to the delegate in dp so the Python side can drive on_scroll,
23
+ * on_end_reached, and viewability callbacks with one code path per platform.</p>
24
+ */
25
+ public class PNVirtualListView extends RecyclerView {
26
+ public interface Delegate {
27
+ int getCount();
28
+
29
+ float getRowHeightDp(int position);
30
+
31
+ void mountRow(int position, FrameLayout container, float widthDp, float heightDp);
32
+
33
+ void onRowPress(int position);
34
+
35
+ void onRowRecycled(FrameLayout container);
36
+
37
+ void onScrolled(float offsetDp, float extentDp, float rangeDp);
38
+ }
39
+
40
+ private final float density;
41
+ private final RowAdapter rowAdapter;
42
+ private Delegate delegate;
43
+
44
+ public PNVirtualListView(@NonNull Context context, @NonNull Delegate delegate) {
45
+ super(context);
46
+ this.delegate = delegate;
47
+ this.density = context.getResources().getDisplayMetrics().density;
48
+ setLayoutManager(new LinearLayoutManager(context));
49
+ rowAdapter = new RowAdapter();
50
+ setAdapter(rowAdapter);
51
+ addOnScrollListener(new OnScrollListener() {
52
+ @Override
53
+ public void onScrolled(@NonNull RecyclerView rv, int dx, int dy) {
54
+ PNVirtualListView.this.delegate.onScrolled(
55
+ rv.computeVerticalScrollOffset() / PNVirtualListView.this.density,
56
+ rv.computeVerticalScrollExtent() / PNVirtualListView.this.density,
57
+ rv.computeVerticalScrollRange() / PNVirtualListView.this.density
58
+ );
59
+ }
60
+ });
61
+ }
62
+
63
+ public void setDelegate(@NonNull Delegate delegate) {
64
+ this.delegate = delegate;
65
+ rowAdapter.notifyDataSetChanged();
66
+ }
67
+
68
+ public void notifyDataChanged() {
69
+ rowAdapter.notifyDataSetChanged();
70
+ }
71
+
72
+ public void scrollToOffsetDp(float offsetDp, boolean animated) {
73
+ int targetPx = Math.round(offsetDp * density);
74
+ int currentPx = computeVerticalScrollOffset();
75
+ if (animated) {
76
+ smoothScrollBy(0, targetPx - currentPx);
77
+ } else {
78
+ scrollBy(0, targetPx - currentPx);
79
+ }
80
+ }
81
+
82
+ public void scrollToIndex(int position, boolean animated) {
83
+ int clamped = Math.max(0, Math.min(position, Math.max(0, delegate.getCount() - 1)));
84
+ if (animated) {
85
+ smoothScrollToPosition(clamped);
86
+ } else {
87
+ scrollToPosition(clamped);
88
+ }
89
+ }
90
+
91
+ private int rowHeightPx(int position) {
92
+ return Math.max(
93
+ 1,
94
+ Math.round(TypedValue.applyDimension(
95
+ TypedValue.COMPLEX_UNIT_DIP,
96
+ delegate.getRowHeightDp(position),
97
+ getResources().getDisplayMetrics()
98
+ ))
99
+ );
100
+ }
101
+
102
+ private float currentWidthDp(FrameLayout container) {
103
+ int widthPx = container.getWidth();
104
+ if (widthPx <= 0) {
105
+ widthPx = getWidth();
106
+ }
107
+ if (widthPx <= 0) {
108
+ widthPx = getResources().getDisplayMetrics().widthPixels;
109
+ }
110
+ return widthPx / density;
111
+ }
112
+
113
+ private class RowHolder extends RecyclerView.ViewHolder {
114
+ final FrameLayout container;
115
+
116
+ RowHolder(@NonNull FrameLayout container) {
117
+ super(container);
118
+ this.container = container;
119
+ }
120
+ }
121
+
122
+ private class RowAdapter extends RecyclerView.Adapter<RowHolder> {
123
+ @Override
124
+ public int getItemCount() {
125
+ return delegate.getCount();
126
+ }
127
+
128
+ @NonNull
129
+ @Override
130
+ public RowHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
131
+ FrameLayout container = new FrameLayout(parent.getContext());
132
+ container.setLayoutParams(
133
+ new RecyclerView.LayoutParams(
134
+ ViewGroup.LayoutParams.MATCH_PARENT,
135
+ ViewGroup.LayoutParams.WRAP_CONTENT
136
+ )
137
+ );
138
+ return new RowHolder(container);
139
+ }
140
+
141
+ @Override
142
+ public void onBindViewHolder(@NonNull RowHolder holder, int position) {
143
+ RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) holder.container.getLayoutParams();
144
+ int heightPx = rowHeightPx(position);
145
+ if (params.height != heightPx) {
146
+ params.height = heightPx;
147
+ holder.container.setLayoutParams(params);
148
+ }
149
+ holder.container.removeAllViews();
150
+ holder.container.setOnClickListener(v -> {
151
+ int pos = holder.getBindingAdapterPosition();
152
+ if (pos != RecyclerView.NO_POSITION) {
153
+ delegate.onRowPress(pos);
154
+ }
155
+ });
156
+ delegate.mountRow(
157
+ position,
158
+ holder.container,
159
+ currentWidthDp(holder.container),
160
+ delegate.getRowHeightDp(position)
161
+ );
162
+ }
163
+
164
+ @Override
165
+ public void onViewRecycled(@NonNull RowHolder holder) {
166
+ holder.container.removeAllViews();
167
+ holder.container.setOnClickListener(null);
168
+ delegate.onRowRecycled(holder.container);
169
+ super.onViewRecycled(holder);
170
+ }
171
+ }
172
+ }
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
8
8
  import android.view.View
9
9
  import android.view.ViewGroup
10
10
  import android.widget.FrameLayout
11
+ import androidx.activity.OnBackPressedCallback
11
12
  import androidx.core.os.bundleOf
12
13
  import androidx.fragment.app.Fragment
13
14
  import com.chaquo.python.PyObject
@@ -54,6 +55,27 @@ class ScreenFragment : Fragment() {
54
55
  } catch (e: Exception) {
55
56
  Log.e(TAG, "Failed to instantiate screen", e)
56
57
  }
58
+
59
+ // Offer the system back action to Python (`use_back_handler`)
60
+ // before running the default pop/finish behavior.
61
+ requireActivity().onBackPressedDispatcher.addCallback(
62
+ this,
63
+ object : OnBackPressedCallback(true) {
64
+ override fun handleOnBackPressed() {
65
+ val consumed = try {
66
+ screen?.callAttr("on_back_pressed")?.toBoolean() ?: false
67
+ } catch (e: Exception) {
68
+ Log.w(TAG, "on_back_pressed failed", e)
69
+ false
70
+ }
71
+ if (!consumed) {
72
+ isEnabled = false
73
+ requireActivity().onBackPressedDispatcher.onBackPressed()
74
+ isEnabled = true
75
+ }
76
+ }
77
+ }
78
+ )
57
79
  }
58
80
 
59
81
  override fun onCreateView(