☰
MooTool(Compose 桌面版)F 工具引擎接线:WiringPresentation 守卫模式、随手记 Vault 底栏与命令盘深链导航实现解析
2026/10/16 6:10:55 网站建设 项目流程
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

导读

本文以 DIFF-556 设计文档为主体,剖析 MooTool Compose 桌面版在单一 diff 切片中完成的四类工作:九个 F 工具的引擎接线与工具栏操作启用守卫(*WiringPresentation纯函数层)、随手记(QuickNote)Vault 底栏的路径/脏标记/复制守卫、五个 CSS 风格组件的落地,以及命令盘(Command Palette)中工具与设置类别的深链导航关键词。读完本文,你将理解 MooTool 如何用"可单测的 Presentation 层 + 引擎层 + Screen 接线层"的分层方式推进功能落地,以及如何用一条 Gradle 命令闭环验证全部改动。

一、背景:DIFF 切片推进与 parity-gap 上下文

DIFF-556 是 MooTool Compose 版(next-compose模块)一系列增量切片中的一环。按其自身说明,前一条 DIFF-555 已经完成了 F10/F13/F14/F19 四个工具的引擎接线、JSON 列编辑 wrap notice、JsonVaultFooterPresentation、更新/托盘链路以及 crypto/vault 相关 CSS,因此本条明确不重复以下内容:

  • Crypto / Encode / Host / MessageBoard 四工具的接线;
  • JSON 列编辑(EditorHost 语义)与 Json 底栏;
  • Update / Tray 链路。

同时,文档在"背景"处保留了对外部差异清单(parity-gap)的承诺状态:仍有六套 CSS 逐选择器皮肤、产品主窗 Tab/IME/TCC 手工 PNG、P7 三平台安装、以及部分 F 工具 substantial(实质性)缺口未达成。这一点说明 DIFF-556 的定位是稳步补齐非 metadata 层面的工具引擎链路,而非一次性完成全部 parity 目标。

