Textual 应用焦点事件 AppFocus:原理、监听与焦点恢复机制详解
Textual 应用焦点事件 AppFocus原理、监听与焦点恢复机制详解【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual本文围绕 Textual 的AppFocus事件展开它表示应用本身重新获得了焦点只在支持 XTerm FocusIn 焦点报告的终端或通过 textual-web 运行时才会被发出。文章结合源码解析该事件的产生链路终端转义序列解析、Web 驱动、框架内置的app_focus状态与焦点自动恢复机制并给出监听、CSS 伪类联动和测试验证的完整实践。什么是 AppFocus 事件AppFocus是 Textual 事件体系中的一个应用级事件定义在 events.pyclass AppFocus(Event, bubbleFalse): Sent when the app has focus. - [ ] Bubbles - [ ] Verbose Note: Only available when running within a terminal that supports FocusIn, or when running via textual-web. 它的语义是应用所在的窗口/标签页重新获得了终端或浏览器级别的输入焦点区别于Focus某个控件获得焦点。关键特性如下属性取值含义基类textual.events.Event应用级事件通常由驱动层直接投递给AppbubbleFalse不冒泡只发给接收消息的节点这里是App自身verbose未开启默认不进入调试日志可用前提终端支持FocusIn焦点报告或经 textual-web 运行普通不发送焦点报告的终端中该事件永远不会触发与它配对的是AppBlur——应用失去焦点时发出对应终端的FocusOut报告。官方 API 文档中两者的See also也是互相指向的见 app_focus.md 与 app_blur.md。事件是如何产生的两条来源链路终端侧解析 XTerm FocusIn/FocusOut 转义序列在终端运行时AppFocus来自 XTerm 的焦点报告转义序列。xterm 序列解析器 定义了三个特殊序列FOCUSIN: Final[str] \x1b[I # ... FOCUSOUT: Final[str] \x1b[O SPECIAL_SEQUENCES {BRACKETED_PASTE_START, BRACKETED_PASTE_END, FOCUSIN, FOCUSOUT}当解析器在读入的字节流中匹配到ESC [ I终端窗口获得焦点时由终端发出或ESC [ O失去焦点时会直接构造对应的事件并交给上层见 解析循环if sequence FOCUSIN: on_token(events.AppFocus()) elif sequence FOCUSOUT: on_token(events.AppBlur())需要注意的前提并不是所有终端都会主动发送焦点报告事件文档中的 Note 明确写明了这一限制。因此是否收到AppFocus依赖于运行环境——这也是为什么框架把它实现为一个可降级的事件收不到就永远不触发应用逻辑应当把它当作可选信号而非必需信号。Web 侧textual-web 驱动直接投递当应用通过 textual-web 运行在浏览器中时焦点管理由页面 JavaScript 侧感知。web 驱动 在页面重新聚焦时会向应用投递事件self._app.post_message(events.AppFocus())源码中_on_app_focus处理函数的注释也印证了这一点Required by textual-web to manage focus in a web page见下文。框架内置行为app_focus 状态与 CSS 伪类App内部为每个应用维护了一个响应式Reactive状态app_focus见 app.pyapp_focus Reactive(True, computeFalse)这个状态有两条用途驱动 CSS:focus/:blur伪类在应用层的解析。app.py 中的伪类映射将focus解析为app.app_focus、blur解析为not app.app_focus。也就是说当应用失去焦点时样式中依赖焦点状态的规则会随之切换。供 UI 组件判断应用是否在前台。例如页脚组件 _footer.py、帮助面板 _help_panel.py、按键面板 _key_panel.py 都会检查screen.app.app_focus在应用未获焦点时隐藏相关 UIscreen.py 中也有类似判断。另外还有一条兜底逻辑如果应用尚未收到任何焦点事件但用户直接按了键或点击了鼠标框架会推断应用已重新获得焦点见 app.pyif not self.app_focus and isinstance(event, (events.Key, events.MouseDown)): self.app_focus True这保证了即使所在终端不支持焦点报告app_focus状态也能随着用户交互被救活。焦点恢复机制AppBlur 记录、AppFocus 还原这是AppFocus在整个框架中最核心的内置消费场景实现在 App._watch_app_focus 中失焦时app_focus变为False把当前屏幕的焦点控件记录到self._last_focused_on_app_blur该字段定义见 app.py注释即说明它是上一次AppBlur时持有焦点的控件用于在AppFocus时恢复正确焦点然后调用self.screen.set_focus(None)清空焦点。重新获焦时app_focus变为True如果记录中仍有该控件、它仍在当前屏幕上、且当前没有任何焦点则通过self.screen.set_focus(..., scroll_visibleFalse, from_app_focusTrue)把焦点还原到原控件且刻意不滚动源码注释说明滚动会带来突兀感。还原完成后清空记录避免持有已销毁控件的引用。App对事件本身的直接处理则很轻见 _on_app_focusasync def _on_app_focus(self, event: events.AppFocus) - None: App has focus. # Required by textual-web to manage focus in a web page. self.app_focus True self.screen.refresh_bindings()即置位app_focus状态触发上面的 watcher 与 CSS 重算并刷新屏幕绑定显示页脚按键提示等依赖焦点状态的展示。_on_app_blur是对称逻辑。from_app_focus区分应用级焦点与控件级焦点Focus事件携带了一个与AppFocus直接相关的标志from_app_focus见 events.pyTrue if this focus event has been sent because the app itself has regained focus (via an AppFocus event). False if the focus came from within the Textual app (e.g. via the user pressing tab or a programmatic setting of the focused widget).典型消费方是Input的获焦即全选行为见 _input.pyif self.select_on_focus and not event.from_app_focus:即当焦点是由应用重新获得焦点而还原回来时Input不会触发全选避免用户切回窗口后正在编辑的文本被整体选中。这是AppFocus与控件级焦点交互的典型联动细节。在自己的应用中监听 AppFocus在App子类中通过on装饰器监听即可from textual.app import App, ComposeResult from textual.events import AppBlur, AppFocus from textual.widgets import Label, Static class FocusTrackerApp(App): 监听应用级焦点变化的示例。 def compose(self) - ComposeResult: yield Label(当前应用状态待命) yield Static() on(AppFocus) def on_app_focus(self) - None: # 应用重新获得终端/页面焦点 self.query_one(#status, Label).update(当前应用状态已聚焦) on(AppBlur) def on_app_blur(self) - None: # 应用失去焦点例如用户切换了终端标签页 self.query_one(#status, Label).update(当前应用状态已失焦)使用建议由于事件仅在终端支持 FocusIn 或 textual-web 时可用监听逻辑应当是渐进增强式的事件不来应用照常工作事件来了做一些暂停计时、暂停动画、保存临时状态之类的善后。AppFocus/AppBlur不冒泡监听器应放在App或直接向App投递消息上而不是普通控件上。事件到达时会先经过App内置的_on_app_focus置位app_focus、刷新绑定随后on(AppFocus)注册的用户处理器才被调用因此你的处理器里读取self.app_focus得到的一定是已更新的值。如何测试 AppFocus 相关行为由于真实终端焦点报告无法在 CI 中复现Textual 的测试方式是在run_test()上下文中直接向应用投递事件见 test_app_focus_blur.pyfrom textual.events import AppBlur, AppFocus async def test_app_focus_restores_focus() - None: async with FocusBlurApp().run_test() as pilot: assert pilot.app.focused.id input-4 # AUTO_FOCUS 初始聚焦 pilot.app.post_message(AppBlur()) await pilot.pause() assert pilot.app.focused is None # 失焦清空焦点 pilot.app.post_message(AppFocus()) await pilot.pause() assert pilot.app.focused.id input-4 # 重新获焦后焦点被还原该测试文件覆盖了焦点恢复机制的全部边界场景适合作为自己实现参考时对照的用例清单test_app_blurAppBlur会清空当前焦点test_app_focus_restores_focusAppFocus将焦点还原到失焦前记录的控件test_app_focus_restores_none_focus失焦前若本来就没有焦点AppFocus不会凭空制造焦点;test_app_focus_handles_missing_widget失焦期间若原控件已被移除AppFocus恢复流程安全降级不报错、不设置焦点test_app_focus_defers_to_new_focus失焦期间若已有新控件获得焦点AppFocus不会覆盖新焦点。小结AppFocus及其配对事件AppBlur是 Textual 中少数由运行环境决定是否送达的事件其设计要点可以归纳为来源终端 XTermESC [ I/ESC [ O焦点报告由 _xterm_parser.py 转译为事件或 textual-web 驱动 直接投递。状态内置app_focusReactive 驱动 CSS:focus/:blur伪类与页脚等 UI 的显隐并有按键/点击兜底置位逻辑。恢复AppBlur时记录焦点控件AppFocus时按条件还原不滚动、不覆盖新焦点、容忍控件已销毁且通过Focus.from_app_focus标志向Input等控件告知这次聚焦来自应用级焦点事件抑制选边副作用如全选。实践监听时按渐进增强思路编写逻辑测试时通过pilot.app.post_message(AppFocus())模拟事件即可完整验证。事件体系的更多背景可参考 事件指南 与 AppBlur 文档。【免费下载链接】textualThe lean application framework for Python. Build sophisticated user interfaces with a simple Python API. Run your apps in the terminal and a web browser.项目地址: https://gitcode.com/gh_mirrors/te/textual创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考