🖥️ GUI 层架构
架构 GUI 层ClassyShark 的桌面端是一个 Swing 应用,核心是 MVM(Model-View-Mediator)模式:
ClassySharkPanel同时扮演ToolbarController、ViewerController、KeyListener三个角色,把所有子组件串成一张交互网,重活全部丢给SwingWorker以不阻塞 EDT。
组件关系
flowchart TD
CSP[ClassySharkPanel<br/>MVM Mediator] -->|buildUI 北/中| TB[Toolbar 工具栏]
CSP -->|左| JT[JTabbedPane]
CSP -->|右| DA[DisplayArea 显示区]
CSP -->|右·Methods 时| RCP[RingChartPanel 环形图]
CSP -->|聚合 SilverGhost| SG[SilverGhost 引擎]
JT -->|tab 0 Classes| FT[FilesTree 左侧树]
JT -->|tab 1 Methods count| MCP[MethodsCountPanel 方法树]
TB --> RB[RecentArchivesButton 最近归档]
TB -->|typingArea 按键回调| CSP
FT -->|选中类| CSP
MCP -->|选中节点| RCP
DA -->|双击/import 导航| CSP
FT & DA & MCP & RCP -->|拖放| FTH[FileTransferHandler 拖放]
FTH -->|displayArchive| CSPClassySharkPanel 与 SilverGhost 的 M-V 关系:GUI 提问(选中类、按键过滤、导出),引擎回给 List<Translator.ELEMENT> 或类名列表,UI 只负责渲染。
ClassySharkPanel:中介者
ClassySharkPanel 继承 JPanel 并同时实现三个接口:
| 接口 | 扮演 | 典型回调 |
|---|---|---|
ToolbarController | 工具栏动作的目标 | openArchive / onGoBackPressed / onExportButtonPressed / onSettingsButtonPressed / onChangeLeftPaneVisibility |
ViewerController | 显示区/树的导航入口 | onSelectedClassName / onSelectedImportFromMouseClick / onSelectedTypeClassFromMouseClick / onSelectedMethodCount |
KeyListener | 全局打字过滤 | keyPressed 驱动 fillDisplayArea 增量过滤 |
构造函数三个重载(0/2/3 参)对应 入参场景:三参带初始类名、两参仅归档、零参空白启动。类体内直接组合所有子面板:
// buildUI 的骨架 —— BorderLayout:北=Toolbar,中=JSplitPane
toolbar = new Toolbar(this); // 北
jTabbedPane = new JTabbedPane(); // 左:Classes + Methods count 两标签
filesTree = new FilesTree(this); // tab 0
methodsCountPanel = new MethodsCountPanel(this); // tab 1
displayArea = new DisplayArea(this); // 右默认
ringChartPanel = new RingChartPanel(this); // 切到 tab 1 时换到右侧
jSplitPane = new JSplitPane(HORIZONTAL_SPLIT, jTabbedPane, rightScrollPane);
add(toolbar, BorderLayout.NORTH);
add(jSplitPane, BorderLayout.CENTER);JTabbedPane 换页监听的关键副作用:切到 Methods count 时右侧从 rightScrollPane(显示区)换成 ringChartPanel(环形图),并记住 divider 位置。
左区一:FilesTree
FilesTree 是三态树的实现者:
- 按归档类型建树——
.apk/.dex/.aar走 Android 分支(classes+res+libs),其余走 class 分支(按包聚合)。 - 节点用
NodeInfo包邮箱名(去扩展名、去重复段),再配CellRenderer渲染图标与颜色。 - 隐掉 root,单击
.dex/.jar/.apk/.so或叶子节点即回调ViewerController.onSelectedClassName。 - 设置
DragEnabled(true)+FileTransferHandler支持 OS 级拖入归档。
左区二:MethodsCountPanel
MethodsCountPanel 方法计数树:
loadFile启动内部NodeWorker(SwingWorker)后台用RootBuilder.fillClassesWithMethods(file)算好ClassNode树。done()里把ClassNode树映射为DefaultMutableTreeNode树挂进JTree,并回调onSelectedMethodCount。- 选中节点 →
ClassySharkPanel.onSelectedMethodCount→RingChartPanel.setRootNode重绘。
右区:DisplayArea
DisplayArea 是只读 JTextPane 上的"五状态渲染器",内部 DisplayDataState 枚举记录 SHARKEY / CLASSES_LIST / INSIDE_CLASS / ERROR:
| 状态 | 展示内容 | 触发 |
|---|---|---|
SHARKEY | 启动 Doodle 吉祥物 | 空启动 |
CLASSES_LIST | 过滤后的类名列表(小端高亮、上限 50 → BatchDocument) | 打字过滤 |
INSIDE_CLASS | Translator.ELEMENT 流按 TAG 上色 | 选中类 |
ERROR | 错误提示 + Doodle | 归档解析失败 |
语法高亮的关键在 对 ELEMENT.tag → theme.getXxxColor() 的 switch:MODIFIER→KEYWORDS、IDENTIFIER→IDENTIFIERS、ANNOTATION→ANNOTATIONS、SELECTION→SELECTION_BG……这是翻译层到展示层的统一契约。双击导航由 MouseAdapter 实现(行首 import → onSelectedImportFromMouseClick,词 → onSelectedTypeClassFromMouseClick)。
北区:Toolbar 全家
Toolbar 是 JToolBar:图标按钮 + 中央 JTextField 打字区。
| 按钮 | 图标来源 | 动作 |
|---|---|---|
| 左树显隐 | getToggleIcon | onChangeLeftPaneVisibility |
| Open | getOpenIcon | openArchive(JFileChooser + 记住当前目录/最近归档) |
| Back | getBackIcon | onGoBackPressed(回类名列表) |
| Forward | getForwardIcon | onViewTopClassPressed |
| 打字区 | — | 按键直接驱动过滤 |
| Mappings | getMappingIcon | onMappingsButtonPressed(读 ProGuard mapping) |
| Export | getExportIcon | 后台 SwingWorker 导出当前类 + 整包 |
| 最近归档 | getRecentIcon | RecentArchivesButton |
| Settings | getSettingsIcon | onSettingsButtonPressed → SettingsFrame |
RecentArchivesButton 用 JPopupMenu 列出 RecentArchivesConfig.INSTANCE 的归档历史,点击即 displayArchive,另有 "Clear" 清空入口。
拖放:FileTransferHandler
FileTransferHandler 继承 TransferHandler,被 JTree、JTextPane、MethodsCountPanel、RingChartPanel 共享:
canImport只接受javaFileListFlavor(文件列表)。importData逐个校验FileChooserUtils.isSupportedArchiveFile。- 通过则更新
CurrentFolderConfig+ 登记RecentArchivesConfig,最后archiveDisplayer.displayArchive(file)交给中介者开闸。
SwingWorker:EDT 保护
所有重活都在 SwingWorker 的 doInBackground 中跑,UI 更新放 done():
| 场景 | 后台任务 | 完成后 |
|---|---|---|
| 打开归档 | silverGhost.readContents() | 填充 FilesTree、显示 Sharkey/错误 |
| 打字过滤 | silverGhost.filter / translateArchiveElement | displayArea.displayClass/searchResults |
| 导出 | Exporter.writeCurrentClass/writeArchive | 空 done() |
| 读 mapping | silverGhost.readMappingFile | addMappings |
| 方法计数 | RootBuilder.fillClassesWithMethods | 挂树 + 环形图回调 |
📌 这层及时响应性是本体感关键:解析大 APK 时窗口不冻结。
GesturePanel与 Agent GUI 控制 的GuiBridge也挂在ClassySharkPanel上(agentOpenArchive、agentNavigateTo等统一invokeLater入 EDT)。
设计要点
- 🧭 单一中介者 — 一个
ClassySharkPanel包揽三个接口,组件间零直接耦合,全部过中介者的回调。 - 🧵 EDT 纪律 — 读与译永不进 UI 线程,
SwingWorker双层割裂"后台算/前台画"。 - 🔗 契约式的展示 — 翻译层只交
ELEMENT流,展示层按TAG上色,两层面彻底解耦。 - 🧲 全平台拖放 — 一个
TransferHandler被四处复用,开档统一收口到displayArchive。
进一步阅读
- 🧩 ClassySharkPanel · FilesTree · MethodsCountPanel · DisplayArea · RingChartPanel · Toolbar · FileTransferHandler · RecentArchivesButton
- 🖥️ GUI 参考 · GUI 主页 · 🎨 GUI 主题
- 🏗️ 架构总览 · 入口层架构 · Theme 架构