从仓库实际结构看,"wiring" 一词对应的是desktopMain与commonMain中的一批*WiringPresentation对象,以及各features/*目录下的 Screen。这个命名约定在本条之前(DIFF-555)已经确立,DIFF-556 只是把同一套模式扩展到其余九个工具。

二、核心模式:Presentation 守卫 + 引擎 + Screen 接线三层分工

要读懂 DIFF-556,先要理解它反复强调"(非 metadata)"的含义。MooTool Compose 版的工具逻辑被拆成三个层次:

  1. 引擎层(Engine):如ColorEngine、CalculatorEngine、CronEngine、RegexEngine、UaEngine、TimeEngine、DiffEngine、ConfigEngine、ImageEngine——只负责纯计算/转换,不感知 UI。
  2. Presentation 层(Wiring Presentation):以object形式暴露一批纯函数,提供"工具栏操作启用守卫"(如canXxx/xxxActionEnabled)与run*包装方法,把引擎调用包进runCatching并折叠为Success/Failure密封结果。这一层不依赖 Compose,因此可以直接跑桌面单测。
  3. Screen 接线层:对应features/*目录下的*Screen.kt,将 Presentation 守卫接到真实按钮的enabled属性,将run*结果接到状态提示(notice/toast)。

文档正文列出了本条涉及的九条链路,与源码一一对应:

工具编号工具Presentation 对象核心守卫
F22调色板ColorWiringPresentation取色/应用代码/收藏
F21计算器CalculatorWiringPresentation运算/复制/进制转换
F06配置转换ConfigWiringPresentationYAML↔Properties 转换/校验/导出
F12UA 解析UaWiringPresentation解析/复制结果
F15正则RegexWiringPresentation运行测试/取消
F18时间转换TimeWiringPresentation时间戳↔本地时间/复制字段
F23图片ImageWiringPresentation导入/批量处理/水印/base64 导出
F02文本对比TextDiffPresentation差异导航/手动比较
F16CronCronWiringPresentation解析/复制运行结果

下面对每一类逐一展开源码级细节。

2.1 F22 调色板:ColorWiringPresentation

ColorWiringPresentation.kt 提供四个守卫与一个引擎路径:

  • canScreenPick(picking):取色过程中(picking == true)禁止再次启动屏幕取色;
  • canApplyCode(code):颜色代码 trim 后非空才允许应用;
  • canFavorite(favoriteFolderId):收藏目录 id 非空才允许收藏;
  • runFormatColor(primary, format):委托ColorEngine.formatColor完成 RGB→指定格式的格式化输出。

此外shouldToastOperationFailure/shouldToastErrorMessage恒为true,表示该工具在操作失败或出现错误信息时总是弹出 toast。对应的 ColorWiringPresentationTest.kt 用assertFalse(ColorWiringPresentation.canScreenPick(picking = true))、assertFalse(canApplyCode(" "))等用例锁定了这些边界行为。

2.2 F21 计算器:CalculatorWiringPresentation

CalculatorWiringPresentation.kt 覆盖的守卫更细:

  • canEvaluate(expression):表达式非空才允许求值;
  • canCopyResult(result):结果非空才允许复制;
  • canConvertField(value):进制转换输入非空;
  • convertBaseActionEnabled(value, fromRadix, toRadix):不仅要求输入非空,还会预先调用CalculatorEngine.convertBase探测是否可转换,异常时返回false(按钮禁用);
  • canBinaryOp(first, second):GCD/LCM 等二元运算要求两个操作数都非空。

run*系列覆盖evaluateExpression、convertBase、gcd、lcm、permutation、combination,全部折叠为TextOutcome.Success/Failure。这也是 DIFF-556 中守卫数量最密集的工具之一,反映计算器工具栏按钮大多处于"有输入才可点"的状态。

2.3 F06 配置转换:ConfigWiringPresentation

ConfigWiringPresentation.kt 面向 YAML ↔.properties互转:

  • canToYaml(properties)/canToProperties(yaml)/canValidateSource(source):三种源文本非空守卫;
  • propertiesToYamlActionEnabled/yamlToPropertiesActionEnabled/validateYamlActionEnabled/formatYamlActionEnabled:对应四个工具栏动作;
  • exportTextActionEnabled(content):侧栏导出在对应 pane 无文本时禁用,注释明确"对齐ConfigConvertScreen侧栏导出"。

引擎路径包括ConfigEngine.propertiesToYaml、ConfigEngine.yamlToProperties、ConfigEngine.validateYaml(返回YamlValidation)、ConfigEngine.formatYaml;文件读写则封装为runReadImportFile(UTF-8 读取)与runWriteExportFile(UTF-8 写出),并各自有shouldToastIoFailure。这些 IO 路径被设计成可单测的纯函数,与 Electron 版 Java 侧行为对齐。

2.4 F12 UA 解析:UaWiringPresentation

UaWiringPresentation.kt 很简单:

  • canParse(source):UA 字符串非空才允许解析;
  • canCopyResult(result):解析结果非空才允许复制;
  • runParse(source)委托UaEngine.parse,折叠为ParseOutcome.Success(Failure)。

对应的 UaRegexTimeWiringPresentationTest.kt 使用UaEngine.presets.first().second作为样例 UA 输入验证成功路径。

2.5 F15 正则:RegexWiringPresentation

RegexWiringPresentation.kt 的守卫关注运行态:

  • canRunTest(pattern, running):pattern 非空且当前不在运行中;
  • showCancel(running)/cancelTestActionEnabled(running):运行中才显示取消按钮。

引擎路径runMatch(pattern, source, options, maxMatches)委托RegexEngine.match,默认匹配上限RegexEngine.DEFAULT_MAX_MATCHES,折叠为MatchOutcome.Success/Failure。注释强调"UI 仍经RegexWorkerClient进程隔离",即正则的实际执行仍走 worker 进程,Presentation 层只负责守卫与结果折叠。测试用例还验证了runMatch结果与直接调用RegexEngine.match完全一致(assertEquals(direct, ...)),以及非法模式"("会折叠为Failure并携带RegexException。

2.6 F18 时间转换:TimeWiringPresentation

TimeWiringPresentation.kt 提供双向转换守卫:

  • canConvertTimestamp(timestamp)/canConvertLocal(localTime):两种输入方向的非空守卫;
  • canCopyField(value):字段复制守卫;
  • runTimestampToLocal(timestamp, unit, zone)/runLocalToTimestamp(localTime, unit, zone):委托TimeEngine,支持TimestampUnit(如 Second)与zone时区参数。

TimeWiringPresentationTest.kt 给出了很好的可验证样例:runTimestampToLocal("0", TimestampUnit.Second, "Asia/Shanghai")得到"1970-01-01 08:00:00";反向runLocalToTimestamp("1970-01-01 08:00:00", ..., "Asia/Shanghai")得到"0";而非法输入"nope"折叠为Failure。

2.7 F23 图片:ImageWiringPresentation

ImageWiringPresentation.kt 的守卫以busy 状态与选中数量为核心:

  • canImport(busy):忙碌时禁止导入;
  • canProcessSelection(selectedCount, busy):有选中项且不忙碌;
  • canActOnCurrent(hasCurrent, busy):存在当前图片且不忙碌;
  • canCopyCurrent(hasCurrent):仅要求存在当前项;
  • canExportBase64(text)/canStartWatermark(text):文本非空守卫;
  • renamePromptActionEnabled(name):重命名输入非空。

引擎路径runDecodeDataUrl(text)委托ImageEngine.decodeDataUrl得到BufferedImage;runExportAssets(block)把资源导出闭包包装成ExportAssetsOutcome。值得注意shouldToastProcessFailure的取消豁免逻辑:当错误是CancellationException或ImageException.code == "cancelled"时不弹 toast,其余才弹——这与 F15 正则的shouldToastWorkerError(code)(code != "cancelled"才提示)是同一套"用户主动取消不算失败"的交互约定。注释还提示 SVG 数值 clamp 归ImageSvgWiringPresentation负责,不在本条范围。

2.8 F02 文本对比:TextDiffPresentation 扩展

TextDiffPresentation.kt 在本条新增了两个能力(文档原话"扩展"):

  • canNavigateDiffs(visibleSegmentCount):可见差异段数 > 0 才允许上下导航;
  • canManualCompare(left, right):两侧文本至少一侧非空才允许手动比较。

围绕它们还有三个"动作使能"别名:manualCompareActionEnabled、navigateDiffActionEnabled、copyPatchActionEnabled(unified)(无 unified 文本时禁用复制补丁)。文件头部保留了既有常量AUTO_COMPARE_DEBOUNCE_MS = 160L(自动对比防抖 160ms,对齐 Electron"输入后短延迟重算"),以及nextNavIndex(current, step, size)的循环导航索引计算((current + step + size) % size,size ≤ 0 返回 -1)。runCompare委托DiffEngine.compare(left, right, ignoreWhitespace);导入则封装runReadImportFile。测试 TextDiffPresentationTest.kt 验证了防抖常量、循环索引、导航/比较/复制三类守卫,以及缺失文件导入折叠为Failure。

2.9 F16 Cron:CronWiringPresentation 扩展

CronWiringPresentation.kt 在本条同样以"扩展"方式加入:

  • canParse(expression)/parseActionEnabled(expression):表达式非空才允许解析;
  • canCopyRuns(runs)/copyRunsActionEnabled(runs):运行结果列表非空才允许复制;
  • runPreview(expression, zone, language):委托CronEngine.nextRuns与CronEngine.describe,折叠为ScheduleOutcome.Success(runs, description) / Failure;
  • shouldToastPreviewFailure恒为true。

文件中还保留了本条之前已有的timezoneMenu(系统时区去重合并)与coerceSessionZone(空白时区回退到 fallback),用于时区菜单与 session 区规范化。测试 CronWiringPresentationTest.kt 用"0 0 * * *"与"0 0 12 * * ?"(Quartz 风格)验证了守卫与引擎路径,其中runPreview("0 0 12 * * ?", "UTC", "en-US")必须返回非空 runs。

2.10 Screen 接线:从守卫到按钮

文档强调上述每条链路都伴随"对应 Screen 接线"。以最典型的一处为例,QuickNoteScreen.kt 中随手记 Vault 底栏的路径计算与脏标记展示直接调用QuickNoteVaultFooterPresentation.effectivePath(...)、showFooter(...)、footerDirty(...),再配合VaultSelectionFooter组件与mooQuickNoteVaultFooter样式渲染。其余工具的 Screen(ColorBoardScreen、CalculatorScreen、ConfigConvertScreen、UaParseScreen、RegexScreen、TimeConvertScreen、ImageScreen、TextDiffScreen、CronScreen)同样以xxxActionEnabled(...)绑定按钮enabled。

三、F01 随手记 Vault 底栏:QuickNoteVaultFooterPresentation

DIFF-556 的 Vault 部分是一个新引入的公共对象 QuickNoteVaultFooterPresentation.kt(位于commonMain,说明可在多平台共享),其定位是"对齐JsonVaultFooterPresentation":

  • effectivePath(vaultSelectedPath, currentFile):直接复用 Json 版本的"优先取树选中路径"规则;
  • showFooter(path):决定是否展示底栏;
  • footerDirty(path, currentFile, documentDirty):仅当路径等于当前打开文件且正文脏时标记脏(path == currentFile && documentDirty);
  • canDuplicate(isDirectory):目录不可重复,复用 Json 规则;
  • gitFlushSkipsWhenClean(currentFile, documentDirty):无打开笔记或正文已经保存时,Vault Git 操作前的 flush 可以跳过写盘;
  • gitUntitledBlockKey(currentFile, editorText, sampleText):无 Vault 笔记但编辑器有"非示例内容"时,阻止 Git 操作并返回 i18n 键git.flush.untitled;若编辑器内容恰好等于示例文本或已有打开文件,则不阻止。

这个"脏标记只看当前文件"的设计在测试 QuickNoteVaultFooterPresentationTest.kt 中非常直白:footerDirty("a.md", "b.md", documentDirty = true)必须为false;gitFlushSkipsWhenClean("", true)与gitFlushSkipsWhenClean("a.md", false)为true;gitUntitledBlockKey("", "draft", "sample")返回"git.flush.untitled",而把"draft"换成示例文本"sample"或传入已打开文件"a.md"时返回null。

在 Screen 侧,QuickNoteScreen的底栏动作行(重命名/复制/移动/删除)全部以mooQuickNoteVaultFooter()+mooQuickNoteVaultFooterActions()样式承载,canDuplicate(footerEntry?.directory)控制复制按钮显隐,形成了与 JSON Vault 一致的底栏交互体验。

四、CSS 组件批次:五个 moo 风格修饰符

DIFF-556 的样式切片包含五个组件,全部集中在 Controls.kt,每一个都在注释中标明了对应的 Electron 选择器:

样式修饰符实现对齐的 Electron 选择器 / 语义
mooQuickNoteVaultFooter()mooJsonVaultFooter()(heightIn(min = 36.dp)+ 顶部分隔线drawBehind).quick-note-vault-footer,与 JSON 底栏共用分隔样式
mooColorFormatRow()heightIn(min = 46.dp)调色板格式/取色工具条
mooUaParseBar()heightIn(min = 46.dp)UA 解析顶栏操作行
mooConfigTabsRow()heightIn(min = 40.dp)配置转换 Tab 行
mooDiffNavCluster()heightIn(min = 34.dp)文本对比工具栏差异导航区

这些修饰符本质是最小行高约束 + 少量绘制,用极低的成本复刻 Electron 版各工具的工具栏/底栏密度。它们在仓库中同时被界面代码与"样式捕获测试"引用:例如 TextDiffNavCaptureTest.kt 使用mooDiffNavCluster(),QuickNoteVaultFooterCaptureTest.kt 使用mooQuickNoteVaultFooter(),ColorScreenPickCaptureTest.kt 使用mooColorFormatRow(),ConfigConvertTabCaptureTest.kt 使用mooConfigTabsRow()。这些 capture 测试的存在说明样式并非"随手加的 magic modifier",而是有截图/捕获证据支撑的逐一落地。

五、A02 导航深链:命令盘关键词与工具注册表

DIFF-556 的导航部分对两个目录做了深链关键词扩展,目标是让命令盘(Command Palette)输入关键词能命中工具或设置分类:

5.1 CommandSearchCatalog:设置分类深链

CommandSearchCatalog.kt 定义了CommandSettingsTarget(categoryId, labelKey, navIcon, keywords),覆盖general / ai / vault / editor / network / runtime / tools / data / appearance / layout / shortcuts / about共 12 个设置分类。其关键词表中英日三语混排,以本条的tools分类为例,除了tools本身,还包含qrcode / qr / random / translation / unicode / hex / urlencode / base64 / aes / rsa / sm2 / calculator / color / palette / protobuf / yaml / regex / gcd / lcm / unixtime / timezone / ua / user-agent以及中文"计算器/调色/编码/二维码/随机/翻译"等;vault分类则覆盖git / merge / rebase / push / pull / fetch / conflict / savecopy / quicknote / quick-note / memo / 随手记 / jsonvault / vaultsearch / includecontent等。

搜索实现上,search(query, translate)会同时用Locale.ROOT与Locale.getDefault()两份小写化结果做匹配,命中条件包括:categoryId 前缀包含、翻译后的分类标签包含、以及任意 keyword 包含。matchesSearch是internal函数,因此可被单测直接引用。

5.2 ToolRegistry:工具深链关键词

ToolRegistry.kt 的ToolDefinition(id, groupId, titleKey, keywords, glyph, status, supportsHistory, supportsFavorites)定义了全部工具的导航元数据。DIFF-556 涉及的工具关键词在表中清晰可见:

  • QuickNote:note / memo / markdown / vault / duplicate / 随手记 / 笔记;
  • TextDiff:diff / compare / whitespace / myers / patch / history / 对比;
  • Calculator:calculator / calc / gcd / lcm / permutation / 计算 / 进制;
  • ColorBoard:color / palette / hex / rgb / picker / screen / 调色 / 取色;
  • UaParse:ua / user-agent / browser / chrome / preset / parse / 浏览器;
  • Regex:regex / regular / pattern / test / global / 正则 / 捕获;
  • TimeConvert:time / timestamp / unix / timezone / 时间;
  • Image:image / 图片 / svg / watermark / compress / base64 / clipboard / 截图;
  • Cron:cron / schedule / quartz / parse / timezone / favorite / history / 定时。

ToolRegistry.search在空查询时返回全部工具,非空时用Locale.ROOT小写化匹配 id / 翻译标题 / keywords。两部分结果由 CommandSearchResults.kt 的commandSearchEntries合并为CommandSearchEntry.Tool与CommandSearchEntry.Settings两类条目,供命令盘统一展示。

5.3 测试锁定的行为

CommandSearchCatalogTest.kt 是本文档"验证"节的直接证据,它锁定了大量深链映射,例如:

  • "mcp"→ai;"git"/"rebase"/"mergeflow"/"savecopy"→vault;
  • "quicknote"/"vaultsearch"→vault(随手记深链命中 Vault 设置);
  • "myers"→layout(Myers diff 属于布局/对比设置);
  • "yaml"/"aes"/"calculator"/"qr"/"translate"→tools;
  • "hardware"/"detect"/"coderun"→runtime;
  • "检查更新"(中文)→about;
  • "catalogCoversEverySettingsNavCategory"用例还会遍历SettingsNavCategory.entries,强制保证每个设置分类都被命令盘覆盖。

六、MCP 边界:本条无新增 handler

DIFF-556 特意声明"Electron MCP 仍为 11 项核心工具 + Vault 扩展,本条无新增 MCP handler",并指向对照文档 electron-next-mcp-server-vitest.md。也就是说,本条的 F 工具接线、Vault 底栏、CSS 与命令盘关键词都发生在桌面 UI 与导航层,没有向 MCP 服务端暴露新的工具。这既是范围控制(避免在一条 diff 里混合太多改动面),也是沟通工具与 UI 工具的演进节奏差异——MCP 工具的扩展走的是另一套 catalog 登记流程(参见 DIFF-555 的McpToolCatalogTest登记mootool_protobuf_wire的例子)。

七、验证方式:单测、证据脚本与 Gradle 离线任务

文档"验证"节给出了三类闭环验证手段:

  1. Presentation 单测:*WiringPresentation*、QuickNoteVaultFooterPresentation*、TextDiffPresentation*、CronWiringPresentation*的测试用例。除本文已列出的几个测试文件外,仓库中还有CalculatorWiringPresentationTest、ConfigWiringPresentationTest、ImageWiringPresentationTest、UaWiringPresentationTest、TimeWiringPresentationTest等一一对应(位于desktopTest的domain包)。从测试命名可以确认,DIFF-556 采取的是"每个 Presentation 一个测试类"的约定。
  2. 命令盘测试:CommandSearchCatalogTest,覆盖 yaml / quicknote / myers 等深链场景(上节已详述)。
  3. 证据恢复脚本 + 桌面测试:
    • ./scripts/prepare-tray-screencapture-evidence.sh:恢复证据 PNG(托盘截屏证据链,仓库中的next-compose/scripts/prepare-tray-screencapture-evidence.sh);
    • ./gradlew :composeApp:desktopTest --offline(JDK 21):离线跑 Compose 桌面端全部测试,这是本模块每次 diff 的最终回归门槛。

这些命令适用于next-compose模块的开发环境(需要 JDK 21 与 Gradle 工具链可用),属于项目内既定流程,读者可按需在本地复现。

八、未做事项与切边界

文档最后明确列出了"未做"清单,作为下一条 diff 的输入:

  • 六套 CSS 逐选择器皮肤(即 parity-gap 中"六套 CSS 逐选择器皮肤"的承诺项);
  • 产品主窗全工具 Tab / 系统 IME 手工 PNG(next-compose/docs/diff/系列中多次提及的"产品主窗 Tab/IME/TCC PNG");
  • P7 三平台安装 / 公证(参见 P7 相关文档 的语境);
  • 其余 F 工具的 substantial 引擎 / UI;
  • 整体 parity 目标仍未达成。

这些"未做"项说明 DIFF-556 是增量合规交付:它只承诺了 F 工具守卫接线、Vault 底栏、CSS 组件批次与导航关键词四件事,并把遗留事项显式登记,供后续切片(如 557/558/559 等相邻编号的 diff 文档)继续消化。

结语:一条 diff 的"可测性"设计

回看 DIFF-556 的完整交付物,最值得借鉴的是它的可测试性纪律:所有 UI 使能逻辑都被抽成无 Compose 依赖的object纯函数;引擎调用一律折叠为密封Success/Failure;取消语义(cancelled)在 toast 层面被显式豁免;样式用最小行高修饰符落地并被 capture 测试引用;命令盘关键词中英日三语覆盖且有 40+ 条断言用例。这种"守卫先行、接线后置、测试兜底"的切片式推进方式,正是 MooTool 在多平台(Electron/Compose/Flutter/Tauri)间维持 parity 且每步都可回归的工程基础。若你在自己的多窗口工具类产品中需要类似的分批落地节奏,可以直接复用这套WiringPresentation+ Screen 接线 + 捕获测试的骨架。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

相关推荐

上一篇:打破单机限制:NucleusCoop让你在一台电脑上玩转多人分屏游戏
下一篇:3步技术解析:如何用Nucleus Co-op实现PC游戏分屏多人体验

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询