From 080cb9abf7f73f441da7584960856af00cedf4d7 Mon Sep 17 00:00:00 2001 From: 7015725 Date: Wed, 13 May 2026 07:27:39 +0800 Subject: [PATCH] refactor: keep ToolApp as single root view --- code/th_02_core.js | 4 ++ code/th_09_animation.js | 7 +++ code/th_15_extra.js | 82 ++++++++++++++++++++++++--- manifest.json | 14 ++--- manifest.sig | 2 +- scripts/verify_toolapp_single_root.py | 37 ++++++++++++ 6 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 scripts/verify_toolapp_single_root.py diff --git a/code/th_02_core.js b/code/th_02_core.js index 77575a0..6655e2e 100644 --- a/code/th_02_core.js +++ b/code/th_02_core.js @@ -50,6 +50,10 @@ function FloatBallAppWM(logger) { toolAppActive: false, toolAppNavStack: [], toolAppRoute: null, + toolAppRoot: null, + toolAppContentHost: null, + toolAppTitleView: null, + toolAppBackButton: null, settingsGroupKey: null, mask: null, diff --git a/code/th_09_animation.js b/code/th_09_animation.js index 4e6982d..aec5e31 100644 --- a/code/th_09_animation.js +++ b/code/th_09_animation.js @@ -183,10 +183,17 @@ FloatBallAppWM.prototype.hideViewerPanel = function() { if (!this.state.addedViewer) return; if (!this.state.viewerPanel) return; + var oldViewerType = String(this.state.viewerPanelType || ""); this.safeRemoveView(this.state.viewerPanel, "viewerPanel"); this.state.viewerPanel = null; this.state.viewerPanelLp = null; this.state.viewerPanelType = null; + if (oldViewerType === "tool_app") { + this.state.toolAppRoot = null; + this.state.toolAppContentHost = null; + this.state.toolAppTitleView = null; + this.state.toolAppBackButton = null; + } this.state.addedViewer = false; this.hideMask(); diff --git a/code/th_15_extra.js b/code/th_15_extra.js index ecf0027..051e49f 100644 --- a/code/th_15_extra.js +++ b/code/th_15_extra.js @@ -509,6 +509,10 @@ FloatBallAppWM.prototype.closeToolApp = function() { this.state.toolAppNavStack = []; this.state.settingsGroupKey = null; this.hideViewerPanel(); + this.state.toolAppRoot = null; + this.state.toolAppContentHost = null; + this.state.toolAppTitleView = null; + this.state.toolAppBackButton = null; } catch (e) { safeLog(this.L, 'e', "closeToolApp fail: " + String(e)); } }; @@ -527,11 +531,10 @@ FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBac bar.setPadding(this.dp(8), this.dp(8), this.dp(8), this.dp(6)); var btnBack = this.ui.createFlatButton(this, canBack ? "‹" : "", C.primary, function() { - if (canBack) self.popToolAppPage("topbar"); + self.popToolAppPage("topbar"); }); btnBack.setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 24); btnBack.setPadding(this.dp(8), 0, this.dp(8), 0); - if (!canBack) btnBack.setVisibility(android.view.View.INVISIBLE); bar.addView(btnBack, new android.widget.LinearLayout.LayoutParams(this.dp(42), this.dp(38))); var tvTitle = new android.widget.TextView(context); @@ -552,12 +555,58 @@ FloatBallAppWM.prototype.buildToolAppShell = function(contentView, title, canBac bar.addView(btnClose, new android.widget.LinearLayout.LayoutParams(this.dp(42), this.dp(38))); root.addView(bar, new android.widget.LinearLayout.LayoutParams(-1, this.dp(52))); - try { contentView.setBackground(null); } catch(eBg) { safeLog(null, 'e', "catch " + String(eBg)); } - try { contentView.setElevation(0); } catch(eEl) { safeLog(null, 'e', "catch " + String(eEl)); } - root.addView(contentView, new android.widget.LinearLayout.LayoutParams(-1, 0, 1)); + var host = new android.widget.FrameLayout(context); + if (contentView) { + try { contentView.setBackground(null); } catch(eBg) { safeLog(null, 'e', "catch " + String(eBg)); } + try { contentView.setElevation(0); } catch(eEl) { safeLog(null, 'e', "catch " + String(eEl)); } + host.addView(contentView, new android.widget.FrameLayout.LayoutParams(-1, -1)); + } + root.addView(host, new android.widget.LinearLayout.LayoutParams(-1, 0, 1)); + + this.state.toolAppRoot = root; + this.state.toolAppContentHost = host; + this.state.toolAppTitleView = tvTitle; + this.state.toolAppBackButton = btnBack; + this.updateToolAppShellChrome(title, canBack); return root; }; +FloatBallAppWM.prototype.ensureToolAppShell = function() { + try { + if (this.state.toolAppRoot && this.state.toolAppContentHost) return this.state.toolAppRoot; + return this.buildToolAppShell(null, "ToolHub", false); + } catch (e) { + safeLog(this.L, 'e', "ensureToolAppShell fail: " + String(e)); + } + return null; +}; + +FloatBallAppWM.prototype.updateToolAppShellChrome = function(title, canBack) { + try { + if (this.state.toolAppTitleView) this.state.toolAppTitleView.setText(String(title || "ToolHub")); + if (this.state.toolAppBackButton) { + this.state.toolAppBackButton.setText(canBack ? "‹" : ""); + this.state.toolAppBackButton.setVisibility(canBack ? android.view.View.VISIBLE : android.view.View.INVISIBLE); + this.state.toolAppBackButton.setEnabled(!!canBack); + } + } catch (e) { safeLog(this.L, 'w', "updateToolAppShellChrome fail: " + String(e)); } +}; + +FloatBallAppWM.prototype.setToolAppContent = function(contentView) { + try { + var host = this.state.toolAppContentHost; + if (!host || !contentView) return false; + host.removeAllViews(); + try { contentView.setBackground(null); } catch(eBg) { safeLog(null, 'e', "catch " + String(eBg)); } + try { contentView.setElevation(0); } catch(eEl) { safeLog(null, 'e', "catch " + String(eEl)); } + host.addView(contentView, new android.widget.FrameLayout.LayoutParams(-1, -1)); + return true; + } catch (e) { + safeLog(this.L, 'e', "setToolAppContent fail: " + String(e)); + } + return false; +}; + FloatBallAppWM.prototype.showToolApp = function(route, resetStack) { if (this.state.closing) return; var r = this.isToolAppRoute(route) ? String(route) : "settings"; @@ -565,7 +614,6 @@ FloatBallAppWM.prototype.showToolApp = function(route, resetStack) { this.touchActivity(); this.hideMainPanel(); this.hideSettingsPanel(); - this.hideViewerPanel(); this.showMask(); this.state.toolAppActive = true; this.state.toolAppRoute = r; @@ -582,7 +630,11 @@ FloatBallAppWM.prototype.showToolApp = function(route, resetStack) { } var raw = this.buildPanelView(r); - var shell = this.buildToolAppShell(raw, this.getToolAppTitle(r), this.state.toolAppNavStack.length > 1); + var shell = this.ensureToolAppShell(); + if (!shell) throw "ToolApp shell missing"; + this.updateToolAppShellChrome(this.getToolAppTitle(r), this.state.toolAppNavStack.length > 1); + this.setToolAppContent(raw); + var maxW = Math.floor(this.state.screen.w * 0.92); var maxH = Math.floor(this.state.screen.h * 0.82); shell.measure( @@ -595,8 +647,20 @@ FloatBallAppWM.prototype.showToolApp = function(route, resetStack) { if (!lp0) lp0 = new android.view.ViewGroup.LayoutParams(pw, ph); lp0.width = pw; lp0.height = ph; shell.setLayoutParams(lp0); - var pos = this.getBestPanelPosition(pw, ph, this.state.ballLp.x, this.state.ballLp.y, this.getDockInfo().ballSize); - this.addPanel(shell, pos.x, pos.y, "tool_app"); + + if (!this.state.addedViewer || this.state.viewerPanel !== shell) { + var pos = this.getBestPanelPosition(pw, ph, this.state.ballLp.x, this.state.ballLp.y, this.getDockInfo().ballSize); + this.addPanel(shell, pos.x, pos.y, "tool_app"); + } else { + try { + if (this.state.viewerPanelLp) { + this.state.viewerPanelLp.width = pw; + this.state.viewerPanelLp.height = ph; + this.state.wm.updateViewLayout(shell, this.state.viewerPanelLp); + } + } catch (eUpd) { safeLog(this.L, 'w', "tool_app update layout fail: " + String(eUpd)); } + try { shell.requestFocus(); } catch (eFocus) {} + } } catch (e) { this.state.toolAppActive = false; safeLog(this.L, 'e', "showToolApp fail route=" + r + " err=" + String(e)); diff --git a/manifest.json b/manifest.json index 6db5d1d..47f0c05 100644 --- a/manifest.json +++ b/manifest.json @@ -6,8 +6,8 @@ "size": 52546 }, "th_02_core.js": { - "sha256": "72c208f17d79c35926f30ab00482f0cf901d7aa42a2c703836e35b6d9953840a", - "size": 4313 + "sha256": "23b94eb4afbf978a75363b412b542d4f2b1f5260c63ab966dcf5b44076a05b6f", + "size": 4423 }, "th_03_icon.js": { "sha256": "717f7f37474d3616c2cd944581455f600020a850ec8812100d0546ec1302c987", @@ -34,8 +34,8 @@ "size": 7938 }, "th_09_animation.js": { - "sha256": "35cb81040678cfb164992f1369fdb303424457fafa6a6fe0c74cbf9736f1263d", - "size": 34943 + "sha256": "8c8c0c166816c36569923de31c2951d16fce5ca2e68168cb6190283e058d14c3", + "size": 35207 }, "th_10_shell.js": { "sha256": "0ed793079c2f6ba7d29f4c0d411705cb72419f45f572cbe37ed32ac16527a8bc", @@ -58,8 +58,8 @@ "size": 237123 }, "th_15_extra.js": { - "sha256": "b48c16ea5cbe5b9033d593eb18ef3028c0d661c65cbf2ad8f3f650ee55bbe2e4", - "size": 71280 + "sha256": "554092fb880e1f645e8665feac752b7412c3a9b604f960ad73f7fa889c8e39a8", + "size": 73786 }, "th_16_entry.js": { "sha256": "e7c99c3dfbd6aedab05551426955081ae6cae034754f2f557cefa01dc75dc001", @@ -68,5 +68,5 @@ }, "keyId": "toolhub-targets-2026-rsa3072", "schema": 2, - "version": 20260512232015 + "version": 20260512232713 } diff --git a/manifest.sig b/manifest.sig index 421429c..4e91f32 100644 --- a/manifest.sig +++ b/manifest.sig @@ -1 +1 @@ -PnK7gl+dD9S3+8iK6E0lRmWVT8ike/ZOKniNy3O3yIdUGIkLiMBQXSLshHsj4bL15MQOweu8o9FLSEgqj1Htk4Viao7K8QbYhaf92LMVnPMXiPcA0d6t/v0VO/QSNQT5h74dVCk9fnwBlzwrGrSsRUU5ejuwyDMyEOPFhZQLnetpjql2NSRQEOOX0UNfAT4BHjV/MSchiAzJE+wcAPudNLqdOGjKN2flmU8YVOt9zc2PFszCYW4gtWMp0ETCkp3Uw3BbY8FPkbz8u17zM8+WDrrHBUMAoMtUULtcHX0EBgrRsCvLPeSKJSWuJaltLYA5A+tAGz9w2BccptGrQhO4Sn7rMWOpqn+2ajkiutTllPYPGvOTSI6m+GiOvctuIG9NyeR8BJEdX4IVkY9f+wh2Qmp1HliKU6YOoy/hvpAa36nm1UNGzYuJ195TqBv6CFU6eEkdLb0IXHvDnmHX3KB39q5OyItyST5eKKnQ5irEo3AkPTOktqnJSSbGy4IbxOwD +Y5kHcy+jDtyrB0AAvEgLdWkQr85CJ7UAvOtzDrp93yI2c+PAItrzWjh+inSskBv7UBa7bHIDXGHVPBN9O5qIPn2Zgb01baozxz+KqO4WomLdEKWTOrRhrla2rJAKf7hcrTLnI2WjHWgg2kMikypvm1S45vcwu1u4uVBEFwE9EExrvjmQiHShqWTJ8ejiMbV3AqQFKCA0QpLujPscedW8YFJOpTJarwKIwSTuAGJdLtKh7ixgzB46kcqkxb+bVdi0+kEblY+heYlTxP5p7F75n0X1aIOuRXYnuGf3ODQi+rioTHYRTQGoJ36SNzxgIHS0JyEDgeduJEhaYaYXLz7BaKFFsJ+2SWPyKS0rx+XW+j91Am9C+wCXKNcPa5/r1y1D/hxtEHSK/YEYVX7IYrCNx3xMJVfILzpCB9lFpEiHNjw0qZU/hjXnRzJhstkK7wrMt6kfvmX7feG34a3t8jlDa0H7Kc3BfkO1wn+wT/P/VurdiaEa5xPU6OPUnyb4fdHY diff --git a/scripts/verify_toolapp_single_root.py b/scripts/verify_toolapp_single_root.py new file mode 100644 index 0000000..6d8a606 --- /dev/null +++ b/scripts/verify_toolapp_single_root.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +import pathlib +import re +import sys + +root = pathlib.Path(__file__).resolve().parents[1] +core = (root / "code" / "th_02_core.js").read_text(encoding="utf-8") +extra = (root / "code" / "th_15_extra.js").read_text(encoding="utf-8") + +checks = [] + +def check(name, ok): + checks.append((name, bool(ok))) + +check("state keeps a single ToolApp root view", "toolAppRoot" in core) +check("state keeps ToolApp content host", "toolAppContentHost" in core) +check("state keeps ToolApp title view", "toolAppTitleView" in core) +check("state keeps ToolApp back button", "toolAppBackButton" in core) +check("has ensureToolAppShell", "FloatBallAppWM.prototype.ensureToolAppShell" in extra) +check("has updateToolAppShellChrome", "FloatBallAppWM.prototype.updateToolAppShellChrome" in extra) +check("has setToolAppContent", "FloatBallAppWM.prototype.setToolAppContent" in extra) + +show_match = re.search(r"FloatBallAppWM\.prototype\.showToolApp\s*=\s*function\([^)]*\)\s*\{(?P.*?)\n\};", extra, re.S) +show_body = show_match.group("body") if show_match else "" +check("showToolApp exists", bool(show_match)) +check("showToolApp does not remove viewer on page switch", "this.hideViewerPanel();" not in show_body) +check("showToolApp ensures shell once", "this.ensureToolAppShell" in show_body) +check("showToolApp swaps content host", "this.setToolAppContent" in show_body) +check("showToolApp only addPanel when shell not added", "!this.state.addedViewer" in show_body and "this.addPanel" in show_body) + +failed = [name for name, ok in checks if not ok] +if failed: + print("ToolApp single-root verification FAILED:") + for name in failed: + print(" - " + name) + sys.exit(1) +print("ToolApp single-root verification OK")