SettingsCocoa // First implementation, replacing CtlPrefWindow.
This commit is contained in:
parent
b8c915dca0
commit
2465814e55
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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: "A") {
|
||||
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: "B") {
|
||||
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: "C") {
|
||||
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()
|
||||
}
|
|
@ -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: "A") {
|
||||
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: "B") {
|
||||
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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -232,7 +232,7 @@ public extension SessionCtl {
|
|||
NSApp.popup()
|
||||
return
|
||||
}
|
||||
CtlPrefWindow.show()
|
||||
CtlSettingsCocoa.show()
|
||||
NSApp.popup()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue