Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* Fix iOS apps built with `flet build ipa` crashing at startup with `Failed to lookup symbol 'serious_python_run': dlsym(RTLD_DEFAULT, serious_python_run): symbol not found`. `serious_python` shipped `dart_bridge` — which provides the in-process Dart↔Python transport — as a **static** library linked into the app executable. An iOS executable exports nothing to the dynamic symbol table by default and the release build strips local symbols, so the `dlsym` lookups that Dart (`DynamicLibrary.process()`) and Python (`import dart_bridge`) perform at runtime could not resolve. Only release/archive (device) builds under the Swift Package Manager path were affected — debug and simulator builds don't dead-strip, so the failure did not reproduce there, and Android was never affected (its `dart_bridge` is a dynamic `.so`, which exports its symbols). Bumps `serious_python` to **4.4.0**, which ships `dart_bridge` as a dynamic framework — embedded and signed into the app like `Python.xcframework`, with its symbols exported — and re-pins the bundled python-build snapshot to **20260726** (`dart_bridge` **1.5.1 → 1.6.1**, Pyodide 3.14 **314.0.2 → 314.0.3**); the bundled Python versions (**3.12.13 / 3.13.14 / 3.14.6**) are unchanged by @FeodorFitsner.

* Fix `MatplotlibChart` freezing permanently when its platform view is disposed with a frame in flight — the common trigger is switching to another tab inside the app, which races the frame stream: `DataChannel.send` on a disposed channel silently drops, so the frame's `[0xFF]` frame-applied ack never arrives and `_send_and_wait`'s unbounded await parks `MatplotlibChart._receive_loop` — the sole consumer of the frame queue — for the rest of the session, with no exception raised; remounting opens a fresh channel but the stale ack futures were never resolved, so the chart stayed frozen. `_capture_channel` now resolves all pending ack futures when a new channel is captured (a fresh channel means every pending ack belongs to the disposed one), unparking the producer instantly on remount, and the ack await is bounded by `FRAME_ACK_TIMEOUT` (5s; a healthy ack lands in milliseconds) — on expiry the frame is dropped and its future removed from the ack FIFO so subsequent acks keep resolving the right entries ([#6709](https://github.com/flet-dev/flet/issues/6709), [#6710](https://github.com/flet-dev/flet/pull/6710)) by @ForsakenDurian.
* Fix `KeyboardListener` on macOS getting stuck in a repeated `Escape` event loop after pressing Command+Period (`Command+.`). The client now handles the macOS/Flutter synthesized Escape sequence for that shortcut so the app remains responsive.
* Fix a system/edge-swipe back gesture exiting the whole host app instead of navigating back when it lands on an embedded `FletApp` (an app rendered inside another Flet app — e.g. a gallery host running example apps in-process). The embedded app's `WidgetsApp` (`MaterialApp`/`CupertinoApp`) ran the default `NavigationNotification` handler, which reported `SystemNavigator.setFrameworkHandlesBack(false)` for a nested app that couldn't pop (typically a single-view example) and swallowed the notification, so the OS finished the whole activity on back and the host never got to report that it could pop. An embedded page now lets that notification bubble to the host (which re-reports `canHandlePop`) and chains a `ChildBackButtonDispatcher` to the host Router, so a system back propagates to the host and pops the view that embeds it by @FeodorFitsner.
* Fix `page.window.maximized = True` intermittently reverting to unmaximized right after startup on macOS, when set in the same patch as `page.title` (e.g. `page.title = "My App"; page.window.maximized = True` in `main()`) by @davidlawson.
* Fix `flet build` picking a non-decodable icon/splash image when several files share a base name, producing a machine-dependent `NoDecoderForImageFormatException` from `flutter_launcher_icons`. When an app's `assets` held, say, both `icon.png` and `icon.svg`, `find_platform_image` selected the first match from `glob.glob(...)` — whose order is filesystem-dependent — so the same app could pick `icon.png` on one machine and `icon.svg` on another (SVG is vector and can't be decoded by the raster icon/splash generators), turning a working build into a crash purely based on directory listing order. Candidates are now filtered to formats the generators can actually decode (`.svg` is dropped everywhere; `.icns` stays macOS-only and `.ico` Windows-only) and ranked so a raster image (`.png` first) always wins, making the choice deterministic across machines. When the only supplied image is an SVG (no raster sibling), it's skipped with a build-log warning and the default Flet icon is used instead of crashing by @FeodorFitsner.
Expand Down
91 changes: 83 additions & 8 deletions packages/flet/lib/src/controls/keyboard_listener.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

Expand All @@ -19,6 +20,8 @@ class KeyboardListenerControl extends StatefulWidget {

class _KeyboardListenerControlState extends State<KeyboardListenerControl> {
final FocusNode _focusNode = FocusNode();
bool _escapeDownSent = false;
DateTime? _lastSynthesizedEscapeUpAt;

@override
void initState() {
Expand All @@ -43,6 +46,75 @@ class _KeyboardListenerControlState extends State<KeyboardListenerControl> {
}
}

void _triggerKeyEvent(String eventName, KeyEvent keyEvent) {
if (keyEvent.logicalKey == LogicalKeyboardKey.escape) {
_escapeDownSent = eventName == "key_down";
}
widget.control
.triggerEvent(eventName, {"key": keyEvent.logicalKey.keyLabel});
}

void _triggerEscapeKeyUp() {
if (!_escapeDownSent) {
return;
}
_escapeDownSent = false;
widget.control.triggerEvent("key_up", {"key": "Escape"});
}

KeyEventResult _handleEscapeLoop(KeyEvent keyEvent) {
final isEscape = keyEvent.logicalKey == LogicalKeyboardKey.escape;
final isPhysicalEscape =
isEscape && keyEvent.physicalKey == PhysicalKeyboardKey.escape;
final isPeriodEscape =
isEscape && keyEvent.physicalKey == PhysicalKeyboardKey.period;
final isMetaUp = keyEvent is KeyUpEvent &&
(keyEvent.physicalKey == PhysicalKeyboardKey.metaLeft ||
keyEvent.physicalKey == PhysicalKeyboardKey.metaRight ||
keyEvent.logicalKey == LogicalKeyboardKey.meta ||
keyEvent.logicalKey == LogicalKeyboardKey.metaLeft ||
keyEvent.logicalKey == LogicalKeyboardKey.metaRight);

if (keyEvent is KeyDownEvent &&
isPeriodEscape &&
HardwareKeyboard.instance.isMetaPressed) {
_triggerKeyEvent("key_down", keyEvent);
return KeyEventResult.handled;
}

if (keyEvent is KeyUpEvent && isPeriodEscape) {
_triggerEscapeKeyUp();
return KeyEventResult.handled;
}

if (_escapeDownSent && isMetaUp) {
_triggerEscapeKeyUp();
}

if (keyEvent is KeyUpEvent && isPhysicalEscape) {
_lastSynthesizedEscapeUpAt = DateTime.now();
if (keyEvent.synthesized) {
_triggerEscapeKeyUp();
return KeyEventResult.handled;
}
}

if (keyEvent is KeyDownEvent && isPhysicalEscape) {
final lastUpAt = _lastSynthesizedEscapeUpAt;
if (lastUpAt != null &&
DateTime.now().difference(lastUpAt) <
const Duration(milliseconds: 300)) {
return KeyEventResult.handled;
}
}

if (!isEscape) {
_lastSynthesizedEscapeUpAt = null;
}

return KeyEventResult.ignored;
}

@override
Widget build(BuildContext context) {
debugPrint("KeyboardListener build: ${widget.control.id}");
Expand All @@ -53,23 +125,26 @@ class _KeyboardListenerControlState extends State<KeyboardListenerControl> {
return const ErrorControl("KeyboardListener control has no content.");
}

return KeyboardListener(
return Focus(
focusNode: _focusNode,
autofocus: widget.control.getBool("autofocus", false)!,
includeSemantics: widget.control.getBool("include_semantics", true)!,
onKeyEvent: (keyEvent) {
onKeyEvent: (FocusNode node, KeyEvent keyEvent) {
final result = _handleEscapeLoop(keyEvent);
if (result == KeyEventResult.handled) {
return result;
}
if (keyEvent is KeyDownEvent) {
widget.control
.triggerEvent("key_down", {"key": keyEvent.logicalKey.keyLabel});
_triggerKeyEvent("key_down", keyEvent);
} else if (keyEvent is KeyUpEvent) {
widget.control
.triggerEvent("key_up", {"key": keyEvent.logicalKey.keyLabel});
_triggerKeyEvent("key_up", keyEvent);
} else if (keyEvent is KeyRepeatEvent) {
widget.control.triggerEvent(
"key_repeat", {"key": keyEvent.logicalKey.keyLabel});
_triggerKeyEvent("key_repeat", keyEvent);
}
return KeyEventResult.ignored;
},
child: content,
);
}
}

Loading