SettingsCocoa // First implementation, replacing CtlPrefWindow.

This commit is contained in:
ShikiSuen 2024-02-08 00:04:21 +08:00
parent b8c915dca0
commit 2465814e55
12 changed files with 1660 additions and 1 deletions

View File

@ -0,0 +1,152 @@
// (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 AppKit
import Shared
private let kWindowTitleHeight: Double = 78
// InputMethodServerPreferencesWindowControllerClass
public class CtlSettingsCocoa: NSWindowController, NSWindowDelegate {
let panes = SettingsPanesCocoa()
var previousView: NSView?
public static var shared: CtlSettingsCocoa?
@objc var observation: NSKeyValueObservation?
public init() {
super.init(
window: .init(
contentRect: CGRect(x: 401, y: 295, width: 577, height: 406),
styleMask: [.titled, .closable, .miniaturizable],
backing: .buffered,
defer: true
)
)
panes.preload()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
public static func show() {
if shared == nil {
shared = CtlSettingsCocoa()
}
guard let shared = shared, let sharedWindow = shared.window else { return }
sharedWindow.delegate = shared
if !sharedWindow.isVisible {
shared.windowDidLoad()
}
sharedWindow.setPosition(vertical: .top, horizontal: .right, padding: 20)
sharedWindow.orderFrontRegardless() //
sharedWindow.level = .statusBar
shared.showWindow(shared)
NSApp.popup()
}
private var currentLanguageSelectItem: NSMenuItem?
override public func windowDidLoad() {
super.windowDidLoad()
window?.setPosition(vertical: .top, horizontal: .right, padding: 20)
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 = PrefUITabs.tabGeneral.toolbarIdentifier
toolbar.showsBaselineSeparator = true
if #available(macOS 11.0, *) {
window?.toolbarStyle = .preference
}
window?.toolbar = toolbar
window?.title = "\(preferencesTitleName) (\(IMEApp.appVersionLabel))"
if #available(macOS 10.10, *) {
window?.titlebarAppearsTransparent = false
}
window?.allowsToolTipsWhenApplicationIsInactive = false
window?.autorecalculatesKeyViewLoop = false
window?.isRestorable = false
window?.animationBehavior = .default
window?.styleMask = [.titled, .closable, .miniaturizable]
use(view: panes.ctlPageGeneral.view, animate: false)
}
}
// MARK: - NSToolbarDelegate Methods
extension CtlSettingsCocoa: NSToolbarDelegate {
func use(view newView: NSView, animate: Bool = true) {
guard let window = window, let existingContentView = window.contentView else { return }
guard previousView != newView else { return }
newView.layoutSubtreeIfNeeded()
previousView = newView
let temporaryViewOld = NSView(frame: existingContentView.frame)
window.contentView = temporaryViewOld
var newWindowRect = NSRect(origin: window.frame.origin, size: newView.fittingSize)
newWindowRect.size.height += kWindowTitleHeight
newWindowRect.origin.y = window.frame.maxY - newWindowRect.height
window.setFrame(newWindowRect, display: true, animate: animate)
window.contentView = newView
}
var toolbarIdentifiers: [NSToolbarItem.Identifier] {
PrefUITabs.allCases.map(\.toolbarIdentifier)
}
public func toolbarDefaultItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarIdentifiers
}
public func toolbarAllowedItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarIdentifiers
}
public func toolbarSelectableItemIdentifiers(_: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarIdentifiers
}
@objc func updateTab(_ target: NSToolbarItem) {
guard let tab = PrefUITabs.fromInt(target.tag) else { return }
switch tab {
case .tabGeneral: use(view: panes.ctlPageGeneral.view)
case .tabCandidates: use(view: panes.ctlPageCandidates.view)
case .tabBehavior: use(view: panes.ctlPageBehavior.view)
case .tabOutput: use(view: panes.ctlPageOutput.view)
case .tabDictionary: use(view: panes.ctlPageDictionary.view)
case .tabPhrases: use(view: panes.ctlPagePhrases.view)
case .tabCassette: use(view: panes.ctlPageCassette.view)
case .tabKeyboard: use(view: panes.ctlPageKeyboard.view)
case .tabDevZone: use(view: panes.ctlPageDevZone.view)
}
window?.toolbar?.selectedItemIdentifier = tab.toolbarIdentifier
}
public func toolbar(
_: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar _: Bool
) -> NSToolbarItem? {
guard let tab = PrefUITabs(rawValue: itemIdentifier.rawValue) else { return nil }
let item = NSToolbarItem(itemIdentifier: itemIdentifier)
item.target = self
item.image = tab.icon
item.label = tab.i18nTitle
item.toolTip = tab.i18nTitle
item.tag = tab.cocoaTag
item.action = #selector(updateTab(_:))
return item
}
}

View File

@ -0,0 +1,74 @@
// (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 AppKit
import Foundation
import Shared
public class SettingsPanesCocoa {
public let ctlPageGeneral = SettingsPanesCocoa.General()
public let ctlPageCandidates = SettingsPanesCocoa.Candidates()
public let ctlPageBehavior = SettingsPanesCocoa.Behavior()
public let ctlPageOutput = SettingsPanesCocoa.Output()
public let ctlPageDictionary = SettingsPanesCocoa.Dictionary()
public let ctlPagePhrases = SettingsPanesCocoa.Phrases()
public let ctlPageCassette = SettingsPanesCocoa.Cassette()
public let ctlPageKeyboard = SettingsPanesCocoa.Keyboard()
public let ctlPageDevZone = SettingsPanesCocoa.DevZone()
}
public extension SettingsPanesCocoa {
func preload() {
ctlPageGeneral.loadView()
ctlPageCandidates.loadView()
ctlPageBehavior.loadView()
ctlPageOutput.loadView()
ctlPageDictionary.loadView()
ctlPagePhrases.loadView()
ctlPageCassette.loadView()
ctlPageKeyboard.loadView()
ctlPageDevZone.loadView()
}
static func warnAboutComDlg32Inavailability() {
let title = "Please drag the desired target from Finder to this place.".localized
let message = "[Technical Reason] macOS releases earlier than 10.13 have an issue: If calling NSOpenPanel directly from an input method, both the input method and its current client app hang in a dead-loop. Furthermore, it makes other apps hang in the same way when you switch into another app. If you don't want to hard-reboot your computer, your last resort is to use SSH to connect to your current computer from another computer and kill the input method process by Terminal commands. That's why vChewing cannot offer access to NSOpenPanel for macOS 10.12 and earlier.".localized
NSApp.keyWindow.callAlert(title: title, text: message)
}
}
class SettingsPreview: NSViewController {
let panes = SettingsPanesCocoa()
override func loadView() {
addChild(panes.ctlPageGeneral)
addChild(panes.ctlPageCandidates)
addChild(panes.ctlPageBehavior)
addChild(panes.ctlPageOutput)
addChild(panes.ctlPageDictionary)
addChild(panes.ctlPagePhrases)
addChild(panes.ctlPageCassette)
addChild(panes.ctlPageKeyboard)
addChild(panes.ctlPageDevZone)
view = NSTabView.build {
NSTabView.TabPage(title: "GENERAL", view: panes.ctlPageGeneral.view)
NSTabView.TabPage(title: "CANDIDATES", view: panes.ctlPageCandidates.view)
NSTabView.TabPage(title: "BEHAVIOR", view: panes.ctlPageBehavior.view)
NSTabView.TabPage(title: "OUTPUT", view: panes.ctlPageOutput.view)
NSTabView.TabPage(title: "DICT", view: panes.ctlPageDictionary.view)
NSTabView.TabPage(title: "PHRASES", view: panes.ctlPagePhrases.view)
NSTabView.TabPage(title: "CASSETTE", view: panes.ctlPageCassette.view)
NSTabView.TabPage(title: "KEYBOARD", view: panes.ctlPageKeyboard.view)
NSTabView.TabPage(title: "DEVZONE", view: panes.ctlPageDevZone.view)
} ?? .init()
}
}
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPreview()
}

