diff --git a/sources/LocalizationEditor/AppDelegate.swift b/sources/LocalizationEditor/AppDelegate.swift index 1346a01..07113d4 100644 --- a/sources/LocalizationEditor/AppDelegate.swift +++ b/sources/LocalizationEditor/AppDelegate.swift @@ -6,29 +6,37 @@ // Copyright © 2018 Igor Kulman. All rights reserved. // +// 导入Cocoa框架 import Cocoa +// AppDelegate类,遵循NSApplicationDelegate协议,是应用程序的委托对象 @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { + // IBOutlet属性,连接到XIB或Storyboard文件中的UI元素 // swiftlint:disable private_outlet @IBOutlet weak var openFolderMenuItem: NSMenuItem! @IBOutlet weak var reloadMenuItem: NSMenuItem! // swiftlint:enable private_outlet + // 私有属性,获取当前编辑器窗口 private var editorWindow: NSWindow? { return NSApp.windows.first(where: { $0.windowController is WindowController }) } + // 应用程序已成功启动后调用 func applicationDidFinishLaunching(_: Notification) {} + // 应用程序将要终止时调用 func applicationWillTerminate(_: Notification) {} + // 应用程序打开未命名文件时调用,返回true表示已成功打开 func applicationOpenUntitledFile(_ sender: NSApplication) -> Bool { showEditorWindow() return true } + // 应用程序打开文件时调用,返回true表示已成功打开 func application(_ sender: NSApplication, openFile filename: String) -> Bool { var isDirectory: ObjCBool = false guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory), @@ -42,6 +50,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { return true } + // 私有方法,显示编辑器窗口 private func showEditorWindow() { guard let editorWindow = editorWindow else { let mainStoryboard = NSStoryboard(name: NSStoryboard.Name("Main"), bundle: nil) diff --git a/sources/LocalizationEditor/Extensions/FileManager+Extension.swift b/sources/LocalizationEditor/Extensions/FileManager+Extension.swift index b9d4d9e..59b1135 100644 --- a/sources/LocalizationEditor/Extensions/FileManager+Extension.swift +++ b/sources/LocalizationEditor/Extensions/FileManager+Extension.swift @@ -8,17 +8,30 @@ import Foundation +// Swift 拓展(Extension)用于在不创建新类型的情况下给已有类型(如 FileManager)添加新功能。 extension FileManager { + // 递归获取 URL 下的所有文件,返回一个 URL 数组。 + // - Parameter url: 需要查找的 URL。 + // - Returns: URL 数组。 func getAllFilesRecursively(url: URL) -> [URL] { + // 获取 enumerator,用于遍历 URL 下的所有条目。 guard let enumerator = FileManager.default.enumerator(atPath: url.path) else { + // 如果 enumerator 创建失败,返回一个空数组。 return [] } + // 使用 enumerator 遍历 URL 下的所有条目,并将符合条件的条目转换为 URL,最终返回一个 URL 数组。 + // - Returns: 条目对应的 URL,或 nil。 return enumerator.compactMap({ element -> URL? in + // 确保条目是字符串类型。 guard let path = element as? String else { + // 如果不是,返回 nil。 return nil } + // 返回条目对应的 URL。 + // - Parameter path: 条目路径。 + // - Parameter isDirectory: 条目是否是目录,false 表示文件。 return url.appendingPathComponent(path, isDirectory: false) }) } diff --git a/sources/LocalizationEditor/Extensions/String+Extensions.swift b/sources/LocalizationEditor/Extensions/String+Extensions.swift index b4ebe32..3253e2f 100644 --- a/sources/LocalizationEditor/Extensions/String+Extensions.swift +++ b/sources/LocalizationEditor/Extensions/String+Extensions.swift @@ -8,30 +8,44 @@ import Foundation +// extension String +// 在 Swift 中,扩展可以为现有类型添加新功能。这个文件中的代码为 String 类型添加了一些额外的功能。 +// 添加了 slice(from:to:) 函数,可以从当前字符串中获取从 fromString 到 toString 之间的子字符串。 extension String { + + // 参数 fromString 和 toString 用于界定子字符串的范围,如果不存在这两个字符串,则返回 nil。 func slice(from fromString: String, to toString: String) -> String? { + + // 获取 fromString 在当前字符串中的范围,并取其 upperBound(即 fromString 的结束位置)。 return (range(of: fromString)?.upperBound).flatMap { substringFrom in + + // 从 substringFrom 的位置开始搜索 toString,找到 toString 后取其开始位置。 (range(of: toString, range: substringFrom.. - + - - + @@ -277,7 +276,7 @@ - + @@ -424,7 +423,7 @@ - + @@ -435,7 +434,7 @@ - + @@ -445,9 +444,6 @@ - + @@ -489,7 +488,7 @@ Gw - + diff --git a/sources/LocalizationEditor/UI/Cells/ActionsCell.swift b/sources/LocalizationEditor/UI/Cells/ActionsCell.swift index d44fc5a..37a49c1 100644 --- a/sources/LocalizationEditor/UI/Cells/ActionsCell.swift +++ b/sources/LocalizationEditor/UI/Cells/ActionsCell.swift @@ -8,34 +8,48 @@ import Cocoa +// 定义 ActionsCellDelegate 协议,用于代理 ActionsCell 的删除操作 protocol ActionsCellDelegate: AnyObject { + // 删除键值对的请求 func userDidRequestRemoval(of key: String) } +// ActionsCell 类,继承自 NSTableCellView,用于显示动作单元格 final class ActionsCell: NSTableCellView { // MARK: - Outlets + // 删除按钮 Outlet @IBOutlet private weak var deleteButton: NSButton! // MARK: - Properties + // 单元格标识符 static let identifier = "ActionsCell" + // 单元格关联的键值 var key: String? + + // ActionsCellDelegate 代理 weak var delegate: ActionsCellDelegate? + // 单元格初始化完成后的操作 override func awakeFromNib() { super.awakeFromNib() + // 设置删除按钮图标和工具提示 deleteButton.image = NSImage(named: NSImage.stopProgressTemplateName) deleteButton.toolTip = "delete".localized } + // 删除按钮点击事件 @IBAction private func removalClicked(_ sender: NSButton) { + // 获取单元格关联的键值 guard let key = key else { return } + // 触发代理方法,删除键值对 delegate?.userDidRequestRemoval(of: key) } } + diff --git a/sources/LocalizationEditor/UI/Cells/KeyCell.swift b/sources/LocalizationEditor/UI/Cells/KeyCell.swift index 4b1a04d..cbadfab 100644 --- a/sources/LocalizationEditor/UI/Cells/KeyCell.swift +++ b/sources/LocalizationEditor/UI/Cells/KeyCell.swift @@ -6,27 +6,34 @@ // Copyright © 2018 Igor Kulman. All rights reserved. // +// 导入Cocoa和Foundation框架 import Cocoa import Foundation +// 定义KeyCell类,继承NSTableCellView类 final class KeyCell: NSTableCellView { - // MARK: - Outlets + // MARK: - Outlets - @IBOutlet private weak var keyLabel: NSTextField! - @IBOutlet private weak var messageLabel: NSTextField! + // 私有弱引用Outlets,连接到 storyboard 中的 NSTextField 视图 + @IBOutlet private weak var keyLabel: NSTextField! + @IBOutlet private weak var messageLabel: NSTextField! - // MARK: - Properties + // MARK: - Properties - static let identifier = "KeyCell" + // 类属性 identifier,用于标识 KeyCell 的唯一标识符 + static let identifier = "KeyCell" - var key: String? { - didSet { - keyLabel.stringValue = key ?? "" - } - } - var message: String? { - didSet { - messageLabel.stringValue = message ?? "" - } - } + // 属性 key,用于存储键值,设置 key 值时,会同步更新 keyLabel 的字符串值 + var key: String? { + didSet { + keyLabel.stringValue = key ?? "" + } + } + + // 属性 message,用于存储消息,设置 message 值时,会同步更新 messageLabel 的字符串值 + var message: String? { + didSet { + messageLabel.stringValue = message ?? "" + } + } } diff --git a/sources/LocalizationEditor/UI/Cells/LocalizationCell.swift b/sources/LocalizationEditor/UI/Cells/LocalizationCell.swift index 4877d1f..0594ee5 100644 --- a/sources/LocalizationEditor/UI/Cells/LocalizationCell.swift +++ b/sources/LocalizationEditor/UI/Cells/LocalizationCell.swift @@ -6,26 +6,40 @@ // Copyright © 2018 Igor Kulman. All rights reserved. // +// 注释:导入Cocoa框架 import Cocoa +// 注释:定义一个协议LocalizationCellDelegate,继承AnyObject,并声明两个必须实现的方法 protocol LocalizationCellDelegate: AnyObject { + // 注释:当文本控件结束编辑时触发 func controlTextDidEndEditing(_ obj: Notification) + + // 注释:当用户更新区域化字符串时触发,参数分别为语言、键、值和可选的消息 func userDidUpdateLocalizationString(language: String, key: String, with value: String, message: String?) } +// 最终类 LocalizationCell 是 NSTableCellView 的子类 final class LocalizationCell: NSTableCellView { + // MARK: - Outlets + // @IBOutlet 属性包装器用于将 Interface Builder 中的视图连接到代码中 + // valueTextField 是 NSTextField 类的实例,它显示本地化字符串的值 @IBOutlet private weak var valueTextField: NSTextField! // MARK: - Properties + // 类属性 identifier 用于标识本地化单元格的唯一标识符 static let identifier = "LocalizationCell" + // delegate 属性是 LocalizationCellDelegate 协议的可选实例,用于处理单元格相关的事件 weak var delegate: LocalizationCellDelegate? + // language 属性是一个可选的字符串,用于存储所选语言 var language: String? + // value 属性是 LocalizationString? 类型,用于存储本地化字符串 + // didSet 属性观察器在 value 属性发生变化时,会调用 setStateUI() 方法更新 UI 界面 var value: LocalizationString? { didSet { valueTextField.stringValue = value?.value ?? "" @@ -34,10 +48,13 @@ final class LocalizationCell: NSTableCellView { } } + // setStateUI() 方法用于更新界面,根据 valueTextField 的字符串是否为空,设置 layer 的边框颜色 private func setStateUI() { valueTextField.layer?.borderColor = valueTextField.stringValue.isEmpty ? NSColor.red.cgColor : NSColor.clear.cgColor } + // awakeFromNib() 方法是 NSTableCellView 的生命周期方法,在加载 NIB 文件时调用 + // 在该方法中,设置 valueTextField 的 wantsLayer 属性为 true,并设置 layer 的边框宽度、圆角半径 override func awakeFromNib() { super.awakeFromNib() @@ -46,9 +63,7 @@ final class LocalizationCell: NSTableCellView { valueTextField.layer?.cornerRadius = 0.0 } - /** - Focues the cell by activating the NSTextField, making sure there is no selection and cursor is moved to the end - */ + // focus() 方法用于激活 NSTextField,并将选中范围设置为空,将光标移动到文档末尾 func focus() { valueTextField?.becomeFirstResponder() valueTextField?.currentEditor()?.selectedRange = NSRange(location: 0, length: 0) @@ -58,14 +73,24 @@ final class LocalizationCell: NSTableCellView { // MARK: - Delegate +// 扩展 LocalizationCell,遵循 NSTextFieldDelegate 协议 extension LocalizationCell: NSTextFieldDelegate { + + // controlTextDidEndEditing(_:) 方法,当文本控件编辑结束时调用 func controlTextDidEndEditing(_ obj: Notification) { + + // 调用代理对象的 controlTextDidEndEditing(_:) 方法 delegate?.controlTextDidEndEditing(obj) + + // 解包 language 和 value guard let language = language, let value = value else { return } - + + // 调用 setStateUI() 方法 setStateUI() + + // 调用代理方法,传递相关参数 delegate?.userDidUpdateLocalizationString(language: language, key: value.key, with: valueTextField.stringValue, message: value.message) } } diff --git a/sources/LocalizationEditor/UI/ViewController.swift b/sources/LocalizationEditor/UI/ViewController.swift index a74981d..304bcd1 100644 --- a/sources/LocalizationEditor/UI/ViewController.swift +++ b/sources/LocalizationEditor/UI/ViewController.swift @@ -6,76 +6,110 @@ // Copyright © 2018 Igor Kulman. All rights reserved. // +// MARK: - 视图控制器协议 (ViewController 协议) import Cocoa - /** -Protocol for announcing changes to the toolbar. Needed because the VC does not have direct access to the toolbar (handled by WindowController) - */ +// 此协议用于通知工具栏的更改。因为视图控制器本身没有直接访问工具栏(由 WindowController 处理) +*/ protocol ViewControllerDelegate: AnyObject { /** - Invoked when localization groups should be set in the toolbar's dropdown list + // 当本地化组应设置在工具栏的下拉列表中时调用 */ func shouldSetLocalizationGroups(groups: [LocalizationGroup]) /** - Invoiked when search and filter should be reset in the toolbar + // 当在工具栏中重置搜索和筛选器时调用 */ func shouldResetSearchTermAndFilter() /** - Invoked when localization group should be selected in the toolbar's dropdown list + // 当在工具栏的下拉列表中选择本地化组时调用 */ func shouldSelectLocalizationGroup(title: String) } final class ViewController: NSViewController { + // MARK: - 固定列(Fixed Column) enum FixedColumn: String { - case key - case actions + // 列的类型,可以是 key 或 actions + case key // 表示该列为 key + case actions // 表示该列为 actions } - // MARK: - Outlets + // MARK: - 控件 Outlet 连接 + // 表格视图 Outlet,用于在界面上显示数据 @IBOutlet private weak var tableView: NSTableView! + + // 进度条 Outlet,用于显示当前操作的进度 @IBOutlet private weak var progressIndicator: NSProgressIndicator! // MARK: - Properties + //Delegate 变量,用于Delegate设计模式,可选类型 weak var delegate: ViewControllerDelegate? + //当前的过滤器,默认为 .all private var currentFilter: Filter = .all + + //当前的搜索词,默认为 "" private var currentSearchTerm: String = "" + + //数据源,负责提供数据 private let dataSource = LocalizationsDataSource() + + //是否已经显示过AddViewController,防止重复显示 private var presendedAddViewController: AddViewController? + + //当前打开的文件夹URL private var currentOpenFolderUrl: URL? override func viewDidLoad() { super.viewDidLoad() + //在这里进行数据的初始化设置 setupData() } // MARK: - Setup + // setupData() 函数用于设置表格视图 (tableView) 所需的数据 private func setupData() { + + // 1. 注册表格视图单元格标识符 (cellIdentifier) + // 在本例中,我们注册了三种单元格的标识符:KeyCell、LocalizationCell 和 ActionsCell let cellIdentifiers = [KeyCell.identifier, LocalizationCell.identifier, ActionsCell.identifier] cellIdentifiers.forEach { identifier in + // 加载 Nib 文件并注册标识符 + // (注意:NSNib.init(nibNamed:bundle:) 方法将在 Nib 文件中查找相应的标识符) let cell = NSNib(nibNamed: identifier, bundle: nil) tableView.register(cell, forIdentifier: NSUserInterfaceItemIdentifier(rawValue: identifier)) } - + + // 2. 为表格视图设置代理 (delegate) 和数据源 (dataSource) + // (注意:delegate 和 dataSource 用于处理表格视图的交互和数据展示) tableView.delegate = self tableView.dataSource = dataSource + + // 3. 启用列宽度调整 tableView.allowsColumnResizing = true + + // 4. 启用自动行高 tableView.usesAutomaticRowHeights = true - + + // 5. 设置选择高亮样式 + // (注意:.none 表示不对选中的行进行高亮显示) tableView.selectionHighlightStyle = .none } private func reloadData(with languages: [String], title: String?) { + // 重置搜索条件和筛选器 delegate?.shouldResetSearchTermAndFilter() + // 获取应用程序的名称 let appName = Bundle.main.infoDictionary![kCFBundleNameKey as String] as! String + + // 设置窗口标题和子标题(根据 macOS 版本) if #available(macOS 11, *) { view.window?.title = appName view.window?.subtitle = title ?? "" @@ -83,18 +117,21 @@ final class ViewController: NSViewController { view.window?.title = title.flatMap({ "\(appName) [\($0)]" }) ?? appName } + // 获取当前表格列,并遍历移除所有列 let columns = tableView.tableColumns columns.forEach { self.tableView.removeTableColumn($0) } - // not sure why this is needed but without it autolayout crashes and the whole tableview breaks visually + // 重新加载数据,避免布局崩溃 tableView.reloadData() + // 添加 "key" 列 let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(FixedColumn.key.rawValue)) column.title = "key".localized tableView.addTableColumn(column) + // 遍历语言数组,添加相应的列 languages.forEach { language in let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(language)) column.title = Flag(languageCode: language).emoji @@ -103,58 +140,87 @@ final class ViewController: NSViewController { self.tableView.addTableColumn(column) } + // 添加 "actions" 列 let actionsColumn = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(FixedColumn.actions.rawValue)) actionsColumn.title = "actions".localized actionsColumn.maxWidth = 48 actionsColumn.minWidth = 32 tableView.addTableColumn(actionsColumn) + // 重新加载数据 tableView.reloadData() - // Also resize the columns: + // 调整所有列的大小 tableView.sizeToFit() - // Needed to properly size the actions column + // 适当调整 actions 列的大小 DispatchQueue.main.async { self.tableView.sizeToFit() self.tableView.layout() } } + // MARK: - 私有函数(Private Functions) + + /// 筛选函数,根据 currentFilter 和 currentSearchTerm 进行数据过滤 + /// - Note: 重新加载 tableView 数据 private func filter() { dataSource.filter(by: currentFilter, searchString: currentSearchTerm) tableView.reloadData() } + /// 处理打开文件夹函数 + /// - Parameter url: URL 类型的文件夹路径 private func handleOpenFolder(_ url: URL) { + // 开始进度条动画 self.progressIndicator.startAnimation(self) + + // 加载数据,并在加载完成后执行闭包中的代码 self.dataSource.load(folder: url) { [unowned self] languages, title, localizationFiles in + // 设置当前已打开的文件夹 URL self.currentOpenFolderUrl = url + + // 重新加载数据,传入 languages 和 title self.reloadData(with: languages, title: title) + + // 停止进度条动画 self.progressIndicator.stopAnimation(self) + // 如果 title 存在,则执行以下操作 if let title = title { + // 委托方设置 LocalizationGroups self.delegate?.shouldSetLocalizationGroups(groups: localizationFiles) + + // 委托方选择 LocalizationGroup self.delegate?.shouldSelectLocalizationGroup(title: title) } } } + // MARK: - 打开文件夹 + + /// 打开文件夹,可以是指定路径或通过对话框选择 private func openFolder(forPath path: String? = nil) { + // 如果已提供路径,直接处理并返回 if let path = path { handleOpenFolder(URL(fileURLWithPath: path)) return } + // 创建并配置 NSOpenPanel 对话框 let openPanel = NSOpenPanel() - openPanel.allowsMultipleSelection = false - openPanel.canChooseDirectories = true - openPanel.canCreateDirectories = true - openPanel.canChooseFiles = false + openPanel.allowsMultipleSelection = false // 只允许选择一个目录 + openPanel.canChooseDirectories = true // 允许选择目录 + openPanel.canCreateDirectories = true // 允许创建目录 + openPanel.canChooseFiles = false // 不允许选择文件 + + // 显示对话框并处理用户选择 openPanel.begin { result -> Void in + // 如果点击了“取消”或未选择目录,直接返回 guard result.rawValue == NSApplication.ModalResponse.OK.rawValue, let url = openPanel.url else { return } + // 选择了目录,处理打开目录 self.handleOpenFolder(url) } } @@ -163,23 +229,29 @@ final class ViewController: NSViewController { // MARK: - NSTableViewDelegate extension ViewController: NSTableViewDelegate { + // 该方法用于自定义 NSTableView 中的每个单元格 func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { + // 获取当前单元格的标识符 guard let identifier = tableColumn?.identifier else { return nil } + // 根据不同的标识符,返回不同的单元格 switch identifier.rawValue { case FixedColumn.key.rawValue: + // 创建 KeyCell 单元格,并设置相应的属性 let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: KeyCell.identifier), owner: self)! as! KeyCell cell.key = dataSource.getKey(row: row) cell.message = dataSource.getMessage(row: row) return cell case FixedColumn.actions.rawValue: + // 创建 ActionsCell 单元格,并设置相应的属性 let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: ActionsCell.identifier), owner: self)! as! ActionsCell cell.delegate = self cell.key = dataSource.getKey(row: row) return cell default: + // 创建 LocalizationCell 单元格,并设置相应的属性 let language = identifier.rawValue let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: LocalizationCell.identifier), owner: self)! as! LocalizationCell cell.delegate = self @@ -193,23 +265,29 @@ extension ViewController: NSTableViewDelegate { // MARK: - LocalizationCellDelegate extension ViewController: LocalizationCellDelegate { + // 当用户更新了本地化字符串时,该方法会被调用 func userDidUpdateLocalizationString(language: String, key: String, with value: String, message: String?) { dataSource.updateLocalization(language: language, key: key, with: value, message: message) } + // 当文本编辑结束时,该方法会被调用 func controlTextDidEndEditing(_ obj: Notification) { + // 获取当前被编辑的视图、文本移动方向等信息 guard let view = obj.object as? NSView, let textMovementInt = obj.userInfo?["NSTextMovement"] as? Int, let textMovement = NSTextMovement(rawValue: textMovementInt) else { return } + // 获取当前被编辑单元格的列和行索引 let columnIndex = tableView.column(for: view) let rowIndex = tableView.row(for: view) - let newRowIndex: Int - let newColumnIndex: Int + // 计算新的列和行索引 + var newRowIndex = rowIndex + var newColumnIndex = columnIndex switch textMovement { case .tab: + // 按 Tab 键时,切换到下一个单元格 if columnIndex + 1 >= tableView.numberOfColumns - 1 { newRowIndex = rowIndex + 1 newColumnIndex = 1 @@ -218,9 +296,11 @@ extension ViewController: LocalizationCellDelegate { newRowIndex = rowIndex } if newRowIndex >= tableView.numberOfRows { + // 如果新的行索引超出了表格的范围,则不做任何操作 return } case .backtab: + // 按 Shift + Tab 键时,切换到上一个单元格 if columnIndex - 1 <= 0 { newRowIndex = rowIndex - 1 newColumnIndex = tableView.numberOfColumns - 2 @@ -229,13 +309,16 @@ extension ViewController: LocalizationCellDelegate { newRowIndex = rowIndex } if newRowIndex < 0 { + // 如果新的行索引小于 0,则不做任何操作 return } default: + // 如果文本没有编辑结束,则不做任何操作 return } DispatchQueue.main.async { [weak self] in + // 切换到新的单元格 self?.tableView.editColumn(newColumnIndex, row: newRowIndex, with: nil, select: true) } } @@ -244,10 +327,12 @@ extension ViewController: LocalizationCellDelegate { // MARK: - ActionsCellDelegate extension ViewController: ActionsCellDelegate { + // 用户请求删除指定键的本地化字符串时,会调用该方法 func userDidRequestRemoval(of key: String) { + // 删除指定键的本地化字符串 dataSource.deleteLocalization(key: key) - // reload keeping scroll position + // 刷新表格视图,同时保留滚动条的位置 let rect = tableView.visibleRect filter() tableView.scrollToVisible(rect) @@ -256,10 +341,13 @@ extension ViewController: ActionsCellDelegate { // MARK: - WindowControllerToolbarDelegate +// 扩展 ViewController,遵循 WindowControllerToolbarDelegate 协议 extension ViewController: WindowControllerToolbarDelegate { + /** - Invoked when user requests adding a new translation + * 当用户请求添加新的翻译时调用 */ + // 用户点击工具栏的 "+" 按钮时,会调用该方法,弹出 AddViewController 视图控制器,用于添加新的翻译 func userDidRequestAddNewTranslation() { let addViewController = storyboard!.instantiateController(withIdentifier: "Add") as! AddViewController addViewController.delegate = self @@ -268,10 +356,11 @@ extension ViewController: WindowControllerToolbarDelegate { } /** - Invoked when user requests filter change - - - Parameter filter: new filter setting + * 当用户请求过滤器更改时调用 + * + * - Parameter filter: 新的过滤器设置 */ + // 用户在界面上更改了某些筛选条件,会调用该方法,进行相应的筛选 func userDidRequestFilterChange(filter: Filter) { guard currentFilter != filter else { return @@ -282,10 +371,11 @@ extension ViewController: WindowControllerToolbarDelegate { } /** - Invoked when user requests searching - - - Parameter searchTerm: new search term + * 当用户请求搜索时调用 + * + * - Parameter searchTerm: 新的搜索词 */ + // 用户在界面上输入搜索关键字,会调用该方法,进行相应的搜索 func userDidRequestSearch(searchTerm: String) { guard currentSearchTerm != searchTerm else { return @@ -296,32 +386,36 @@ extension ViewController: WindowControllerToolbarDelegate { } /** - Invoked when user request change of the selected localization group - - - Parameter group: new localization group title + * 当用户请求更改所选的本地化组时调用 + * + * - Parameter group: 新的本地化组标题 */ + // 用户在界面上选择不同的本地化组,会调用该方法,重新加载数据 func userDidRequestLocalizationGroupChange(group: String) { let languages = dataSource.selectGroupAndGetLanguages(for: group) reloadData(with: languages, title: group) } /** - Invoked when user requests opening a folder + * 当用户请求打开文件夹时调用 */ + // 用户点击工具栏的 "Open" 按钮,会调用该方法,打开一个文件选择器,用于选择要打开的文件夹 func userDidRequestFolderOpen() { openFolder() } /** - Invoked when user requests opening a folder for specific path + * 当用户请求打开特定路径的文件夹时调用 */ + // 用户在界面上输入特定的文件夹路径,会调用该方法,打开指定的文件夹 func userDidRequestFolderOpen(withPath path: String) { openFolder(forPath: path) } /** - Invoked when user requests reload selected folder + * 当用户请求重新加载所选文件夹时调用 */ + // 用户点击工具栏的 "Reload" 按钮,会调用该方法,重新加载所选的文件夹 func userDidRequestReloadData() { guard let currentOpenFolderUrl = currentOpenFolderUrl else { return @@ -334,16 +428,24 @@ extension ViewController: WindowControllerToolbarDelegate { // MARK: - AddViewControllerDelegate extension ViewController: AddViewControllerDelegate { + // 用户点击取消时,会调用该方法 func userDidCancel() { + // 关闭当前的 AddViewController dismiss() } + // 用户点击添加翻译时,会调用该方法 func userDidAddTranslation(key: String, message: String?) { + // 关闭当前的 AddViewController dismiss() + // 添加新的本地化字符串 dataSource.addLocalizationKey(key: key, message: message) + + // 刷新表格视图 filter() + // 如果新添加的本地化字符串的键存在,则滚动到该行 if let row = dataSource.getRowForKey(key: key) { DispatchQueue.main.async { self.tableView.scrollRowToVisible(row) @@ -351,11 +453,15 @@ extension ViewController: AddViewControllerDelegate { } } + // 关闭当前的 AddViewController private func dismiss() { + // 获取当前的 AddViewController guard let presendedAddViewController = presendedAddViewController else { + // 如果当前没有 AddViewController,则不做任何操作 return } + // 关闭当前的 AddViewController dismiss(presendedAddViewController) } } diff --git a/sources/LocalizationEditor/UI/WindowController.swift b/sources/LocalizationEditor/UI/WindowController.swift index d451e13..a186c65 100644 --- a/sources/LocalizationEditor/UI/WindowController.swift +++ b/sources/LocalizationEditor/UI/WindowController.swift @@ -8,69 +8,89 @@ import Cocoa +// MARK: - 协议定义 - 窗口控制器代理协议 + /** -Protocol for announcing user interaction with the toolbar + 协议:WindowControllerToolbarDelegate + 用途:宣布用户与工具栏的交互 */ protocol WindowControllerToolbarDelegate: AnyObject { + /** - Invoked when user requests opening a folder + 方法:userDidRequestFolderOpen() + 用途:用户请求打开文件夹时调用 */ func userDidRequestFolderOpen() /** - Invoked when user requests opening a folder for a specific path + 方法:userDidRequestFolderOpen(withPath:) + 用途:用户请求打开特定路径的文件夹时调用 + - Parameter withPath: 文件夹路径 */ func userDidRequestFolderOpen(withPath: String) /** - Invoked when user requests filter change - - - Parameter filter: new filter setting + 方法:userDidRequestFilterChange(filter:) + 用途:用户请求筛选器更改时调用 + - Parameter filter: 新筛选器设置 */ func userDidRequestFilterChange(filter: Filter) /** - Invoked when user requests searching - - - Parameter searchTerm: new search term + 方法:userDidRequestSearch(searchTerm:) + 用途:用户请求搜索时调用 + - Parameter searchTerm: 新搜索词 */ func userDidRequestSearch(searchTerm: String) /** - Invoked when user requests change of the selected localization group - - - Parameter group: new localization group title + 方法:userDidRequestLocalizationGroupChange(group:) + 用途:用户请求本地化组更改时调用 + - Parameter group: 新本地化组标题 */ func userDidRequestLocalizationGroupChange(group: String) /** - Invoked when user requests adding a new translation + 方法:userDidRequestAddNewTranslation() + 用途:用户请求添加新翻译时调用 */ func userDidRequestAddNewTranslation() /** - Invoked when user requests reload selected folder + 方法:userDidRequestReloadData() + 用途:用户请求重新加载所选文件夹时调用 */ func userDidRequestReloadData() } +// MARK: - 类定义 final class WindowController: NSWindowController { - // MARK: - Outlets + // MARK: - IBOutlet 属性 + // 打开按钮 @IBOutlet private weak var openButton: NSToolbarItem! + + // 搜索文本框 @IBOutlet private weak var searchField: NSSearchField! + + // 选择按钮 @IBOutlet private weak var selectButton: NSPopUpButton! + + // 筛选按钮 @IBOutlet private weak var filterButton: NSPopUpButton! + + // 新建按钮 @IBOutlet private weak var newButton: NSToolbarItem! // MARK: - Properties weak var delegate: WindowControllerToolbarDelegate? + // MARK: - 系统回调方法 override func windowDidLoad() { super.windowDidLoad() - + // 在window加载完成后进行UI和数据的设置 setupUI() setupSearch() setupFilter() @@ -78,33 +98,43 @@ final class WindowController: NSWindowController { setupDelegates() } - // MAKR: - Interfaces - + // MARK: - 动作方法 + + // 打开文件夹 func openFolder(withPath path: String) { delegate?.userDidRequestFolderOpen(withPath: path) } - // MARK: - Setup + // MARK: - 私有方法 + // 设置UI private func setupUI() { + // 设置openButton的图片和toolTip openButton.image = NSImage(named: NSImage.folderName) openButton.toolTip = "open_folder".localized + // 设置其他控件的toolTip searchField.toolTip = "search".localized filterButton.toolTip = "filter".localized selectButton.toolTip = "string_table".localized newButton.toolTip = "new_translation".localized } + // 设置搜索栏 private func setupSearch() { + // 设置代理和初始值 searchField.delegate = self searchField.stringValue = "" + // 让搜索栏失去焦点 _ = searchField.resignFirstResponder() } + // 设置筛选器 private func setupFilter() { + // 清空menu filterButton.menu?.removeAllItems() + // 遍历Filter枚举,添加menuItem for option in Filter.allCases { let item = NSMenuItem(title: "\(option.description)".capitalizedFirstLetter, action: #selector(WindowController.filterAction(sender:)), keyEquivalent: "") item.tag = option.rawValue @@ -112,39 +142,51 @@ final class WindowController: NSWindowController { } } + // 设置Menu private func setupMenu() { + // 获取AppDelegate,设置openFolderMenuItem和reloadMenuItem的action let appDelegate = NSApplication.shared.delegate as! AppDelegate appDelegate.openFolderMenuItem.action = #selector(WindowController.openFolderAction(_:)) appDelegate.reloadMenuItem.action = #selector(WindowController.reloadDataAction(_:)) } + // 启用控件 private func enableControls() { + // 启用所有控件 searchField.isEnabled = true filterButton.isEnabled = true selectButton.isEnabled = true newButton.isEnabled = true } + // 设置代理 private func setupDelegates() { + // 获取window的contentViewController,转换为ViewController,设置delegate guard let mainViewController = window?.contentViewController as? ViewController else { fatalError("Broken window hierarchy") } - // informing the window about toolbar appearence + // 设置ViewController作为toolbar的delegate mainViewController.delegate = self - // informing the VC about user interacting with the toolbar + // 设置ViewController的delegate self.delegate = mainViewController } - // MARK: - Actions + // MARK: - 动作 + + // selectAction 方法的功能是:根据用户点击的按钮名称,触发本地化组切换事件。 + // sender: NSMenuItem 参数是被点击的按钮对象,从按钮对象中获取按钮名称 groupName。 + // groupName 传递给代理对象,通过代理方法 userDidRequestLocalizationGroupChange,完成本地化组切换。 @objc private func selectAction(sender: NSMenuItem) { let groupName = sender.title delegate?.userDidRequestLocalizationGroupChange(group: groupName) } + // 筛选器选择时被调用 @objc private func filterAction(sender: NSMenuItem) { + // 根据tag获取Filter,调用代理方法 guard let filter = Filter(rawValue: sender.tag) else { return } @@ -152,23 +194,30 @@ final class WindowController: NSWindowController { delegate?.userDidRequestFilterChange(filter: filter) } + // 打开文件夹被调用 @IBAction private func openFolder(_ sender: Any) { delegate?.userDidRequestFolderOpen() } + // 新建翻译被调用 @IBAction private func addAction(_ sender: Any) { + // 如果新建按钮被禁用,则直接返回 guard newButton.isEnabled else { return } + // 调用代理方法 delegate?.userDidRequestAddNewTranslation() } + // 打开文件夹菜单被调用 @objc private func openFolderAction(_ sender: NSMenuItem) { delegate?.userDidRequestFolderOpen() } + // 刷新数据被调用 @objc private func reloadDataAction(_ sender: NSMenuItem) { + // 调用代理方法 delegate?.userDidRequestReloadData() } }