vChewing-macOS/Source/Modules/WindowControllers/CtlPrefWindow.swift

537 lines
19 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import BookmarkManager
import IMKUtils
import Shared
private let kWindowTitleHeight: Double = 78
private extension NSToolbarItem.Identifier {
static let ofGeneral = NSToolbarItem.Identifier(rawValue: "tabGeneral")
static let ofExperience = NSToolbarItem.Identifier(rawValue: "tabExperience")
static let ofDictionary = NSToolbarItem.Identifier(rawValue: "tabDictionary")
static let ofPhrases = NSToolbarItem.Identifier(rawValue: "tabPhrases")
static let ofCassette = NSToolbarItem.Identifier(rawValue: "tabCassette")
static let ofKeyboard = NSToolbarItem.Identifier(rawValue: "tabKeyboard")
static let ofDevZone = NSToolbarItem.Identifier(rawValue: "tabDevZone")
}
// Note: The "InputMethodServerPreferencesWindowControllerClass" in Info.plist
// only works with macOS System Preference pane (like macOS built-in input methods).
// It should be set as "Preferences" which correspondes to the "Preference" pref pane
// of this build target.
class CtlPrefWindow: NSWindowController, NSWindowDelegate {
@IBOutlet var uiLanguageButton: NSPopUpButton!
@IBOutlet var basicKeyboardLayoutButton: NSPopUpButton!
@IBOutlet var selectionKeyComboBox: NSComboBox!
@IBOutlet var chkTrad2KangXi: NSButton!
@IBOutlet var chkTrad2JISShinjitai: NSButton!
@IBOutlet var lblCurrentlySpecifiedUserDataFolder: NSTextFieldCell!
@IBOutlet var tglControlDevZoneIMKCandidate: NSButton!
@IBOutlet var cmbCandidateFontSize: NSPopUpButton!
@IBOutlet var chkFartSuppressor: NSButton!
@IBOutlet var cmbPEInputModeMenu: NSPopUpButton!
@IBOutlet var cmbPEDataTypeMenu: NSPopUpButton!
@IBOutlet var btnPEReload: NSButton!
@IBOutlet var btnPEConsolidate: NSButton!
@IBOutlet var btnPESave: NSButton!
@IBOutlet var btnPEAdd: NSButton!
@IBOutlet var btnPEOpenExternally: NSButton!
@IBOutlet var tfdPETextEditor: NSTextView!
@IBOutlet var txtPECommentField: NSTextField!
@IBOutlet var txtPEField1: NSTextField!
@IBOutlet var txtPEField2: NSTextField!
@IBOutlet var txtPEField3: NSTextField!
var isLoading = false {
didSet { setPEUIControlAvailability() }
}
@IBOutlet var vwrGeneral: NSView!
@IBOutlet var vwrExperience: NSView!
@IBOutlet var vwrDictionary: NSView!
@IBOutlet var vwrPhrases: NSView!
@IBOutlet var vwrCassette: NSView!
@IBOutlet var vwrKeyboard: NSView!
@IBOutlet var vwrDevZone: NSView!
public static var shared: CtlPrefWindow?
static func show() {
let resetPhraseEditor: Bool = shared?.window == nil || !(shared?.window?.isVisible ?? false) || shared == nil
if shared == nil { shared = CtlPrefWindow(windowNibName: "frmPrefWindow") }
guard let shared = shared, let sharedWindow = shared.window else { return }
sharedWindow.delegate = shared
sharedWindow.setPosition(vertical: .top, horizontal: .right, padding: 20)
sharedWindow.orderFrontRegardless() //
sharedWindow.level = .statusBar
sharedWindow.titlebarAppearsTransparent = true
shared.showWindow(shared)
if resetPhraseEditor { shared.initPhraseEditor() }
NSApp.activate(ignoringOtherApps: true)
}
private var currentLanguageSelectItem: NSMenuItem?
override func windowDidLoad() {
super.windowDidLoad()
window?.setPosition(vertical: .top, horizontal: .right, padding: 20)
chkFartSuppressor.isHidden = !Date.isTodayTheDate(from: 0401)
chkFartSuppressor.isEnabled = !chkFartSuppressor.isHidden
cmbCandidateFontSize.isEnabled = true
if #unavailable(macOS 10.14) {
if PrefMgr.shared.useIMKCandidateWindow {
cmbCandidateFontSize.isEnabled = false
}
}
var preferencesTitleName = NSLocalizedString("vChewing Preferences…", comment: "")
preferencesTitleName.removeLast()
let toolbar = NSToolbar(identifier: "preference toolbar")
toolbar.allowsUserCustomization = false
toolbar.autosavesConfiguration = false
toolbar.sizeMode = .default
toolbar.delegate = self
toolbar.selectedItemIdentifier = .ofGeneral
toolbar.showsBaselineSeparator = true
window?.titlebarAppearsTransparent = false
if #available(macOS 11.0, *) {
window?.toolbarStyle = .preference
}
window?.toolbar = toolbar
window?.title = preferencesTitleName
use(view: vwrGeneral)
lblCurrentlySpecifiedUserDataFolder.placeholderString = LMMgr.dataFolderPath(
isDefaultFolder: true)
// Credit: Hiraku Wang (for the implementation of the UI language select support in Cocoa PrefWindow.
// Note: The SwiftUI PrefWindow has the same feature implemented by Shiki Suen.
do {
let languages = ["auto", "en", "zh-Hans", "zh-Hant", "ja"]
var autoMUISelectItem: NSMenuItem?
var chosenLanguageItem: NSMenuItem?
uiLanguageButton.menu?.removeAllItems()
let appleLanguages = PrefMgr.shared.appleLanguages
for language in languages {
let menuItem = NSMenuItem()
menuItem.title = NSLocalizedString(language, comment: language)
menuItem.representedObject = language
if language == "auto" {
autoMUISelectItem = menuItem
}
if !appleLanguages.isEmpty {
if appleLanguages[0] == language {
chosenLanguageItem = menuItem
}
}
uiLanguageButton.menu?.addItem(menuItem)
}
currentLanguageSelectItem = chosenLanguageItem ?? autoMUISelectItem
uiLanguageButton.select(currentLanguageSelectItem)
}
var usKeyboardLayoutItem: NSMenuItem?
var chosenBaseKeyboardLayoutItem: NSMenuItem?
basicKeyboardLayoutButton.menu?.removeAllItems()
let basicKeyboardLayoutID = PrefMgr.shared.basicKeyboardLayout
for source in IMKHelper.allowedBasicLayoutsAsTISInputSources {
guard let source = source else {
basicKeyboardLayoutButton.menu?.addItem(NSMenuItem.separator())
continue
}
let menuItem = NSMenuItem()
menuItem.title = source.vChewingLocalizedName
menuItem.representedObject = source.identifier
if source.identifier == "com.apple.keylayout.US" { usKeyboardLayoutItem = menuItem }
if basicKeyboardLayoutID == source.identifier { chosenBaseKeyboardLayoutItem = menuItem }
basicKeyboardLayoutButton.menu?.addItem(menuItem)
}
basicKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem)
selectionKeyComboBox.usesDataSource = false
selectionKeyComboBox.removeAllItems()
selectionKeyComboBox.addItems(withObjectValues: CandidateKey.suggestions)
var candidateSelectionKeys = PrefMgr.shared.candidateKeys
if candidateSelectionKeys.isEmpty {
candidateSelectionKeys = CandidateKey.defaultKeys
}
selectionKeyComboBox.stringValue = candidateSelectionKeys
if PrefMgr.shared.useIMKCandidateWindow {
selectionKeyComboBox.isEnabled = false // IMKCandidates
}
initPhraseEditor()
}
func windowWillClose(_: Notification) {
tfdPETextEditor.string = ""
}
// CNS
//
@IBAction func toggleCNSSupport(_: Any) {
LMMgr.setCNSEnabled(PrefMgr.shared.cns11643Enabled)
}
@IBAction func toggleSymbolInputEnabled(_: Any) {
LMMgr.setSymbolEnabled(PrefMgr.shared.symbolInputEnabled)
}
@IBAction func toggleTrad2KangXiAction(_: Any) {
if chkTrad2KangXi.state == .on, chkTrad2JISShinjitai.state == .on {
PrefMgr.shared.shiftJISShinjitaiOutputEnabled.toggle()
}
}
@IBAction func toggleTrad2JISShinjitaiAction(_: Any) {
if chkTrad2KangXi.state == .on, chkTrad2JISShinjitai.state == .on {
PrefMgr.shared.chineseConversionEnabled.toggle()
}
}
@IBAction func updateBasicKeyboardLayoutAction(_: Any) {
if let sourceID = basicKeyboardLayoutButton.selectedItem?.representedObject as? String {
PrefMgr.shared.basicKeyboardLayout = sourceID
}
}
@IBAction func updateUiLanguageAction(_: Any) {
if let selectItem = uiLanguageButton.selectedItem {
if currentLanguageSelectItem == selectItem {
return
}
}
if let language = uiLanguageButton.selectedItem?.representedObject as? String {
if language != "auto" {
PrefMgr.shared.appleLanguages = [language]
} else {
UserDefaults.standard.removeObject(forKey: "AppleLanguages")
}
NSLog("vChewing App self-terminated due to UI language change.")
NSApp.terminate(nil)
}
}
@IBAction func updateIMKCandidateEnableStatusAction(_: Any) {
NSLog("vChewing App self-terminated due to enabling / disabling IMK candidate window.")
NSApp.terminate(nil)
}
@IBAction func clickedWhetherIMEShouldNotFartToggleAction(_: Any) {
let content = String(
format: NSLocalizedString(
"You are about to uncheck this fart suppressor. You are responsible for all consequences lead by letting people nearby hear the fart sound come from your computer. We strongly advise against unchecking this in any public circumstance that prohibits NSFW netas.",
comment: ""
))
let alert = NSAlert(error: NSLocalizedString("Warning", comment: ""))
alert.informativeText = content
alert.addButton(withTitle: NSLocalizedString("Uncheck", comment: ""))
if #available(macOS 11, *) {
alert.buttons.forEach { button in
button.hasDestructiveAction = true
}
}
alert.addButton(withTitle: NSLocalizedString("Leave it checked", comment: ""))
if let window = window, !PrefMgr.shared.shouldNotFartInLieuOfBeep {
PrefMgr.shared.shouldNotFartInLieuOfBeep = true
alert.beginSheetModal(for: window) { result in
switch result {
case .alertFirstButtonReturn:
PrefMgr.shared.shouldNotFartInLieuOfBeep = false
case .alertSecondButtonReturn:
PrefMgr.shared.shouldNotFartInLieuOfBeep = true
default: break
}
IMEApp.buzz()
}
return
}
IMEApp.buzz()
}
@IBAction func changeSelectionKeyAction(_ sender: Any) {
guard
let keys = (sender as AnyObject).stringValue?.trimmingCharacters(
in: .whitespacesAndNewlines
)
.deduplicated
else {
selectionKeyComboBox.stringValue = PrefMgr.shared.candidateKeys
return
}
guard let errorResult = CandidateKey.validate(keys: keys) else {
PrefMgr.shared.candidateKeys = keys
selectionKeyComboBox.stringValue = PrefMgr.shared.candidateKeys
return
}
if let window = window {
let alert = NSAlert(error: NSLocalizedString("Invalid Selection Keys.", comment: ""))
alert.informativeText = errorResult
alert.beginSheetModal(for: window) { _ in
self.selectionKeyComboBox.stringValue = PrefMgr.shared.candidateKeys
}
IMEApp.buzz()
}
}
@IBAction func toggledExternalFactoryPlistDataOnOff(_: NSButton) {
LMMgr.reloadFactoryDictionaryPlists()
}
@IBAction func resetSpecifiedUserDataFolder(_: Any) {
LMMgr.resetSpecifiedUserDataFolder()
}
@IBAction func chooseUserDataFolderToSpecify(_: Any) {
guard let window = window else { return }
let dlgOpenPath = NSOpenPanel()
dlgOpenPath.title = NSLocalizedString(
"Choose your desired user data folder.", comment: ""
)
dlgOpenPath.showsResizeIndicator = true
dlgOpenPath.showsHiddenFiles = true
dlgOpenPath.canChooseFiles = false
dlgOpenPath.canChooseDirectories = true
dlgOpenPath.allowsMultipleSelection = false
let bolPreviousFolderValidity = LMMgr.checkIfSpecifiedUserDataFolderValid(
PrefMgr.shared.userDataFolderSpecified.expandingTildeInPath)
dlgOpenPath.beginSheetModal(for: window) { result in
if result == NSApplication.ModalResponse.OK {
guard let url = dlgOpenPath.url else { return }
// CommonDialog
//
var newPath = url.path
newPath.ensureTrailingSlash()
if LMMgr.checkIfSpecifiedUserDataFolderValid(newPath) {
PrefMgr.shared.userDataFolderSpecified = newPath
BookmarkManager.shared.saveBookmark(for: url)
(NSApp.delegate as? AppDelegate)?.updateDirectoryMonitorPath()
} else {
IMEApp.buzz()
if !bolPreviousFolderValidity {
LMMgr.resetSpecifiedUserDataFolder()
}
return
}
} else {
if !bolPreviousFolderValidity {
LMMgr.resetSpecifiedUserDataFolder()
}
return
}
}
}
@IBAction func onToggleCassetteMode(_: Any) {
if PrefMgr.shared.cassetteEnabled, !LMMgr.checkCassettePathValidity(PrefMgr.shared.cassettePath) {
if let window = window {
IMEApp.buzz()
let alert = NSAlert(error: NSLocalizedString("Path invalid or file access error.", comment: ""))
alert.informativeText = NSLocalizedString(
"Please reconfigure the cassette path to a valid one before enabling this mode.", comment: ""
)
alert.beginSheetModal(for: window) { _ in
LMMgr.resetCassettePath()
PrefMgr.shared.cassetteEnabled = false
}
}
} else {
LMMgr.loadCassetteData()
}
}
@IBAction func resetSpecifiedCassettePath(_: Any) {
LMMgr.resetCassettePath()
}
@IBAction func chooseCassettePath(_: Any) {
guard let window = window else { return }
let dlgOpenFile = NSOpenPanel()
dlgOpenFile.title = NSLocalizedString(
"Choose your desired cassette file path.", comment: ""
)
dlgOpenFile.showsResizeIndicator = true
dlgOpenFile.showsHiddenFiles = true
dlgOpenFile.canChooseFiles = true
dlgOpenFile.canChooseDirectories = false
dlgOpenFile.allowsMultipleSelection = false
dlgOpenFile.allowedFileTypes = ["cin2", "vcin", "cin"]
dlgOpenFile.allowsOtherFileTypes = true
let bolPreviousPathValidity = LMMgr.checkCassettePathValidity(
PrefMgr.shared.cassettePath.expandingTildeInPath)
dlgOpenFile.beginSheetModal(for: window) { result in
if result == NSApplication.ModalResponse.OK {
guard let url = dlgOpenFile.url else { return }
if LMMgr.checkCassettePathValidity(url.path) {
PrefMgr.shared.cassettePath = url.path
LMMgr.loadCassetteData()
BookmarkManager.shared.saveBookmark(for: url)
} else {
IMEApp.buzz()
if !bolPreviousPathValidity {
LMMgr.resetCassettePath()
}
return
}
} else {
if !bolPreviousPathValidity {
LMMgr.resetCassettePath()
}
return
}
}
}
}
// MARK: - NSToolbarDelegate Methods
extension CtlPrefWindow: NSToolbarDelegate {
func use(view: NSView) {
guard let window = window else {
return
}
window.contentView?.subviews.first?.removeFromSuperview()
let viewFrame = view.frame
var windowRect = window.frame
windowRect.size.height = kWindowTitleHeight + viewFrame.height
windowRect.size.width = viewFrame.width
windowRect.origin.y = window.frame.maxY - (viewFrame.height + kWindowTitleHeight)
window.setFrame(windowRect, display: true, animate: true)
window.contentView?.frame = view.bounds
window.contentView?.addSubview(view)
}
var toolbarIdentifiers: [NSToolbarItem.Identifier] {
[.ofGeneral, .ofExperience, .ofDictionary, .ofPhrases, .ofCassette, .ofKeyboard]
}
func toolbarDefaultItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarIdentifiers
}
func toolbarAllowedItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarIdentifiers
}
func toolbarSelectableItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarIdentifiers
}
@objc func showGeneralView(_: Any?) {
use(view: vwrGeneral)
window?.toolbar?.selectedItemIdentifier = .ofGeneral
}
@objc func showExperienceView(_: Any?) {
use(view: vwrExperience)
window?.toolbar?.selectedItemIdentifier = .ofExperience
}
@objc func showDictionaryView(_: Any?) {
use(view: vwrDictionary)
window?.toolbar?.selectedItemIdentifier = .ofDictionary
}
@objc func showPhrasesView(_: Any?) {
use(view: vwrPhrases)
window?.toolbar?.selectedItemIdentifier = .ofPhrases
}
@objc func showCassetteView(_: Any?) {
use(view: vwrCassette)
window?.toolbar?.selectedItemIdentifier = .ofCassette
}
@objc func showKeyboardView(_: Any?) {
use(view: vwrKeyboard)
window?.toolbar?.selectedItemIdentifier = .ofKeyboard
}
@objc func showDevZoneView(_: Any?) {
use(view: vwrDevZone)
window?.toolbar?.selectedItemIdentifier = .ofDevZone
}
func toolbar(
_: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar _: Bool
) -> NSToolbarItem? {
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.target = self
switch itemIdentifier {
case .ofGeneral:
let title = NSLocalizedString("General", comment: "")
item.label = title
item.image = .tabImageGeneral
item.action = #selector(showGeneralView(_:))
case .ofExperience:
let title = NSLocalizedString("Experience", comment: "")
item.label = title
item.image = .tabImageExperience
item.action = #selector(showExperienceView(_:))
case .ofDictionary:
let title = NSLocalizedString("Dictionary", comment: "")
item.label = title
item.image = .tabImageDictionary
item.action = #selector(showDictionaryView(_:))
case .ofPhrases:
item.label = CtlPrefWindow.locPhrasesTabTitle
item.image = .tabImagePhrases
item.action = #selector(showPhrasesView(_:))
case .ofCassette:
let title = NSLocalizedString("Cassette", comment: "")
item.label = title
item.image = .tabImageCassette
item.action = #selector(showCassetteView(_:))
case .ofKeyboard:
let title = NSLocalizedString("Keyboard", comment: "")
item.label = title
item.image = .tabImageKeyboard
item.action = #selector(showKeyboardView(_:))
case .ofDevZone:
let title = NSLocalizedString("DevZone", comment: "")
item.label = title
item.image = .tabImageDevZone
item.action = #selector(showDevZoneView(_:))
default:
return nil
}
return item
}
}