View File

@ -0,0 +1,142 @@
// (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 AppKit
import Foundation
import Shared
public extension SettingsPanesCocoa {
class Behavior: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512 - 37
let tabContainerWidth: CGFloat = 512 + 20
override public func loadView() {
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
}
var body: NSView? {
NSStackView.build(.vertical) {
NSView().makeSimpleConstraint(.height, relation: .equal, value: 4)
NSTabView.build {
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: contentWidth) {
UserDef.kSpecifyShiftBackSpaceKeyBehavior.render(fixWidth: contentWidth)
UserDef.kSpecifyShiftTabKeyBehavior.render(fixWidth: contentWidth)
UserDef.kSpecifyShiftSpaceKeyBehavior.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kUpperCaseLetterKeyBehavior.render(fixWidth: contentWidth)
UserDef.kNumPadCharInputBehavior.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kSpecifyIntonationKeyBehavior.render(fixWidth: contentWidth)
UserDef.kAcceptLeadingIntonations.render(fixWidth: contentWidth)
}?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: contentWidth) {
UserDef.kChooseCandidateUsingSpace.render(fixWidth: contentWidth)
UserDef.kEscToCleanInputBuffer.render(fixWidth: contentWidth)
UserDef.kAlsoConfirmAssociatedCandidatesByEnter.render(fixWidth: contentWidth)
UserDef.kUseSpaceToCommitHighlightedSCPCCandidate.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
if #available(macOS 12, *) {
UserDef.kShowNotificationsWhenTogglingCapsLock.render(fixWidth: contentWidth)
}
UserDef.kAlwaysShowTooltipTextsHorizontally.render(fixWidth: contentWidth)
if Date.isTodayTheDate(from: 0401) {
UserDef.kShouldNotFartInLieuOfBeep.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.onFartControlChange(_:))
}
}
}?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: contentWidth) {
UserDef.kBypassNonAppleCapsLockHandling.render(fixWidth: contentWidth)
UserDef.kShareAlphanumericalModeStatusAcrossClients.render(fixWidth: contentWidth)
if #available(macOS 10.15, *) {
NSStackView.build(.vertical) {
UserDef.kTogglingAlphanumericalModeWithLShift.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.syncShiftKeyUpChecker(_:))
}
UserDef.kTogglingAlphanumericalModeWithRShift.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.syncShiftKeyUpChecker(_:))
}
var strOSReq = ""
strOSReq += String(
format: "This feature requires macOS %@ and above.".localized, arguments: ["10.15"]
)
strOSReq += "\n"
strOSReq += "i18n:settings.shiftKeyASCIITogle.description".localized
strOSReq.makeNSLabel(descriptive: true, fixWidth: contentWidth)
}
}
UserDef.kShiftEisuToggleOffTogetherWithCapsLock.render(fixWidth: contentWidth)
}?.boxed()
NSView()
}
}?.makeSimpleConstraint(.width, relation: .equal, value: tabContainerWidth)
NSView().makeSimpleConstraint(.height, relation: .equal, value: NSFont.systemFontSize)
}
}
@IBAction func syncShiftKeyUpChecker(_: NSControl) {
print("Syncing ShiftKeyUpChecker configurations.")
SessionCtl.theShiftKeyDetector.toggleWithLShift = PrefMgr.shared.togglingAlphanumericalModeWithLShift
SessionCtl.theShiftKeyDetector.toggleWithRShift = PrefMgr.shared.togglingAlphanumericalModeWithRShift
}
@IBAction func onFartControlChange(_: NSControl) {
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: ""))
let window = NSApp.keyWindow
if !PrefMgr.shared.shouldNotFartInLieuOfBeep {
PrefMgr.shared.shouldNotFartInLieuOfBeep = true
alert.beginSheetModal(at: window) { result in
switch result {
case .alertFirstButtonReturn:
PrefMgr.shared.shouldNotFartInLieuOfBeep = false
case .alertSecondButtonReturn:
PrefMgr.shared.shouldNotFartInLieuOfBeep = true
default: break
}
IMEApp.buzz()
}
return
}
IMEApp.buzz()
}
}
}
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPanesCocoa.Behavior()
}

View File

@ -0,0 +1,116 @@
// (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 AppKit
import Foundation
import Shared
public extension SettingsPanesCocoa {
class Candidates: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512 - 37
let tabContainerWidth: CGFloat = 512 + 20
override public func loadView() {
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
}
var body: NSView? {
NSStackView.build(.vertical) {
NSView().makeSimpleConstraint(.height, relation: .equal, value: 4)
NSTabView.build {
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: contentWidth) {
UserDef.kCandidateKeys.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.candidateKeysDidSet(_:))
renderable.currentControl?.alignment = .right
}
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kUseHorizontalCandidateList.render(fixWidth: contentWidth)
UserDef.kCandidateListTextSize.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.candidateFontSizeDidSet(_:))
}
UserDef.kCandidateWindowShowOnlyOneLine.render(fixWidth: contentWidth)
UserDef.kAlwaysExpandCandidateWindow.render(fixWidth: contentWidth)
UserDef.kRespectClientAccentColor.render(fixWidth: contentWidth)
}?.boxed()
NSView()
}
NSTabView.TabPage(title: "") {
NSStackView.buildSection(width: contentWidth) {
UserDef.kUseRearCursorMode.render(fixWidth: contentWidth)
UserDef.kMoveCursorAfterSelectingCandidate.render(fixWidth: contentWidth)
UserDef.kUseDynamicCandidateWindowOrigin.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kShowReverseLookupInCandidateUI.render(fixWidth: contentWidth)
UserDef.kUseFixedCandidateOrderOnSelection.render(fixWidth: contentWidth)
UserDef.kConsolidateContextOnCandidateSelection.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kEnableMouseScrollingForTDKCandidatesCocoa.render(fixWidth: contentWidth)
NSStackView.build(.horizontal) {
"Where's IMK Candidate Window?".makeNSLabel(fixWidth: contentWidth)
NSView()
NSButton(verbatim: "...", target: self, action: #selector(whereIsIMKCandidatesWindow(_:)))
}
}?.boxed()
NSView()
}
}?.makeSimpleConstraint(.width, relation: .equal, value: tabContainerWidth)
NSView().makeSimpleConstraint(.height, relation: .equal, value: NSFont.systemFontSize)
}
}
@IBAction func whereIsIMKCandidatesWindow(_: Any) {
let window = NSApp.keyWindow
let title = "The End of Support for IMK Candidate Window"
let explanation = "1) Only macOS has IMKCandidates. Since it relies on a dedicated ObjC Bridging Header to expose necessary internal APIs to work, it hinders vChewing from completely modularized for multi-platform support.\n\n2) IMKCandidates is buggy. It is not likely to be completely fixed by Apple, and its devs are not allowed to talk about it to non-Apple individuals. That's why we have had enough with IMKCandidates. It is likely the reason why Apple had never used IMKCandidates in their official InputMethodKit sample projects (as of August 2023)."
window.callAlert(title: title.localized, text: explanation.localized)
}
@IBAction func candidateKeysDidSet(_ sender: NSComboBox) {
let keys = sender.stringValue.trimmingCharacters(
in: .whitespacesAndNewlines
).lowercased().deduplicated
// Start Error Handling.
guard let errorResult = CandidateKey.validate(keys: keys) else {
PrefMgr.shared.candidateKeys = keys
return
}
let alert = NSAlert(error: NSLocalizedString("Invalid Selection Keys.", comment: ""))
alert.informativeText = errorResult
IMEApp.buzz()
if let window = NSApp.keyWindow {
alert.beginSheetModal(for: window) { _ in
sender.stringValue = CandidateKey.defaultKeys
}
} else {
switch alert.runModal() {
default: sender.stringValue = CandidateKey.defaultKeys
}
}
}
@IBAction func candidateFontSizeDidSet(_: NSControl) {
print("Candidate Font Size Changed to \(PrefMgr.shared.candidateListTextSize)")
guard !(12 ... 196).contains(PrefMgr.shared.candidateListTextSize) else { return }
PrefMgr.shared.candidateListTextSize = max(12, min(PrefMgr.shared.candidateListTextSize, 196))
}
}
}
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPanesCocoa.Candidates()
}

