MooTool Compose 版二维码导出目录迁移:从 Java `qrCodeSaveAsPath` 到统一 `tools.exportDirectory` 的 DIFF-189 实战解析
桌面应用开发工具【免费下载链接】MooToolHandy tool set for developers项目地址https://gitcode.com/gh_mirrors/mo/MooTool点击查看免费下载导读本文围绕 MooTool 下一代 Compose 桌面版中 DIFF-189 文档 记载的一项关键兼容性改造展开把旧 Java 版二维码工具的「上次导出目录」func.qrCode.qrCodeSaveAsPath迁入 Compose 版全局统一的「工具默认导出目录」tools.exportDirectory并让二维码生成页的保存对话框正确复用该目录。读完本文你将掌握 legacyconfig.setting的解析与补丁应用机制、tools.exportDirectory的存储与校验规则、AWTFileDialog在 Compose 桌面端的封装方式以及如何用applyPatch候选链与集成测试验证这一迁移行为。一、改造背景两条导出目录来源的归一1.1 Java 旧版的「记住上次保存目录」在 JavaSwing版 MooTool 中二维码工具维护着一个独立的「上次保存路径」配置项配置键func.qrCode.qrCodeSaveAsPath存储位置Java 版的config.settingINI 风格配置文件位于config/config.setting行为用户在二维码生成 Tab 保存图片后该键会被更新为本次保存文件的父目录下次打开保存对话框时以此目录为初始位置这一行为在 Compose 版对应的实现中同样被继承生成 Tab 的「保存」成功后会将父目录写回导出目录配置详见后文「四、F23 运行时行为」。1.2 Compose 版的全局导出目录Compose 版不再为每个工具各自维护一个导出路径而是引入全局配置项配置键tools.exportDirectory对应AppSettings.tools中的ToolSettings.exportDirectory默认值空字符串表示使用产品默认行为适用范围随手记QuickNote、JSON、图片、Host、二维码等多个工具共用从 Settings.kt 可以看到ToolSettings的定义Serializable data class ToolSettings( val qrCodeSize: Int 300, val qrErrorCorrection: String M, val randomStringLength: Int 16, val exportDirectory: String , val translationProvider: String google, val translationSourceLang: String auto, val translationTargetLang: String zh-CN )这一全局合并的决策源自同目录下的 DIFF-142export-directory-absolute-path.md它明确了两条语义留空表示默认行为非空必须为绝对路径并规定设置页提交时复用VaultPathConfig校验、加载时清除已保存的相对路径。二、迁移机制qrCodeSaveAsPath进入applyPatch导出目录候选链2.1 迁移入口applyPatch旧 Java 配置的导入发生在「设置 → 迁移」流程中。当用户选择旧版 MooTool 源码目录后CrossProductImporter会扫描并解析config.setting随后通过LegacyJavaSettings.applyPatch将可迁移的配置项合并进当前的AppSettings。调用链位于 MigrationSettingsPanel.ktif (LegacyJavaSettings.hasMigratableSettings(scanned.legacyJavaConfig)) { next LegacyJavaSettings.applyPatch(next, scanned.legacyJavaConfig) }2.2 候选链的优先级顺序在 LegacyJavaSettings.kt 中导出目录的合并遵循一条「按工具优先级依次回退」的候选链var tools next.tools ... val exportDirectory value(config, func.quickNote, quickNoteExportPath) .ifEmpty { value(config, func.jsonBeauty, jsonBeautyExportPath) } .ifEmpty { value(config, func.image, imageExportPath) } .ifEmpty { value(config, func.host, hostExportPath) } .ifEmpty { value(config, func.qrCode, qrCodeSaveAsPath) } if (exportDirectory.isNotEmpty() Path.of(exportDirectory).isAbsolute()) { tools tools.copy(exportDirectory exportDirectory) }DIFF-189 的改动正是把qrCodeSaveAsPath追加到这条候选链的末尾作为最后一个兜底来源。候选链的完整顺序为优先级legacy 配置键来源工具1func.quickNote.quickNoteExportPath随手记2func.jsonBeauty.jsonBeautyExportPathJSON3func.image.imageExportPath图片4func.host.hostExportPathHost5func.qrCode.qrCodeSaveAsPath二维码DIFF-189 新增需要注意两个关键条件绝对路径校验候选链只接受绝对路径。if (exportDirectory.isNotEmpty() Path.of(exportDirectory).isAbsolute())—— 相对路径或空值都会被忽略这保证了写入tools.exportDirectory的值总是合法的绝对路径。非空优先链上更靠前的工具只要非空即短路生效二维码路径只在前四项都为空时才会被采用。顺带一提applyPatch同时也迁移了二维码工具的其他 legacy 配置例如qrCodeSize默认 300和qrCodeErrorCorrection映射到 L/M/Q/H见legacyQrCorrection这些与导出目录共同完成了二维码工具设置的整体迁移。2.3 为什么需要绝对路径校验Compose 版的路径语义与 Vault 目录保持一致配置值为空时使用产品默认目录非空时必须是绝对路径。这一校验同时存在于三个层面迁移层上述applyPatch的isAbsolute判断存储层SettingsRepository.sanitizeLoadedSettings在加载设置时调用VaultPathConfig.effectiveCustomRoot清理非法值见 SettingsRepository.kt设置页提交层设置页的导出目录输入框通过DirectorySettingRow提交同样复用路径校验逻辑。VaultPathConfig.kt 的实现提供了统一的判定fun normalizedCustomRoot(value: String): String? { val trimmed value.trim() if (trimmed.isEmpty()) return val path runCatching { Path.of(trimmed).normalize() }.getOrNull() ?: return null if (!path.isAbsolute) return null return path.toString() } fun effectiveCustomRoot(value: String): String normalizedCustomRoot(value) ?: 三、存储与持久化设置加载时的自动净化迁移写入的tools.exportDirectory最终会随AppSettings一起以 JSON 形式持久化。为保证配置始终合法SettingsRepository.load会在加载时对导出目录做归一化处理空值 → 保持空字符串默认行为合法绝对路径 → 原样保留相对路径 / 非法路径 → 清空为对应的测试位于 SettingsVaultPathSanitizeTest.kttools AppSettings.Default.tools.copy(exportDirectory exports/out) ... val loaded SettingsRepository(directories).load() assertEquals(, loaded.tools.exportDirectory)这意味着即使外部手工编辑设置文件写入了相对路径应用加载后也会将其清除从而保证运行期文件对话框不会使用一个无法解析的目录。四、F23 运行时行为保存对话框与目录回写4.1 保存对话框的初始目录迁移完成只是第一步DIFF-189 还规定二维码生成 Tab 在运行时也要遵循统一的导出目录。Compose 版二维码页面在 QrCodeScreen.kt 中生成 PNG 后点击「保存」时MooButton(container.t(common.save), enabled QrWiringPresentation.hasPngOutput(session.pngBytes), p5Toolbar true, onClick { val bytes session.pngBytes ?: returnMooButton val file pickSavePngFile(container, container.t(common.save)) ?: returnMooButton when (val outcome QrWiringPresentation.runWritePngFile(file, bytes)) { QrWiringPresentation.WritePngOutcome.Success - { persistToolsExportDirectory(container, file) session.notice container.t(common.save) ... } ... } })其中pickSavePngFile与选择 Logo、识别图片所用的pickImageFile一样都收敛到统一的 ToolsExportDirectory.kt 中的chooseFileWithExportDirectoryfun chooseFileWithExportDirectory( container: AppContainer, save: Boolean, title: String, defaultFileName: String , ): File? { val exportDir VaultPathConfig.effectiveCustomRoot(container.settings.value.tools.exportDirectory) val dialog FileDialog(null as Frame?, title, if (save) FileDialog.SAVE else FileDialog.LOAD) if (defaultFileName.isNotBlank()) { dialog.file defaultFileName } if (exportDir.isNotBlank()) { val dir File(exportDir) if (dir.isDirectory) { dialog.directory exportDir } else { dir.parentFile?.takeIf { it.isDirectory }?.let { dialog.directory it.absolutePath } } } dialog.isVisible true val directory dialog.directory ?: return null val name dialog.file ?: return null return File(directory, name) }这里有几个值得注意的工程细节初始目录以VaultPathConfig.effectiveCustomRoot(exportDirectory)计算即迁移写入或用户在设置页配置的tools.exportDirectoryAWT FileDialog 封装Compose 桌面版复用java.awt.FileDialog作为系统保存/打开对话框SAVE与LOAD两种模式共用同一入口目录容错若配置的目录已被删除dir.isDirectory为 false会自动回退到其父目录若父目录仍存在避免对话框打开到一个不存在的路径默认文件名保存 PNG 时默认文件名固定为mootool-qrcode.png见 QrCodeScreen.kt且当用户输入的文件名缺少扩展名时会自动补上.png。4.2 保存成功后回写父目录保存成功后调用persistToolsExportDirectory(container, file)将所选文件的父目录回写到tools.exportDirectory见 ToolsExportDirectory.ktfun persistToolsExportDirectory(container: AppContainer, file: File) { val parent when { file.isDirectory - file.absolutePath.takeIf { it.isNotBlank() } else - file.parentFile?.absolutePath?.takeIf { it.isNotBlank() } } ?: return if (!java.nio.file.Path.of(parent).isAbsolute) return if (container.settings.value.tools.exportDirectory parent) return container.updateSettings { current - current.copy(tools current.tools.copy(exportDirectory parent)) } }这一「保存后更新导出目录」的运行时行为正是对齐 Java 旧版「保存成功后更新qrCodeSaveAsPath」的语义 —— 二者都实现了「记住上次导出目录」的用户体验。同时该函数内置了双重防护父目录必须为绝对路径、且与当前值不同时才触发设置更新避免无意义的重复写入。4.3 二维码工具 IO 的统一收口从 DIFF-657qrcode-export-directory-slice.md可以看到二维码工具的 Logo 选择、识别图片选择、PNG 保存三条 IO 路径统一收口到chooseFileWithExportDirectorypersistToolsExportDirectory取代了此前各页面本地的裸FileDialog。因此无论迁移配置、设置页配置还是运行中保存产生的新目录都能在二维码工具的这三类操作中被一致地当作初始目录使用。五、验证方式单元测试与离线测试命令5.1 迁移行为测试DIFF-189 的核心验证用例是LegacyJavaSettingsTest.applyPatchUsesQrSaveAsExportDirectory位于 LegacyJavaSettingsTest.ktTest fun applyPatchUsesQrSaveAsExportDirectory() { val root createTempDirectory(legacy-java-qr-save-) val saveDir root.resolve(qr-out).also { it.toFile().mkdirs() } val config LegacyJavaSettings.parse( [func.qrCode] qrCodeSaveAsPath${saveDir} .trimIndent() ) val patched LegacyJavaSettings.applyPatch(AppSettings.Default, config) assertEquals(saveDir.toString(), patched.tools.exportDirectory) root.toFile().deleteRecursively() }该测试的验证路径完整覆盖了 DIFF-189 的迁移链路用LegacyJavaSettings.parse解析一段仅含[func.qrCode] qrCodeSaveAsPath绝对路径的 INI 片段调用applyPatch(AppSettings.Default, config)断言patched.tools.exportDirectory等于该绝对路径。由于AppSettings.Default中前四个候选键quickNote/json/image/host均为空二维码路径通过候选链兜底生效从而证明了 DIFF-189 的迁移逻辑成立。若传入的是相对路径如qr-out则因不满足isAbsolute条件而不会写入导出目录 —— 这一边界行为也与前面分析的绝对路径校验一致。5.2 运行全部桌面端测试文档同时给出了离线运行测试的命令./gradlew :composeApp:desktopTest --offline该命令在next-compose目录下执行需 JDK 21 工具链会运行desktopTest下的全部测试其中包括上述LegacyJavaSettingsTest以及SettingsVaultPathSanitizeTest、VaultPathConfigTest等路径校验相关用例。若只想验证本改动也可以配合--tests参数精确过滤./gradlew :composeApp:desktopTest --offline --tests *LegacyJavaSettingsTest六、整体链路小结把 DIFF-189 的改造串联起来二维码导出目录在 Compose 版中的完整生命周期是迁移阶段用户从「设置 → 迁移」导入旧 Java 版目录时LegacyJavaSettings.parse解析config.settingapplyPatch将func.qrCode.qrCodeSaveAsPath绝对路径经候选链写入tools.exportDirectory持久化阶段设置以 JSON 保存SettingsRepository.load加载时通过effectiveCustomRoot净化非法相对路径运行阶段二维码生成 Tab 的「保存」通过chooseFileWithExportDirectory以tools.exportDirectory为初始目录打开系统保存对话框保存成功后persistToolsExportDirectory将父目录写回实现「记住上次导出目录」校验阶段applyPatchUsesQrSaveAsExportDirectory等测试用例保障迁移行为不回归。这一改造的意义在于Compose 版把原来散落在各工具随手记、JSON、图片、Host、二维码的独立导出路径配置统一收敛为一个全局tools.exportDirectory既保留了 Java 旧版「记住上次保存位置」的体验又为后续所有工具的导出/导入路径提供了单一、可校验的配置来源。参考资料与延伸阅读DIFF-189 原文本文主体依据LegacyJavaSettings.ktapplyPatch候选链与 legacy 配置解析实现LegacyJavaSettingsTest.kt迁移行为测试ToolsExportDirectory.kt文件对话框统一封装与目录回写QrCodeScreen.kt二维码页面保存/选择 IOVaultPathConfig.kt绝对路径校验与回退语义SettingsRepository.kt设置加载净化SettingsVaultPathSanitizeTest.kt路径净化测试DIFF-142导出目录绝对路径语义的基线决策DIFF-657二维码工具 IO 统一收口的关联切片赞分享桌面应用开发工具【免费下载链接】MooToolHandy tool set for developers项目地址https://gitcode.com/gh_mirrors/mo/MooTool点击查看免费下载相关推荐MooTool Compose 版 DIFF-180Java 版 ConsoleUtil 控制台草稿计算器/二维码迁移实现解析MooTool Compose 版 DIFF 180Java 版 ConsoleUtil 控制台草稿计算器/二维码迁移实现解析 本文聚焦 MooTool桌面应用开发工具MooToolCompose 版遗留 Java 版二维码 Logo 与识别图路径迁入指南DIFF-183MooToolCompose 版遗留 Java 版二维码 Logo 与识别图路径迁入指南DIFF 183 导读 MooTool 的新版桌面客户端Com桌面应用开发工具Learn Harness Engineering 第三讲让代码仓库成为 AI Agent 的唯一事实来源System of RecordLearn Harness Engineering 第三讲让代码仓库成为 AI Agent 的唯一事实来源System of Record 导读 本讲解桌面应用开发工具上一篇Ant Design Blazor TreeSelect 节点可选择控制Selectable 与 SelectableExpression 完整实战指南下一篇如何5分钟完成QQ空间历史说说的完整备份开源工具GetQzonehistory终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考