View File

@ -0,0 +1,173 @@
// (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 AppKit
import BookmarkManager
import Foundation
import Shared
public extension SettingsPanesCocoa {
class Cassette: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512
let pctCassetteFilePath: NSPathControl = .init()
override public func loadView() {
prepareCassetteFolderPathControl(pctCassetteFilePath)
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
}
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
NSStackView.buildSection(width: contentWidth) {
UserDef.kCassettePath.render { renderable in
renderable.currentControl = self.pctCassetteFilePath
renderable.mainViewOverride = self.pathControlMainView
}
UserDef.kCassetteEnabled.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.cassetteEnabledToggled(_:))
}
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kAutoCompositeWithLongestPossibleCassetteKey.render(fixWidth: contentWidth)
UserDef.kShowTranslatedStrokesInCompositionBuffer.render(fixWidth: contentWidth)
UserDef.kForceCassetteChineseConversion.render(fixWidth: contentWidth)
}?.boxed()
NSView().makeSimpleConstraint(.height, relation: .equal, value: NSFont.systemFontSize)
}
}
func pathControlMainView() -> NSView? {
NSStackView.build(.horizontal) {
self.pctCassetteFilePath
NSButton(verbatim: "...", target: self, action: #selector(chooseCassetteFileToSpecify(_:)))
NSButton(verbatim: "×", target: self, action: #selector(resetCassettePath(_:)))
}
}
func prepareCassetteFolderPathControl(_ pathCtl: NSPathControl) {
pathCtl.delegate = self
(pathCtl.cell as? NSTextFieldCell)?.placeholderString = "Please drag the desired target from Finder to this place.".localized
pathCtl.allowsExpansionToolTips = true
(pathCtl.cell as? NSPathCell)?.allowedTypes = ["cin2", "cin", "vcin"]
pathCtl.translatesAutoresizingMaskIntoConstraints = false
pathCtl.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
if #available(macOS 10.10, *) {
pathCtl.controlSize = .small
}
pathCtl.backgroundColor = .controlBackgroundColor
pathCtl.target = self
pathCtl.doubleAction = #selector(pathControlDoubleAction(_:))
pathCtl.setContentHuggingPriority(.defaultHigh, for: .vertical)
pathCtl.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
pathCtl.makeSimpleConstraint(.height, relation: .equal, value: NSFont.smallSystemFontSize * 2)
pathCtl.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: 432)
let currentPath = LMMgr.cassettePath()
pathCtl.url = currentPath.isEmpty ? nil : URL(fileURLWithPath: LMMgr.cassettePath())
pathCtl.toolTip = "Please drag the desired target from Finder to this place.".localized
}
@IBAction func cassetteEnabledToggled(_: NSControl) {}
}
}
// MARK: - Controls related to data path settings.
extension SettingsPanesCocoa.Cassette: NSPathControlDelegate {
public func pathControl(_ pathControl: NSPathControl, acceptDrop info: NSDraggingInfo) -> Bool {
let urls = info.draggingPasteboard.readObjects(forClasses: [NSURL.self])
guard let url = urls?.first as? URL else { return false }
guard pathControl === pctCassetteFilePath else { return false }
let bolPreviousPathValidity = LMMgr.checkCassettePathValidity(
PrefMgr.shared.cassettePath.expandingTildeInPath
)
if LMMgr.checkCassettePathValidity(url.path) {
PrefMgr.shared.cassettePath = url.path
LMMgr.loadCassetteData()
BookmarkManager.shared.saveBookmark(for: url)
pathControl.url = url
return true
}
// On Error:
IMEApp.buzz()
if !bolPreviousPathValidity {
LMMgr.resetCassettePath()
}
return false
}
@IBAction func resetCassettePath(_: Any) {
LMMgr.resetCassettePath()
}
@IBAction func pathControlDoubleAction(_ sender: NSPathControl) {
guard let url = sender.url else { return }
NSWorkspace.shared.activateFileViewerSelecting([url])
}
@IBAction func chooseCassetteFileToSpecify(_: Any) {
if NSEvent.keyModifierFlags == .option, let url = pctCassetteFilePath.url {
NSWorkspace.shared.activateFileViewerSelecting([url])
return
}
guard #available(macOS 10.13, *) else {
SettingsPanesCocoa.warnAboutComDlg32Inavailability()
return
}
let dlgOpenFile = NSOpenPanel()
dlgOpenFile.showsResizeIndicator = true
dlgOpenFile.showsHiddenFiles = true
dlgOpenFile.canChooseFiles = true
dlgOpenFile.canChooseDirectories = false
dlgOpenFile.allowsMultipleSelection = false
if #available(macOS 11.0, *) {
dlgOpenFile.allowedContentTypes = ["cin2", "vcin", "cin"].compactMap { .init(filenameExtension: $0) }
} else {
dlgOpenFile.allowedFileTypes = ["cin2", "vcin", "cin"]
}
dlgOpenFile.allowsOtherFileTypes = true
let bolPreviousPathValidity = LMMgr.checkCassettePathValidity(
PrefMgr.shared.cassettePath.expandingTildeInPath)
let window = NSApp.keyWindow
dlgOpenFile.beginSheetModal(at: 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)
self.pctCassetteFilePath.url = url
} else {
IMEApp.buzz()
if !bolPreviousPathValidity {
LMMgr.resetCassettePath()
}
return
}
} else {
if !bolPreviousPathValidity {
LMMgr.resetCassettePath()
}
return
}
}
}
}
// MARK: - Preview
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPanesCocoa.Cassette()
}

View File

@ -0,0 +1,50 @@
// (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 AppKit
import Foundation
import Shared
public extension SettingsPanesCocoa {
class DevZone: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512
override public func loadView() {
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
}
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
NSStackView.build(.horizontal, insets: .new(all: 0, left: 16, right: 16)) {
"Warning: This page is for testing future features. \nFeatures listed here may not work as expected.".makeNSLabel(fixWidth: contentWidth)
NSView()
}
NSStackView.buildSection(width: contentWidth) {
UserDef.kSecurityHardenedCompositionBuffer.render(fixWidth: contentWidth)
UserDef.kDisableSegmentedThickUnderlineInMarkingModeForManagedClients.render(fixWidth: contentWidth)
UserDef.kCheckAbusersOfSecureEventInputAPI.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.build(.horizontal, insets: .new(all: 0, left: 16, right: 16)) {
"Some previous options are moved to other tabs.".makeNSLabel(descriptive: true, fixWidth: contentWidth)
NSView()
}
NSView().makeSimpleConstraint(.height, relation: .equal, value: NSFont.systemFontSize)
}
}
@IBAction func sanityCheck(_: NSControl) {}
}
}
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPanesCocoa.DevZone()
}

View File

@ -0,0 +1,249 @@
// (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 AppKit
import BookmarkManager
import Foundation
import Shared
public extension SettingsPanesCocoa {
class Dictionary: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512
let pctUserDictionaryFolder: NSPathControl = .init()
override public func loadView() {
prepareUserDictionaryFolderPathControl(pctUserDictionaryFolder)
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
}
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
NSStackView.buildSection(width: contentWidth) {
UserDef.kUserDataFolderSpecified.render { renderable in
renderable.currentControl = self.pctUserDictionaryFolder
renderable.mainViewOverride = self.pathControlMainView
}
NSStackView.build(.vertical) {
UserDef.kShouldAutoReloadUserDataFiles.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.lmmgrInitUserLMsWhenShould(_:))
}
"Due to security concerns, we don't consider implementing anything related to shell script execution here. An input method doing this without implementing App Sandbox will definitely have system-wide vulnerabilities, considering that its related UserDefaults are easily tamperable to execute malicious shell scripts. vChewing is designed to be invulnerable from this kind of attack. Also, official releases of vChewing are Sandboxed.".makeNSLabel(descriptive: true, fixWidth: contentWidth)
}
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kUseExternalFactoryDict.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.lmmgrConnectCoreDB(_:))
}
UserDef.kFetchSuggestionsFromUserOverrideModel.render(fixWidth: contentWidth)
UserDef.kCNS11643Enabled.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.lmmgrSyncLMPrefs(_:))
}
UserDef.kSymbolInputEnabled.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.lmmgrSyncLMPrefs(_:))
}
UserDef.kPhraseReplacementEnabled.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.lmmgrSyncLMPrefsWithReplacementTable(_:))
}
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kAllowBoostingSingleKanjiAsUserPhrase.render(fixWidth: contentWidth)
NSStackView.build(.horizontal) {
"i18n:settings.importFromKimoTxt.buttonText".makeNSLabel(fixWidth: contentWidth)
NSView()
NSButton(
verbatim: "...",
target: self,
action: #selector(importYahooKeyKeyUserDictionaryData(_:))
)
}
}?.boxed()
NSView().makeSimpleConstraint(.height, relation: .equal, value: NSFont.systemFontSize)
}
}
func pathControlMainView() -> NSView? {
NSStackView.build(.horizontal) {
self.pctUserDictionaryFolder
NSButton(verbatim: "...", target: self, action: #selector(chooseUserDataFolderToSpecify(_:)))
NSButton(verbatim: "", target: self, action: #selector(resetSpecifiedUserDataFolder(_:)))
}
}
func prepareUserDictionaryFolderPathControl(_ pathCtl: NSPathControl) {
pathCtl.delegate = self
pathCtl.allowsExpansionToolTips = true
pathCtl.translatesAutoresizingMaskIntoConstraints = false
pathCtl.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
if #available(macOS 10.10, *) {
pathCtl.controlSize = .small
}
pathCtl.backgroundColor = .controlBackgroundColor
pathCtl.target = self
pathCtl.doubleAction = #selector(pathControlDoubleAction(_:))
pathCtl.setContentHuggingPriority(.defaultHigh, for: .vertical)
pathCtl.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
pathCtl.makeSimpleConstraint(.height, relation: .equal, value: NSFont.smallSystemFontSize * 2)
pathCtl.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: 432)
pathCtl.url = URL(fileURLWithPath: LMMgr.dataFolderPath(isDefaultFolder: false))
pathCtl.toolTip = "Please drag the desired target from Finder to this place.".localized
}
@IBAction func lmmgrInitUserLMsWhenShould(_: NSControl) {
if PrefMgr.shared.shouldAutoReloadUserDataFiles {
LMMgr.initUserLangModels()
}
}
@IBAction func lmmgrConnectCoreDB(_: NSControl) {
LMMgr.connectCoreDB()
}
@IBAction func lmmgrSyncLMPrefs(_: NSControl) {
LMMgr.syncLMPrefs()
}
@IBAction func lmmgrSyncLMPrefsWithReplacementTable(_: NSControl) {
LMMgr.syncLMPrefs()
if PrefMgr.shared.phraseReplacementEnabled {
LMMgr.loadUserPhraseReplacement()
}
}
@IBAction func importYahooKeyKeyUserDictionaryData(_: NSButton) {
let dlgOpenFile = NSOpenPanel()
dlgOpenFile.title = NSLocalizedString(
"i18n:settings.importFromKimoTxt.buttonText", comment: ""
) + ":"
dlgOpenFile.showsResizeIndicator = true
dlgOpenFile.showsHiddenFiles = true
dlgOpenFile.canChooseFiles = true
dlgOpenFile.allowsMultipleSelection = false
dlgOpenFile.canChooseDirectories = false
if #unavailable(macOS 11) {
dlgOpenFile.allowedFileTypes = ["txt"]
} else {
dlgOpenFile.allowedContentTypes = [.init(filenameExtension: "txt")].compactMap { $0 }
}
let window = NSApp.keyWindow
dlgOpenFile.beginSheetModal(at: window) { result in
if result == NSApplication.ModalResponse.OK {
guard let url = dlgOpenFile.url else { return }
guard var rawString = try? String(contentsOf: url) else { return }
let count = LMMgr.importYahooKeyKeyUserDictionary(text: &rawString)
window.callAlert(title: String(format: "i18n:settings.importFromKimoTxt.finishedCount:%@".localized, count.description))
}
}
}
}
}
// MARK: - Controls related to data path settings.
extension SettingsPanesCocoa.Dictionary: NSPathControlDelegate {
public func pathControl(_ pathControl: NSPathControl, acceptDrop info: NSDraggingInfo) -> Bool {
let urls = info.draggingPasteboard.readObjects(forClasses: [NSURL.self])
guard let url = urls?.first as? URL else { return false }
guard pathControl === pctUserDictionaryFolder else { return false }
let bolPreviousFolderValidity = LMMgr.checkIfSpecifiedUserDataFolderValid(
PrefMgr.shared.userDataFolderSpecified.expandingTildeInPath)
var newPath = url.path
newPath.ensureTrailingSlash()
if LMMgr.checkIfSpecifiedUserDataFolderValid(newPath) {
PrefMgr.shared.userDataFolderSpecified = newPath
BookmarkManager.shared.saveBookmark(for: url)
AppDelegate.shared.updateDirectoryMonitorPath()
pathControl.url = url
return true
}
// On Error:
IMEApp.buzz()
if !bolPreviousFolderValidity {
LMMgr.resetSpecifiedUserDataFolder()
pathControl.url = URL(fileURLWithPath: LMMgr.dataFolderPath(isDefaultFolder: true))
}
return false
}
@IBAction func resetSpecifiedUserDataFolder(_: Any) {
LMMgr.resetSpecifiedUserDataFolder()
pctUserDictionaryFolder.url = URL(fileURLWithPath: LMMgr.dataFolderPath(isDefaultFolder: true))
}
@IBAction func pathControlDoubleAction(_ sender: NSPathControl) {
guard let url = sender.url else { return }
NSWorkspace.shared.activateFileViewerSelecting([url])
}
@IBAction func chooseUserDataFolderToSpecify(_: Any) {
if NSEvent.keyModifierFlags == .option, let url = pctUserDictionaryFolder.url {
NSWorkspace.shared.activateFileViewerSelecting([url])
return
}
guard #available(macOS 10.13, *) else {
SettingsPanesCocoa.warnAboutComDlg32Inavailability()
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)
let window = NSApp.keyWindow
dlgOpenPath.beginSheetModal(at: 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)
AppDelegate.shared.updateDirectoryMonitorPath()
self.pctUserDictionaryFolder.url = url
} else {
IMEApp.buzz()
if !bolPreviousFolderValidity {
LMMgr.resetSpecifiedUserDataFolder()
self.pctUserDictionaryFolder.url = URL(fileURLWithPath: LMMgr.dataFolderPath(isDefaultFolder: true))
}
return
}
} else {
if !bolPreviousFolderValidity {
LMMgr.resetSpecifiedUserDataFolder()
self.pctUserDictionaryFolder.url = URL(fileURLWithPath: LMMgr.dataFolderPath(isDefaultFolder: true))
}
return
}
}
}
}
// MARK: - Preview
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPanesCocoa.Dictionary()
}

View File

@ -0,0 +1,109 @@
// (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 AppKit
import Foundation
import Shared
public extension SettingsPanesCocoa {
class General: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512
var contentHalfWidth: CGFloat { contentWidth / 2 - 4 }
var currentLanguageSelectItem: NSMenuItem?
let btnLangSelector = NSPopUpButton()
let languages = ["auto", "en", "zh-Hans", "zh-Hant", "ja"]
override public func loadView() {
prepareLangSelectorButton()
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
}
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
NSStackView.buildSection(width: contentWidth, withDividers: false) {
var strNotice = "\u{2022} "
strNotice += "Please use mouse wheel to scroll each page if needed. The CheatSheet is available in the IME menu.".localized
strNotice += "\n\u{2022} "
strNotice += "Note: The “Delete ⌫” key on Mac keyboard is named as “BackSpace ⌫” here in order to distinguish the real “Delete ⌦” key from full-sized desktop keyboards. If you want to use the real “Delete ⌦” key on a Mac keyboard with no numpad equipped, you have to press “Fn+⌫” instead.".localized
strNotice.makeNSLabel(descriptive: true, fixWidth: contentWidth)
UserDef.kAppleLanguages.render { renderable in
renderable.currentControl = self.btnLangSelector
}
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kReadingNarrationCoverage.render(fixWidth: contentWidth)
UserDef.kAutoCorrectReadingCombination.render(fixWidth: contentWidth)
UserDef.kShowHanyuPinyinInCompositionBuffer.render(fixWidth: contentWidth)
UserDef.kKeepReadingUponCompositionError.render(fixWidth: contentWidth)
UserDef.kClassicHaninKeyboardSymbolModeShortcutEnabled.render(fixWidth: contentWidth)
UserDef.kUseSCPCTypingMode.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.buildSection(.horizontal, width: contentWidth) {
UserDef.kCheckUpdateAutomatically.render(fixWidth: contentHalfWidth)
UserDef.kIsDebugModeEnabled.render(fixWidth: contentHalfWidth)
}?.boxed()
NSView().makeSimpleConstraint(.height, relation: .equal, value: NSFont.systemFontSize)
}
}
// Credit: Hiraku (in ObjC; 2022); Refactored by Shiki (2024).
func prepareLangSelectorButton() {
let chosenLangObj = PrefMgr.shared.appleLanguages.first ?? "auto"
btnLangSelector.menu?.removeAllItems()
// NSMenu.items macOS 10.13
// property 西
btnLangSelector.menu?.appendItems {
for language in languages {
NSMenuItem(language.localized)?.represent(language)
}
}
currentLanguageSelectItem = btnLangSelector.menu?.items.first {
$0.representedObject as? String == chosenLangObj
} ?? btnLangSelector.menu?.items.first
btnLangSelector.select(currentLanguageSelectItem)
btnLangSelector.action = #selector(updateUiLanguageAction(_:))
btnLangSelector.target = self
btnLangSelector.font = NSFont.systemFont(ofSize: 12)
}
@IBAction func updateNarratorSettingsAction(_: NSControl) {
SpeechSputnik.shared.refreshStatus()
}
@IBAction func updateSCPCSettingsAction(_: NSControl) {
guard PrefMgr.shared.useSCPCTypingMode else { return }
LMMgr.loadSCPCSequencesData()
}
@IBAction func updateUiLanguageAction(_ sender: NSPopUpButton) {
let language = languages[sender.indexOfSelectedItem]
guard let bundleID = Bundle.main.bundleIdentifier, bundleID.contains("vChewing") else {
print("App Language Changed to \(language).")
return
}
if let selectItem = btnLangSelector.selectedItem, currentLanguageSelectItem == selectItem {
return
}
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)
}
}
}
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPanesCocoa.General()
}

View File

@ -0,0 +1,95 @@
// (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 AppKit
import Foundation
import IMKUtils
import Shared
public extension SettingsPanesCocoa {
class Keyboard: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512
var contentHalfWidth: CGFloat { contentWidth / 2 - 4 }
override public func loadView() {
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
}
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
NSStackView.buildSection(width: contentWidth) {
NSStackView.build(.horizontal) {
"Quick Setup:".makeNSLabel(fixWidth: contentWidth)
NSView()
NSButton(
verbatim: "↻ㄅ" + " " + "Dachen Trad.".localized,
target: self,
action: #selector(quickSetupButtonDachen(_:))
)
NSButton(
verbatim: "↻ㄅ" + " " + "Eten Trad.".localized,
target: self,
action: #selector(quickSetupButtonEtenTraditional(_:))
)
NSButton(
verbatim: "↻A", target: self,
action: #selector(quickSetupButtonHanyuPinyin(_:))
)
}
UserDef.kKeyboardParser.render(fixWidth: contentWidth)
UserDef.kBasicKeyboardLayout.render(fixWidth: contentWidth)
UserDef.kAlphanumericalKeyboardLayout.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.build(.horizontal, insets: .new(all: 4, left: 16, right: 16)) {
"Keyboard Shortcuts:".makeNSLabel(fixWidth: contentWidth)
NSView()
}
NSStackView.buildSection(.horizontal, width: contentWidth) {
NSStackView.build(.vertical) {
UserDef.kUsingHotKeySCPC.render(fixWidth: contentHalfWidth)
UserDef.kUsingHotKeyAssociates.render(fixWidth: contentHalfWidth)
UserDef.kUsingHotKeyCNS.render(fixWidth: contentHalfWidth)
UserDef.kUsingHotKeyKangXi.render(fixWidth: contentHalfWidth)
UserDef.kUsingHotKeyRevLookup.render(fixWidth: contentHalfWidth)
}
NSStackView.build(.vertical) {
UserDef.kUsingHotKeyJIS.render(fixWidth: contentHalfWidth)
UserDef.kUsingHotKeyHalfWidthASCII.render(fixWidth: contentHalfWidth)
UserDef.kUsingHotKeyCurrencyNumerals.render(fixWidth: contentHalfWidth)
UserDef.kUsingHotKeyCassette.render(fixWidth: contentHalfWidth)
UserDef.kUsingHotKeyInputMode.render(fixWidth: contentHalfWidth)
}
}?.boxed()
NSView().makeSimpleConstraint(.height, relation: .equal, value: NSFont.systemFontSize)
}
}
@IBAction func quickSetupButtonDachen(_: NSControl) {
PrefMgr.shared.keyboardParser = 0
PrefMgr.shared.basicKeyboardLayout = "com.apple.keylayout.ZhuyinBopomofo"
}
@IBAction func quickSetupButtonEtenTraditional(_: NSControl) {
PrefMgr.shared.keyboardParser = 1
PrefMgr.shared.basicKeyboardLayout = "com.apple.keylayout.ZhuyinEten"
}
@IBAction func quickSetupButtonHanyuPinyin(_: NSControl) {
PrefMgr.shared.keyboardParser = 100
PrefMgr.shared.basicKeyboardLayout = "com.apple.keylayout.ABC"
}
}
}
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPanesCocoa.Keyboard()
}

View File

@ -0,0 +1,62 @@
// (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 AppKit
import Foundation
import Shared
public extension SettingsPanesCocoa {
class Output: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512
override public func loadView() {
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
}
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
NSStackView.buildSection(width: contentWidth) {
UserDef.kChineseConversionEnabled.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.sanityCheckKangXi(_:))
}
UserDef.kShiftJISShinjitaiOutputEnabled.render { renderable in
renderable.currentControl?.target = self
renderable.currentControl?.action = #selector(self.sanityCheckJIS(_:))
}
UserDef.kInlineDumpPinyinInLieuOfZhuyin.render(fixWidth: contentWidth)
UserDef.kTrimUnfinishedReadingsOnCommit.render(fixWidth: contentWidth)
}?.boxed()
NSStackView.buildSection(width: contentWidth) {
UserDef.kHardenVerticalPunctuations.render(fixWidth: contentWidth)
}?.boxed()
NSView().makeSimpleConstraint(.height, relation: .equal, value: NSFont.systemFontSize)
}
}
@IBAction func sanityCheckKangXi(_: NSControl) {
if PrefMgr.shared.chineseConversionEnabled, PrefMgr.shared.shiftJISShinjitaiOutputEnabled {
PrefMgr.shared.shiftJISShinjitaiOutputEnabled = false
}
}
@IBAction func sanityCheckJIS(_: NSControl) {
if PrefMgr.shared.chineseConversionEnabled, PrefMgr.shared.shiftJISShinjitaiOutputEnabled {
PrefMgr.shared.chineseConversionEnabled = false
}
}
}
}
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPanesCocoa.Output()
}

View File

@ -0,0 +1,437 @@
// (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 AppKit
import Foundation
import LangModelAssembly
import Shared
public extension SettingsPanesCocoa {
class Phrases: NSViewController {
let windowWidth: CGFloat = 577
let contentWidth: CGFloat = 512
let cmbPEInputModeMenu = NSPopUpButton()
let cmbPEDataTypeMenu = NSPopUpButton()
let btnPEReload = NSButton()
let btnPEConsolidate = NSButton()
let btnPESave = NSButton()
let btnPEOpenExternally = NSButton()
let txtPECommentField = NSTextField()
let txtPEField1 = NSTextField()
let txtPEField2 = NSTextField()
let txtPEField3 = NSTextField()
let btnPEAdd = NSButton()
let formatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximum = 1.0
formatter.minimum = -114.514
return formatter
}()
lazy var scrollview = NSScrollView()
lazy var tfdPETextEditor: NSTextView = {
let result = NSTextView(frame: CGRect())
result.font = NSFont.systemFont(ofSize: 13)
result.allowsUndo = true
return result
}()
@objc var observation: NSKeyValueObservation?
var isLoading = false {
didSet { setPEUIControlAvailability() }
}
override public func loadView() {
observation = Broadcaster.shared.observe(\.eventForReloadingPhraseEditor, options: [.new]) { _, _ in
self.updatePhraseEditor()
}
initPhraseEditor()
view = body ?? .init()
(view as? NSStackView)?.alignment = .centerX
view.makeSimpleConstraint(.width, relation: .equal, value: windowWidth)
}
var body: NSView? {
NSStackView.build(.vertical, insets: .new(all: 14)) {
NSStackView.buildSection(width: contentWidth) {
NSStackView.build(.vertical) {
NSStackView.build(.horizontal) {
cmbPEInputModeMenu
cmbPEDataTypeMenu
NSView()
btnPEReload
btnPEConsolidate
btnPESave
btnPEOpenExternally
}
createTextViewStack().makeSimpleConstraint(.height, relation: .equal, value: 370)
NSStackView.build(.horizontal) {
txtPECommentField
}
NSStackView.build(.horizontal) {
txtPEField1.makeSimpleConstraint(.width, relation: .equal, value: 185)
txtPEField2.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: 80)
txtPEField3.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: 90)
btnPEAdd
}
UserDef.kPhraseEditorAutoReloadExternalModifications.render { renderable in
renderable.tinySize = true
}
}
}?.boxed()
NSView().makeSimpleConstraint(.height, relation: .equal, value: NSFont.systemFontSize)
}
}
public func createTextViewStack() -> NSScrollView {
let contentSize = scrollview.contentSize
if let n = tfdPETextEditor.textContainer {
n.containerSize = CGSize(width: contentSize.width, height: CGFloat.greatestFiniteMagnitude)
n.widthTracksTextView = true
}
tfdPETextEditor.minSize = CGSize(width: 0, height: 0)
tfdPETextEditor.maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
tfdPETextEditor.isVerticallyResizable = true
tfdPETextEditor.frame = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
tfdPETextEditor.autoresizingMask = [.width]
tfdPETextEditor.delegate = self
scrollview.borderType = .noBorder
scrollview.hasVerticalScroller = true
scrollview.hasHorizontalScroller = true
scrollview.documentView = tfdPETextEditor
scrollview.scrollerStyle = .legacy
scrollview.autohidesScrollers = true
return scrollview
}
override public func viewWillAppear() {
initPhraseEditor()
}
override public func viewWillDisappear() {
tfdPETextEditor.string.removeAll()
}
@IBAction func sanityCheck(_: NSControl) {}
}
}
extension SettingsPanesCocoa.Phrases: NSTextViewDelegate, NSTextFieldDelegate {
var selInputMode: Shared.InputMode {
switch cmbPEInputModeMenu.selectedTag() {
case 0: return .imeModeCHS
case 1: return .imeModeCHT
default: return .imeModeNULL
}
}
var selUserDataType: vChewingLM.ReplacableUserDataType {
switch cmbPEDataTypeMenu.selectedTag() {
case 0: return .thePhrases
case 1: return .theFilter
case 2: return .theReplacements
case 3: return .theAssociates
case 4: return .theSymbols
default: return .thePhrases
}
}
func updatePhraseEditor() {
updateLabels()
clearAllFields()
isLoading = true
tfdPETextEditor.string = NSLocalizedString("Loading…", comment: "")
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.tfdPETextEditor.string = LMMgr.retrieveData(mode: self.selInputMode, type: self.selUserDataType)
self.tfdPETextEditor.toolTip = PETerminology.TooltipTexts.sampleDictionaryContent(for: self.selUserDataType)
self.isLoading = false
}
}
func setPEUIControlAvailability() {
btnPEReload.isEnabled = selInputMode != .imeModeNULL && !isLoading
btnPEConsolidate.isEnabled = selInputMode != .imeModeNULL && !isLoading
btnPESave.isEnabled = true // TextView
btnPEAdd.isEnabled =
!txtPEField1.stringValue.isEmpty && !txtPEField2.stringValue.isEmpty && selInputMode != .imeModeNULL && !isLoading
tfdPETextEditor.isEditable = selInputMode != .imeModeNULL && !isLoading
txtPEField1.isEnabled = selInputMode != .imeModeNULL && !isLoading
txtPEField2.isEnabled = selInputMode != .imeModeNULL && !isLoading
txtPEField3.isEnabled = selInputMode != .imeModeNULL && !isLoading
txtPEField3.isHidden = selUserDataType != .thePhrases || isLoading
txtPECommentField.isEnabled = selUserDataType != .theAssociates && !isLoading
}
func updateLabels() {
clearAllFields()
switch selUserDataType {
case .thePhrases:
(txtPEField1.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locPhrase.localized.0
(txtPEField2.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locReadingOrStroke.localized.0
(txtPEField3.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locWeight.localized.0
(txtPECommentField.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locComment.localized.0
case .theFilter:
(txtPEField1.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locPhrase.localized.0
(txtPEField2.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locReadingOrStroke.localized.0
(txtPEField3.cell as? NSTextFieldCell)?.placeholderString = ""
(txtPECommentField.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locComment.localized.0
case .theReplacements:
(txtPEField1.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locReplaceTo.localized.0
(txtPEField2.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locReplaceTo.localized.1
(txtPEField3.cell as? NSTextFieldCell)?.placeholderString = ""
(txtPECommentField.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locComment.localized.0
case .theAssociates:
(txtPEField1.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locInitial.localized.0
(txtPEField2.cell as? NSTextFieldCell)?.placeholderString = {
let result = PETerminology.AddPhrases.locPhrase.localized.0
return (result == "Phrase") ? "Phrases" : result
}()
(txtPEField3.cell as? NSTextFieldCell)?.placeholderString = ""
(txtPECommentField.cell as? NSTextFieldCell)?.placeholderString = NSLocalizedString(
"Inline comments are not supported in associated phrases.", comment: ""
)
case .theSymbols:
(txtPEField1.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locPhrase.localized.0
(txtPEField2.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locReadingOrStroke.localized.0
(txtPEField3.cell as? NSTextFieldCell)?.placeholderString = ""
(txtPECommentField.cell as? NSTextFieldCell)?.placeholderString = PETerminology.AddPhrases.locComment.localized.0
}
}
func clearAllFields() {
txtPEField1.stringValue = ""
txtPEField2.stringValue = ""
txtPEField3.stringValue = ""
txtPECommentField.stringValue = ""
}
func initPhraseEditor() {
// InputMode combobox.
cmbPEInputModeMenu.menu?.removeAllItems()
cmbPEInputModeMenu.menu?.appendItems {
NSMenu.Item("Simplified Chinese")?.tag(0).represent(Shared.InputMode.imeModeCHS)
NSMenu.Item("Traditional Chinese")?.tag(1).represent(Shared.InputMode.imeModeCHT)
}
let toSelect = cmbPEInputModeMenu.menu?.items.first {
$0.representedObject as? Shared.InputMode == IMEApp.currentInputMode
} ?? cmbPEInputModeMenu.menu?.items.first
cmbPEInputModeMenu.select(toSelect)
// DataType combobox.
cmbPEDataTypeMenu.menu?.removeAllItems()
// NSMenu.items macOS 10.13
// property 西
cmbPEDataTypeMenu.menu?.appendItems {
for neta in vChewingLM.ReplacableUserDataType.allCases {
NSMenu.Item(verbatim: neta.localizedDescription)?.tag(cmbPEDataTypeMenu.menu?.items.count)
}
}
cmbPEDataTypeMenu.select(cmbPEDataTypeMenu.menu?.items.first)
// Buttons.
btnPEReload.title = NSLocalizedString("Reload", comment: "")
btnPEConsolidate.title = NSLocalizedString("Consolidate", comment: "")
btnPESave.title = NSLocalizedString("Save", comment: "")
btnPEAdd.title = PETerminology.AddPhrases.locAdd.localized.0
btnPEOpenExternally.title = NSLocalizedString("...", comment: "")
// DataFormatter.
txtPEField3.formatter = formatter
// Text Editor View
tfdPETextEditor.font = NSFont.systemFont(ofSize: 13)
tfdPETextEditor.isRichText = false
// Tab key targets.
tfdPETextEditor.delegate = self
txtPECommentField.nextKeyView = txtPEField1
txtPEField1.nextKeyView = txtPEField2
txtPEField2.nextKeyView = txtPEField3
txtPEField3.nextKeyView = btnPEAdd
// Delegates.
tfdPETextEditor.delegate = self
txtPECommentField.delegate = self
txtPEField1.delegate = self
txtPEField2.delegate = self
txtPEField3.delegate = self
// Tooltip.
txtPEField3.toolTip = PETerminology.TooltipTexts.weightInputBox.localized
tfdPETextEditor.toolTip = PETerminology.TooltipTexts.sampleDictionaryContent(for: selUserDataType)
// Appearance and Constraints.
btnPEAdd.bezelStyle = .rounded
btnPEReload.bezelStyle = .rounded
btnPEConsolidate.bezelStyle = .rounded
btnPESave.bezelStyle = .rounded
btnPEOpenExternally.bezelStyle = .rounded
if #available(macOS 10.10, *) {
txtPECommentField.controlSize = .small
}
txtPECommentField.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
cmbPEInputModeMenu.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
cmbPEInputModeMenu.setContentHuggingPriority(.defaultHigh, for: .horizontal)
// Key Equivalent
btnPESave.keyEquivalent = "s"
btnPESave.keyEquivalentModifierMask = .command
btnPEConsolidate.keyEquivalent = "O"
btnPEConsolidate.keyEquivalentModifierMask = [.command, .shift]
btnPEReload.keyEquivalent = "r"
btnPEReload.keyEquivalentModifierMask = .command
// Action Selectors.
cmbPEInputModeMenu.target = self
cmbPEInputModeMenu.action = #selector(inputModePEMenuDidChange(_:))
cmbPEDataTypeMenu.target = self
cmbPEDataTypeMenu.action = #selector(dataTypePEMenuDidChange(_:))
btnPEReload.target = self
btnPEReload.action = #selector(reloadPEButtonClicked(_:))
btnPEConsolidate.target = self
btnPEConsolidate.action = #selector(consolidatePEButtonClicked(_:))
btnPESave.target = self
btnPESave.action = #selector(savePEButtonClicked(_:))
btnPEConsolidate.target = self
btnPEConsolidate.action = #selector(consolidatePEButtonClicked(_:))
btnPEOpenExternally.target = self
btnPEOpenExternally.action = #selector(openExternallyPEButtonClicked(_:))
btnPEAdd.target = self
btnPEAdd.action = #selector(addPEButtonClicked(_:))
// Finally, update the entire editor UI.
updatePhraseEditor()
}
public func controlTextDidChange(_: Notification) { setPEUIControlAvailability() }
@IBAction func inputModePEMenuDidChange(_: NSPopUpButton) { updatePhraseEditor() }
@IBAction func dataTypePEMenuDidChange(_: NSPopUpButton) { updatePhraseEditor() }
@IBAction func reloadPEButtonClicked(_: NSButton) { updatePhraseEditor() }
@IBAction func consolidatePEButtonClicked(_: NSButton) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.isLoading = true
vChewingLM.LMConsolidator.consolidate(text: &self.tfdPETextEditor.string, pragma: false)
if self.selUserDataType == .thePhrases {
LMMgr.shared.tagOverrides(in: &self.tfdPETextEditor.string, mode: self.selInputMode)
}
self.isLoading = false
}
}
@IBAction func savePEButtonClicked(_: NSButton) {
let toSave = tfdPETextEditor.string
isLoading = true
tfdPETextEditor.string = NSLocalizedString("Loading…", comment: "")
let newResult = LMMgr.saveData(mode: selInputMode, type: selUserDataType, data: toSave)
tfdPETextEditor.string = newResult
isLoading = false
}
@IBAction func openExternallyPEButtonClicked(_: NSButton) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let app: FileOpenMethod = NSEvent.keyModifierFlags.contains(.option) ? .textEdit : .finder
LMMgr.shared.openPhraseFile(mode: self.selInputMode, type: self.selUserDataType, using: app)
}
}
@IBAction func addPEButtonClicked(_: NSButton) {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.txtPEField1.stringValue.removeAll { "  \t\n\r".contains($0) }
if self.selUserDataType != .theAssociates {
self.txtPEField2.stringValue.regReplace(pattern: #"( +| +| +|\t+)+"#, replaceWith: "-")
}
self.txtPEField2.stringValue.removeAll {
self.selUserDataType == .theAssociates ? "\n\r".contains($0) : "  \t\n\r".contains($0)
}
self.txtPEField3.stringValue.removeAll { !"0123456789.-".contains($0) }
self.txtPECommentField.stringValue.removeAll { "\n\r".contains($0) }
guard !self.txtPEField1.stringValue.isEmpty, !self.txtPEField2.stringValue.isEmpty else { return }
var arrResult: [String] = [self.txtPEField1.stringValue, self.txtPEField2.stringValue]
if let weightVal = Double(self.txtPEField3.stringValue), weightVal < 0 {
arrResult.append(weightVal.description)
}
if !self.txtPECommentField.stringValue.isEmpty { arrResult.append("#" + self.txtPECommentField.stringValue) }
if LMMgr.shared.checkIfPhrasePairExists(
userPhrase: self.txtPEField1.stringValue, mode: self.selInputMode, key: self.txtPEField2.stringValue
) {
arrResult.append(" #𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎")
}
if let lastChar = self.tfdPETextEditor.string.last, !"\n".contains(lastChar) {
arrResult.insert("\n", at: 0)
}
self.tfdPETextEditor.string.append(arrResult.joined(separator: " ") + "\n")
self.clearAllFields()
}
}
}
private enum PETerminology {
public enum AddPhrases: String {
case locPhrase = "Phrase"
case locReadingOrStroke = "Reading/Stroke"
case locWeight = "Weight"
case locComment = "Comment"
case locReplaceTo = "Replace to"
case locAdd = "Add"
case locInitial = "Initial"
public var localized: (String, String) {
if self == .locAdd {
let loc = PrefMgr.shared.appleLanguages[0]
return loc.prefix(2) == "zh" ? ("添入", "") : loc.prefix(2) == "ja" ? ("記入", "") : ("Add", "")
}
let rawArray = NSLocalizedString(self.rawValue, comment: "").components(separatedBy: " ")
if rawArray.isEmpty { return ("N/A", "N/A") }
let val1: String = rawArray[0]
let val2: String = (rawArray.count >= 2) ? rawArray[1] : ""
return (val1, val2)
}
}
public enum TooltipTexts: String {
case weightInputBox =
"If not filling the weight, it will be 0.0, the maximum one. An ideal weight situates in [-9.5, 0], making itself can be captured by the walking algorithm. The exception is -114.514, the disciplinary weight. The walking algorithm will ignore it unless it is the unique result."
public static func sampleDictionaryContent(for type: vChewingLM.ReplacableUserDataType) -> String {
var result = ""
switch type {
case .thePhrases:
result =
"Example:\nCandidate Reading-Reading Weight #Comment\nCandidate Reading-Reading #Comment".localized + "\n\n"
+ weightInputBox.localized
case .theFilter: result = "Example:\nCandidate Reading-Reading #Comment".localized
case .theReplacements: result = "Example:\nOldPhrase NewPhrase #Comment".localized
case .theAssociates:
result = "Example:\nInitial RestPhrase\nInitial RestPhrase1 RestPhrase2 RestPhrase3...".localized
case .theSymbols: result = "Example:\nCandidate Reading-Reading #Comment".localized
}
return result
}
public var localized: String { rawValue.localized }
}
}
@available(macOS 14.0, *)
#Preview(traits: .fixedLayout(width: 600, height: 768)) {
SettingsPanesCocoa.Phrases()
}

View File

@ -232,7 +232,7 @@ public extension SessionCtl {
NSApp.popup()
return
}
CtlPrefWindow.show()
CtlSettingsCocoa.show()
NSApp.popup()
}