1.5.0 Milestone 1 // Merge GitHub PR#57 from dev/1.5.x

This commit is contained in:
ShikiSuen 2022-04-11 14:37:34 +08:00 committed by GitHub
commit e0cd85537f
53 changed files with 3636 additions and 1014 deletions

View File

@ -321,7 +321,7 @@ class AppDelegate: NSWindowController, NSApplicationDelegate {
ntfPostInstall.addButton(withTitle: NSLocalizedString("OK", comment: ""))
}
}
ntfPostInstall.beginSheetModal(for: window!) { response in
ntfPostInstall.beginSheetModal(for: window!) { _ in
self.endAppWithDelay()
}
}

View File

@ -20,7 +20,9 @@ debug:
DSTROOT = /Library/Input Methods
VC_APP_ROOT = $(DSTROOT)/vChewing.app
.PHONY: clang-format lint
.PHONY: clang-format lint batchfix format
format: batchfix clang-format lint
clang-format:
@swift-format format --in-place --configuration ./.clang-format-swift.json --recursive ./DataCompiler/
@ -30,11 +32,15 @@ clang-format:
@find ./Installer/ -iname '*.h' -o -iname '*.m' | xargs clang-format -i -style=Microsoft
@find ./Source/3rdParty/OVMandarin -iname '*.h' -o -iname '*.cpp' -o -iname '*.mm' -o -iname '*.m' | xargs clang-format -i -style=Microsoft
@find ./Source/Modules/ -iname '*.h' -o -iname '*.cpp' -o -iname '*.mm' -o -iname '*.m' | xargs clang-format -i -style=Microsoft
lint:
@swift-format lint --configuration ./.clang-format-swift.json --recursive ./DataCompiler/
@swift-format lint --configuration ./.clang-format-swift.json --recursive ./Installer/
@swift-format lint --configuration ./.clang-format-swift.json --recursive ./Source/
@swift-format lint --configuration ./.clang-format-swift.json --recursive ./UserPhraseEditor/
@swift-format lint --configuration ./.clang-format-swift.json --recursive --parallel ./DataCompiler/
@swift-format lint --configuration ./.clang-format-swift.json --recursive --parallel ./Installer/
@swift-format lint --configuration ./.clang-format-swift.json --recursive --parallel ./Source/
@swift-format lint --configuration ./.clang-format-swift.json --recursive --parallel ./UserPhraseEditor/
batchfix:
@swiftlint --fix ./
.PHONY: permission-check install-debug install-release

View File

@ -21,7 +21,7 @@ let package = Package(
dependencies: ["OpenCC"],
resources: [
.copy("benchmark"),
.copy("testcases"),
.copy("testcases")
]),
.target(
name: "copencc",
@ -66,20 +66,20 @@ let package = Package(
"deps/marisa-0.2.6/AUTHORS",
"deps/marisa-0.2.6/CMakeLists.txt",
"deps/marisa-0.2.6/COPYING.md",
"deps/marisa-0.2.6/README.md",
"deps/marisa-0.2.6/README.md"
],
sources: [
"source.cpp",
"src",
"deps/marisa-0.2.6",
"deps/marisa-0.2.6"
],
cxxSettings: [
.headerSearchPath("src"),
.headerSearchPath("deps/darts-clone"),
.headerSearchPath("deps/marisa-0.2.6/include"),
.headerSearchPath("deps/marisa-0.2.6/lib"),
.define("ENABLE_DARTS"),
]),
.define("ENABLE_DARTS")
])
],
cxxLanguageStandard: .cxx14
)

View File

@ -10,7 +10,7 @@ let testCases: [(String, ChineseConverter.Options)] = [
("s2tw", [.traditionalize, .twStandard]),
("tw2s", [.simplify, .twStandard]),
("s2twp", [.traditionalize, .twStandard, .twIdiom]),
("tw2sp", [.simplify, .twStandard, .twIdiom]),
("tw2sp", [.simplify, .twStandard, .twIdiom])
]
class OpenCCTests: XCTestCase {

View File

@ -0,0 +1,103 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import SwiftUI
@available(macOS 10.15, *)
extension Preferences {
/**
Function builder for `Preferences` components used in order to restrict types of child views to be of type `Section`.
*/
@resultBuilder
public struct SectionBuilder {
public static func buildBlock(_ sections: Section...) -> [Section] {
sections
}
}
/**
A view which holds `Preferences.Section` views and does all the alignment magic similar to `NSGridView` from AppKit.
*/
public struct Container: View {
private let sectionBuilder: () -> [Section]
private let contentWidth: Double
private let minimumLabelWidth: Double
@State private var maximumLabelWidth = 0.0
/**
Creates an instance of container component, which handles layout of stacked `Preferences.Section` views.
Custom alignment requires content width to be specified beforehand.
- Parameters:
- contentWidth: A fixed width of the container's content (excluding paddings).
- minimumLabelWidth: A minimum width for labels within this container. By default, it will fit to the largest label.
- builder: A view builder that creates `Preferences.Section`'s of this container.
*/
public init(
contentWidth: Double,
minimumLabelWidth: Double = 0,
@SectionBuilder builder: @escaping () -> [Section]
) {
self.sectionBuilder = builder
self.contentWidth = contentWidth
self.minimumLabelWidth = minimumLabelWidth
}
public var body: some View {
let sections = sectionBuilder()
return VStack(alignment: .preferenceSectionLabel) {
ForEach(0..<sections.count, id: \.self) { index in
viewForSection(sections, index: index)
}
}
.modifier(Section.LabelWidthModifier(maximumWidth: $maximumLabelWidth))
.frame(width: CGFloat(contentWidth), alignment: .leading)
.padding(.vertical, 20)
.padding(.horizontal, 30)
}
@ViewBuilder
private func viewForSection(_ sections: [Section], index: Int) -> some View {
sections[index]
if index != sections.count - 1 && sections[index].bottomDivider {
Divider()
// Strangely doesn't work without width being specified. Probably because of custom alignment.
.frame(width: CGFloat(contentWidth), height: 20)
.alignmentGuide(.preferenceSectionLabel) {
$0[.leading] + CGFloat(max(minimumLabelWidth, maximumLabelWidth))
}
}
}
}
}
/// Extension with custom alignment guide for section title labels.
@available(macOS 10.15, *)
extension HorizontalAlignment {
private enum PreferenceSectionLabelAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[HorizontalAlignment.leading]
}
}
static let preferenceSectionLabel = HorizontalAlignment(PreferenceSectionLabelAlignment.self)
}

View File

@ -0,0 +1,155 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Foundation
struct Localization {
enum Identifier {
case preferences
case preferencesEllipsized
}
private static let localizedStrings: [Identifier: [String: String]] = [
.preferences: [
"ar": "تفضيلات",
"ca": "Preferències",
"cs": "Předvolby",
"da": "Indstillinger",
"de": "Einstellungen",
"el": "Προτιμήσεις",
"en": "Preferences",
"en-AU": "Preferences",
"en-GB": "Preferences",
"es": "Preferencias",
"es-419": "Preferencias",
"fi": "Asetukset",
"fr": "Préférences",
"fr-CA": "Préférences",
"he": "העדפות",
"hi": "प्राथमिकता",
"hr": "Postavke",
"hu": "Beállítások",
"id": "Preferensi",
"it": "Preferenze",
"ja": "環境設定",
"ko": "환경설정",
"ms": "Keutamaan",
"nl": "Voorkeuren",
"no": "Valg",
"pl": "Preferencje",
"pt": "Preferências",
"pt-PT": "Preferências",
"ro": "Preferințe",
"ru": "Настройки",
"sk": "Nastavenia",
"sv": "Inställningar",
"th": "การตั้งค่า",
"tr": "Tercihler",
"uk": "Параметри",
"vi": "Tùy chọn",
"zh-CN": "偏好设置",
"zh-HK": "偏好設定",
"zh-TW": "偏好設定",
],
.preferencesEllipsized: [
"ar": "تفضيلات…",
"ca": "Preferències…",
"cs": "Předvolby…",
"da": "Indstillinger…",
"de": "Einstellungen…",
"el": "Προτιμήσεις…",
"en": "Preferences…",
"en-AU": "Preferences…",
"en-GB": "Preferences…",
"es": "Preferencias…",
"es-419": "Preferencias…",
"fi": "Asetukset…",
"fr": "Préférences…",
"fr-CA": "Préférences…",
"he": "העדפות…",
"hi": "प्राथमिकता…",
"hr": "Postavke…",
"hu": "Beállítások…",
"id": "Preferensi…",
"it": "Preferenze…",
"ja": "環境設定…",
"ko": "환경설정...",
"ms": "Keutamaan…",
"nl": "Voorkeuren…",
"no": "Valg…",
"pl": "Preferencje…",
"pt": "Preferências…",
"pt-PT": "Preferências…",
"ro": "Preferințe…",
"ru": "Настройки…",
"sk": "Nastavenia…",
"sv": "Inställningar…",
"th": "การตั้งค่า…",
"tr": "Tercihler…",
"uk": "Параметри…",
"vi": "Tùy chọn…",
"zh-CN": "偏好设置…",
"zh-HK": "偏好設定⋯",
"zh-TW": "偏好設定⋯",
],
]
/**
Returns the localized version of the given string.
- Parameter identifier: Identifier of the string to localize.
- Note: If the system's locale can't be determined, the English localization of the string will be returned.
*/
static subscript(identifier: Identifier) -> String {
// Force-unwrapped since all of the involved code is under our control.
let localizedDict = Localization.localizedStrings[identifier]!
let defaultLocalizedString = localizedDict["en"]!
// Iterate through all user-preferred languages until we find one that has a valid language code.
let preferredLocale =
Locale.preferredLanguages
.lazy
.map { Locale(identifier: $0) }
.first { $0.languageCode != nil }
?? .current
guard let languageCode = preferredLocale.languageCode else {
return defaultLocalizedString
}
// Chinese is the only language where different region codes result in different translations.
if languageCode == "zh" {
let regionCode = preferredLocale.regionCode ?? ""
if regionCode == "HK" || regionCode == "TW" {
return localizedDict["\(languageCode)-\(regionCode)"]!
} else {
// Fall back to "regular" zh-CN if neither the HK or TW region codes are found.
return localizedDict["\(languageCode)-CN"]!
}
} else {
if let localizedString = localizedDict[languageCode] {
return localizedString
}
}
return defaultLocalizedString
}
}

View File

@ -0,0 +1,112 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import SwiftUI
/// Represents a type that can be converted to `PreferencePane`.
///
/// Acts as type-eraser for `Preferences.Pane<T>`.
public protocol PreferencePaneConvertible {
/**
Convert `self` to equivalent `PreferencePane`.
*/
func asPreferencePane() -> PreferencePane
}
@available(macOS 10.15, *)
extension Preferences {
/**
Create a SwiftUI-based preference pane.
SwiftUI equivalent of the `PreferencePane` protocol.
*/
public struct Pane<Content: View>: View, PreferencePaneConvertible {
let identifier: PaneIdentifier
let title: String
let toolbarIcon: NSImage
let content: Content
public init(
identifier: PaneIdentifier,
title: String,
toolbarIcon: NSImage,
contentView: () -> Content
) {
self.identifier = identifier
self.title = title
self.toolbarIcon = toolbarIcon
self.content = contentView()
}
public var body: some View { content }
public func asPreferencePane() -> PreferencePane {
PaneHostingController(pane: self)
}
}
/**
Hosting controller enabling `Preferences.Pane` to be used alongside AppKit `NSViewController`'s.
*/
public final class PaneHostingController<Content: View>: NSHostingController<Content>, PreferencePane {
public let preferencePaneIdentifier: PaneIdentifier
public let preferencePaneTitle: String
public let toolbarItemIcon: NSImage
init(
identifier: PaneIdentifier,
title: String,
toolbarIcon: NSImage,
content: Content
) {
self.preferencePaneIdentifier = identifier
self.preferencePaneTitle = title
self.toolbarItemIcon = toolbarIcon
super.init(rootView: content)
}
public convenience init(pane: Pane<Content>) {
self.init(
identifier: pane.identifier,
title: pane.title,
toolbarIcon: pane.toolbarIcon,
content: pane.content
)
}
@available(*, unavailable)
@objc
dynamic required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
}
@available(macOS 10.15, *)
extension View {
/**
Applies font and color for a label used for describing a preference.
*/
public func preferenceDescription() -> some View {
font(.system(size: 11.0))
// TODO: Use `.foregroundStyle` when targeting macOS 12.
.foregroundColor(.secondary)
}
}

View File

@ -0,0 +1,59 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
extension Preferences {
public struct PaneIdentifier: Hashable, RawRepresentable, Codable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
}
public protocol PreferencePane: NSViewController {
var preferencePaneIdentifier: Preferences.PaneIdentifier { get }
var preferencePaneTitle: String { get }
var toolbarItemIcon: NSImage { get }
}
extension PreferencePane {
public var toolbarItemIdentifier: NSToolbarItem.Identifier {
preferencePaneIdentifier.toolbarItemIdentifier
}
public var toolbarItemIcon: NSImage { .empty }
}
extension Preferences.PaneIdentifier {
public init(_ rawValue: String) {
self.init(rawValue: rawValue)
}
public init(fromToolbarItemIdentifier itemIdentifier: NSToolbarItem.Identifier) {
self.init(rawValue: itemIdentifier.rawValue)
}
public var toolbarItemIdentifier: NSToolbarItem.Identifier {
NSToolbarItem.Identifier(rawValue)
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/// The namespace for this package.
public enum Preferences {}

View File

@ -0,0 +1,28 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
extension Preferences {
public enum Style {
case toolbarItems
case segmentedControl
}
}

View File

@ -0,0 +1,35 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
protocol PreferencesStyleController: AnyObject {
var delegate: PreferencesStyleControllerDelegate? { get set }
var isKeepingWindowCentered: Bool { get }
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier]
func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem?
func selectTab(index: Int)
}
protocol PreferencesStyleControllerDelegate: AnyObject {
func activateTab(preferenceIdentifier: Preferences.PaneIdentifier, animated: Bool)
func activateTab(index: Int, animated: Bool)
}

View File

@ -0,0 +1,261 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
final class PreferencesTabViewController: NSViewController, PreferencesStyleControllerDelegate {
private var activeTab: Int?
private var preferencePanes = [PreferencePane]()
private var style: Preferences.Style?
internal var preferencePanesCount: Int { preferencePanes.count }
private var preferencesStyleController: PreferencesStyleController!
private var isKeepingWindowCentered: Bool { preferencesStyleController.isKeepingWindowCentered }
private var toolbarItemIdentifiers: [NSToolbarItem.Identifier] {
preferencesStyleController?.toolbarItemIdentifiers() ?? []
}
var window: NSWindow! { view.window }
var isAnimated = true
var activeViewController: NSViewController? {
guard let activeTab = activeTab else {
return nil
}
return preferencePanes[activeTab]
}
override func loadView() {
view = NSView()
view.translatesAutoresizingMaskIntoConstraints = false
}
func configure(preferencePanes: [PreferencePane], style: Preferences.Style) {
self.preferencePanes = preferencePanes
self.style = style
children = preferencePanes
let toolbar = NSToolbar(identifier: "PreferencesToolbar")
toolbar.allowsUserCustomization = false
toolbar.displayMode = .iconAndLabel
toolbar.showsBaselineSeparator = true
toolbar.delegate = self
switch style {
case .segmentedControl:
preferencesStyleController = SegmentedControlStyleViewController(preferencePanes: preferencePanes)
case .toolbarItems:
preferencesStyleController = ToolbarItemStyleViewController(
preferencePanes: preferencePanes,
toolbar: toolbar,
centerToolbarItems: false
)
}
preferencesStyleController.delegate = self
// Called last so that `preferencesStyleController` can be asked for items.
window.toolbar = toolbar
}
func activateTab(preferencePane: PreferencePane, animated: Bool) {
activateTab(preferenceIdentifier: preferencePane.preferencePaneIdentifier, animated: animated)
}
func activateTab(preferenceIdentifier: Preferences.PaneIdentifier, animated: Bool) {
guard let index = (preferencePanes.firstIndex { $0.preferencePaneIdentifier == preferenceIdentifier }) else {
return activateTab(index: 0, animated: animated)
}
activateTab(index: index, animated: animated)
}
func activateTab(index: Int, animated: Bool) {
defer {
activeTab = index
preferencesStyleController.selectTab(index: index)
updateWindowTitle(tabIndex: index)
}
if activeTab == nil {
immediatelyDisplayTab(index: index)
} else {
guard index != activeTab else {
return
}
animateTabTransition(index: index, animated: animated)
}
}
func restoreInitialTab() {
if activeTab == nil {
activateTab(index: 0, animated: false)
}
}
private func updateWindowTitle(tabIndex: Int) {
window.title = {
// if preferencePanes.count > 1 {
// return preferencePanes[tabIndex].preferencePaneTitle
// } else {
// let preferences = Localization[.preferences]
// let appName = Bundle.main.appName
// return "\(appName) \(preferences)"
// }
var preferencesTitleName = NSLocalizedString("vChewing Preferences…", comment: "")
preferencesTitleName.removeLast()
return preferencesTitleName
}()
}
/// Cached constraints that pin `childViewController` views to the content view.
private var activeChildViewConstraints = [NSLayoutConstraint]()
private func immediatelyDisplayTab(index: Int) {
let toViewController = preferencePanes[index]
view.addSubview(toViewController.view)
activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds()
setWindowFrame(for: toViewController, animated: false)
}
private func animateTabTransition(index: Int, animated: Bool) {
guard let activeTab = activeTab else {
assertionFailure(
"animateTabTransition called before a tab was displayed; transition only works from one tab to another")
immediatelyDisplayTab(index: index)
return
}
let fromViewController = preferencePanes[activeTab]
let toViewController = preferencePanes[index]
// View controller animations only work on macOS 10.14 and newer.
let options: NSViewController.TransitionOptions
if #available(macOS 10.14, *) {
options = animated && isAnimated ? [.crossfade] : []
} else {
options = []
}
view.removeConstraints(activeChildViewConstraints)
transition(
from: fromViewController,
to: toViewController,
options: options
) { [self] in
activeChildViewConstraints = toViewController.view.constrainToSuperviewBounds()
}
}
override func transition(
from fromViewController: NSViewController,
to toViewController: NSViewController,
options: NSViewController.TransitionOptions = [],
completionHandler completion: (() -> Void)? = nil
) {
let isAnimated =
options
.intersection([
.crossfade,
.slideUp,
.slideDown,
.slideForward,
.slideBackward,
.slideLeft,
.slideRight,
])
.isEmpty == false
if isAnimated {
NSAnimationContext.runAnimationGroup(
{ context in
context.allowsImplicitAnimation = true
context.duration = 0.25
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
setWindowFrame(for: toViewController, animated: true)
super.transition(
from: fromViewController,
to: toViewController,
options: options,
completionHandler: completion
)
}, completionHandler: nil)
} else {
super.transition(
from: fromViewController,
to: toViewController,
options: options,
completionHandler: completion
)
}
}
private func setWindowFrame(for viewController: NSViewController, animated: Bool = false) {
guard let window = window else {
preconditionFailure()
}
let contentSize = viewController.view.fittingSize
let newWindowSize = window.frameRect(forContentRect: CGRect(origin: .zero, size: contentSize)).size
var frame = window.frame
frame.origin.y += frame.height - newWindowSize.height
frame.size = newWindowSize
if isKeepingWindowCentered {
let horizontalDiff = (window.frame.width - newWindowSize.width) / 2
frame.origin.x += horizontalDiff
}
let animatableWindow = animated ? window.animator() : window
animatableWindow.setFrame(frame, display: false)
}
}
extension PreferencesTabViewController: NSToolbarDelegate {
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarItemIdentifiers
}
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
toolbarItemIdentifiers
}
func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
style == .segmentedControl ? [] : toolbarItemIdentifiers
}
public func toolbar(
_ toolbar: NSToolbar,
itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
willBeInsertedIntoToolbar flag: Bool
) -> NSToolbarItem? {
if itemIdentifier == .flexibleSpace {
return nil
}
return preferencesStyleController.toolbarItem(
preferenceIdentifier: Preferences.PaneIdentifier(fromToolbarItemIdentifier: itemIdentifier))
}
}

View File

@ -0,0 +1,188 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
extension NSWindow.FrameAutosaveName {
static let preferences: NSWindow.FrameAutosaveName = "com.sindresorhus.Preferences.FrameAutosaveName"
}
public final class PreferencesWindowController: NSWindowController {
private let tabViewController = PreferencesTabViewController()
public var isAnimated: Bool {
get { tabViewController.isAnimated }
set {
tabViewController.isAnimated = newValue
}
}
public var hidesToolbarForSingleItem: Bool {
didSet {
updateToolbarVisibility()
}
}
private func updateToolbarVisibility() {
window?.toolbar?.isVisible =
(hidesToolbarForSingleItem == false)
|| (tabViewController.preferencePanesCount > 1)
}
public init(
preferencePanes: [PreferencePane],
style: Preferences.Style = .toolbarItems,
animated: Bool = true,
hidesToolbarForSingleItem: Bool = true
) {
precondition(!preferencePanes.isEmpty, "You need to set at least one view controller")
let window = UserInteractionPausableWindow(
contentRect: preferencePanes[0].view.bounds,
styleMask: [
.titled,
.closable,
],
backing: .buffered,
defer: true
)
self.hidesToolbarForSingleItem = hidesToolbarForSingleItem
super.init(window: window)
window.contentViewController = tabViewController
window.titleVisibility = {
switch style {
case .toolbarItems:
return .visible
case .segmentedControl:
return preferencePanes.count <= 1 ? .visible : .hidden
}
}()
if #available(macOS 11.0, *), style == .toolbarItems {
window.toolbarStyle = .preference
}
tabViewController.isAnimated = animated
tabViewController.configure(preferencePanes: preferencePanes, style: style)
updateToolbarVisibility()
}
@available(*, unavailable)
override public init(window: NSWindow?) {
fatalError("init(window:) is not supported, use init(preferences:style:animated:)")
}
@available(*, unavailable)
public required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported, use init(preferences:style:animated:)")
}
/**
Show the preferences window and brings it to front.
If you pass a `Preferences.PaneIdentifier`, the window will activate the corresponding tab.
- Parameter preferencePane: Identifier of the preference pane to display, or `nil` to show the tab that was open when the user last closed the window.
- Note: Unless you need to open a specific pane, prefer not to pass a parameter at all or `nil`.
- See `close()` to close the window again.
- See `showWindow(_:)` to show the window without the convenience of activating the app.
*/
public func show(preferencePane preferenceIdentifier: Preferences.PaneIdentifier? = nil) {
if let preferenceIdentifier = preferenceIdentifier {
tabViewController.activateTab(preferenceIdentifier: preferenceIdentifier, animated: false)
} else {
tabViewController.restoreInitialTab()
}
showWindow(self)
restoreWindowPosition()
NSApp.activate(ignoringOtherApps: true)
}
private func restoreWindowPosition() {
guard
let window = window,
let screenContainingWindow = window.screen
else {
return
}
window.setFrameOrigin(
CGPoint(
x: screenContainingWindow.visibleFrame.midX - window.frame.width / 2,
y: screenContainingWindow.visibleFrame.midY - window.frame.height / 2
))
window.setFrameUsingName(.preferences)
window.setFrameAutosaveName(.preferences)
}
}
extension PreferencesWindowController {
/// Returns the active pane if it responds to the given action.
override public func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? {
if let target = super.supplementalTarget(forAction: action, sender: sender) {
return target
}
guard let activeViewController = tabViewController.activeViewController else {
return nil
}
if let target = NSApp.target(forAction: action, to: activeViewController, from: sender) as? NSResponder,
target.responds(to: action)
{
return target
}
if let target = activeViewController.supplementalTarget(forAction: action, sender: sender) as? NSResponder,
target.responds(to: action)
{
return target
}
return nil
}
}
@available(macOS 10.15, *)
extension PreferencesWindowController {
/**
Create a preferences window from only SwiftUI-based preference panes.
*/
public convenience init(
panes: [PreferencePaneConvertible],
style: Preferences.Style = .toolbarItems,
animated: Bool = true,
hidesToolbarForSingleItem: Bool = true
) {
let preferencePanes = panes.map { $0.asPreferencePane() }
self.init(
preferencePanes: preferencePanes,
style: style,
animated: animated,
hidesToolbarForSingleItem: hidesToolbarForSingleItem
)
}
}

View File

@ -0,0 +1,138 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import SwiftUI
@available(macOS 10.15, *)
extension Preferences {
/**
Represents a section with right-aligned title and optional bottom divider.
*/
@available(macOS 10.15, *)
public struct Section: View {
/**
Preference key holding max width of section labels.
*/
private struct LabelWidthPreferenceKey: PreferenceKey {
typealias Value = Double
static var defaultValue = 0.0
static func reduce(value: inout Double, nextValue: () -> Double) {
let next = nextValue()
value = next > value ? next : value
}
}
/**
Convenience overlay for finding a label's dimensions using `GeometryReader`.
*/
private struct LabelOverlay: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: LabelWidthPreferenceKey.self, value: Double(geometry.size.width))
}
}
}
/**
Convenience modifier for applying `LabelWidthPreferenceKey`.
*/
struct LabelWidthModifier: ViewModifier {
@Binding var maximumWidth: Double
func body(content: Content) -> some View {
content
.onPreferenceChange(LabelWidthPreferenceKey.self) { newMaximumWidth in
maximumWidth = Double(newMaximumWidth)
}
}
}
public let label: AnyView
public let content: AnyView
public let bottomDivider: Bool
public let verticalAlignment: VerticalAlignment
/**
A section is responsible for controlling a single preference.
- Parameters:
- bottomDivider: Whether to place a `Divider` after the section content. Default is `false`.
- verticalAlignement: The vertical alignment of the section content.
- label: A view describing preference handled by this section.
- content: A content view.
*/
public init<Label: View, Content: View>(
bottomDivider: Bool = false,
verticalAlignment: VerticalAlignment = .firstTextBaseline,
label: @escaping () -> Label,
@ViewBuilder content: @escaping () -> Content
) {
self.label = label()
.overlay(LabelOverlay())
.eraseToAnyView() // TODO: Remove use of `AnyView`.
self.bottomDivider = bottomDivider
self.verticalAlignment = verticalAlignment
let stack = VStack(alignment: .leading) { content() }
self.content = stack.eraseToAnyView()
}
/**
Creates instance of section, responsible for controling single preference with `Text` as a `Label`.
- Parameters:
- title: A string describing preference handled by this section.
- bottomDivider: Whether to place a `Divider` after the section content. Default is `false`.
- verticalAlignement: The vertical alignment of the section content.
- content: A content view.
*/
public init<Content: View>(
title: String,
bottomDivider: Bool = false,
verticalAlignment: VerticalAlignment = .firstTextBaseline,
@ViewBuilder content: @escaping () -> Content
) {
let textLabel = {
Text(title)
.font(.system(size: 13.0))
.overlay(LabelOverlay())
.eraseToAnyView()
}
self.init(
bottomDivider: bottomDivider,
verticalAlignment: verticalAlignment,
label: textLabel,
content: content
)
}
public var body: some View {
HStack(alignment: verticalAlignment) {
label
.alignmentGuide(.preferenceSectionLabel) { $0[.trailing] }
content
Spacer()
}
}
}
}

View File

@ -0,0 +1,159 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
extension NSToolbarItem.Identifier {
static let toolbarSegmentedControlItem = Self("toolbarSegmentedControlItem")
}
extension NSUserInterfaceItemIdentifier {
static let toolbarSegmentedControl = Self("toolbarSegmentedControl")
}
final class SegmentedControlStyleViewController: NSViewController, PreferencesStyleController {
var segmentedControl: NSSegmentedControl! {
get { view as? NSSegmentedControl }
set {
view = newValue
}
}
var isKeepingWindowCentered: Bool { true }
weak var delegate: PreferencesStyleControllerDelegate?
private var preferencePanes: [PreferencePane]!
required init(preferencePanes: [PreferencePane]) {
super.init(nibName: nil, bundle: nil)
self.preferencePanes = preferencePanes
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
view = createSegmentedControl(preferencePanes: preferencePanes)
}
fileprivate func createSegmentedControl(preferencePanes: [PreferencePane]) -> NSSegmentedControl {
let segmentedControl = NSSegmentedControl()
segmentedControl.segmentCount = preferencePanes.count
segmentedControl.segmentStyle = .texturedSquare
segmentedControl.target = self
segmentedControl.action = #selector(segmentedControlAction)
segmentedControl.identifier = .toolbarSegmentedControl
if let cell = segmentedControl.cell as? NSSegmentedCell {
cell.controlSize = .regular
cell.trackingMode = .selectOne
}
let segmentSize: CGSize = {
let insets = CGSize(width: 36, height: 12)
var maxSize = CGSize.zero
for preferencePane in preferencePanes {
let title = preferencePane.preferencePaneTitle
let titleSize = title.size(
withAttributes: [
.font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular))
]
)
maxSize = CGSize(
width: max(titleSize.width, maxSize.width),
height: max(titleSize.height, maxSize.height)
)
}
return CGSize(
width: maxSize.width + insets.width,
height: maxSize.height + insets.height
)
}()
let segmentBorderWidth = CGFloat(preferencePanes.count) + 1
let segmentWidth = segmentSize.width * CGFloat(preferencePanes.count) + segmentBorderWidth
let segmentHeight = segmentSize.height
segmentedControl.frame = CGRect(x: 0, y: 0, width: segmentWidth, height: segmentHeight)
for (index, preferencePane) in preferencePanes.enumerated() {
segmentedControl.setLabel(preferencePane.preferencePaneTitle, forSegment: index)
segmentedControl.setWidth(segmentSize.width, forSegment: index)
if let cell = segmentedControl.cell as? NSSegmentedCell {
cell.setTag(index, forSegment: index)
}
}
return segmentedControl
}
@IBAction private func segmentedControlAction(_ control: NSSegmentedControl) {
delegate?.activateTab(index: control.selectedSegment, animated: true)
}
func selectTab(index: Int) {
segmentedControl.selectedSegment = index
}
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] {
[
.flexibleSpace,
.toolbarSegmentedControlItem,
.flexibleSpace,
]
}
func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? {
let toolbarItemIdentifier = preferenceIdentifier.toolbarItemIdentifier
precondition(toolbarItemIdentifier == .toolbarSegmentedControlItem)
// When the segments outgrow the window, we need to provide a group of
// NSToolbarItems with custom menu item labels and action handling for the
// context menu that pops up at the right edge of the window.
let toolbarItemGroup = NSToolbarItemGroup(itemIdentifier: toolbarItemIdentifier)
toolbarItemGroup.view = segmentedControl
toolbarItemGroup.subitems = preferencePanes.enumerated().map { index, preferenceable -> NSToolbarItem in
let item = NSToolbarItem(itemIdentifier: .init("segment-\(preferenceable.preferencePaneTitle)"))
item.label = preferenceable.preferencePaneTitle
let menuItem = NSMenuItem(
title: preferenceable.preferencePaneTitle,
action: #selector(segmentedControlMenuAction),
keyEquivalent: ""
)
menuItem.tag = index
menuItem.target = self
item.menuFormRepresentation = menuItem
return item
}
return toolbarItemGroup
}
@IBAction private func segmentedControlMenuAction(_ menuItem: NSMenuItem) {
delegate?.activateTab(index: menuItem.tag, animated: true)
}
}

View File

@ -0,0 +1,77 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
final class ToolbarItemStyleViewController: NSObject, PreferencesStyleController {
let toolbar: NSToolbar
let centerToolbarItems: Bool
let preferencePanes: [PreferencePane]
var isKeepingWindowCentered: Bool { centerToolbarItems }
weak var delegate: PreferencesStyleControllerDelegate?
init(preferencePanes: [PreferencePane], toolbar: NSToolbar, centerToolbarItems: Bool) {
self.preferencePanes = preferencePanes
self.toolbar = toolbar
self.centerToolbarItems = centerToolbarItems
}
func toolbarItemIdentifiers() -> [NSToolbarItem.Identifier] {
var toolbarItemIdentifiers = [NSToolbarItem.Identifier]()
if centerToolbarItems {
toolbarItemIdentifiers.append(.flexibleSpace)
}
for preferencePane in preferencePanes {
toolbarItemIdentifiers.append(preferencePane.toolbarItemIdentifier)
}
if centerToolbarItems {
toolbarItemIdentifiers.append(.flexibleSpace)
}
return toolbarItemIdentifiers
}
func toolbarItem(preferenceIdentifier: Preferences.PaneIdentifier) -> NSToolbarItem? {
guard let preference = (preferencePanes.first { $0.preferencePaneIdentifier == preferenceIdentifier }) else {
preconditionFailure()
}
let toolbarItem = NSToolbarItem(itemIdentifier: preferenceIdentifier.toolbarItemIdentifier)
toolbarItem.label = preference.preferencePaneTitle
toolbarItem.image = preference.toolbarItemIcon
toolbarItem.target = self
toolbarItem.action = #selector(toolbarItemSelected)
return toolbarItem
}
@IBAction private func toolbarItemSelected(_ toolbarItem: NSToolbarItem) {
delegate?.activateTab(
preferenceIdentifier: Preferences.PaneIdentifier(fromToolbarItemIdentifier: toolbarItem.itemIdentifier),
animated: true
)
}
func selectTab(index: Int) {
toolbar.selectedItemIdentifier = preferencePanes[index].toolbarItemIdentifier
}
}

View File

@ -0,0 +1,136 @@
// Copyright (c) 2018 and onwards Sindre Sorhus (MIT License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
import SwiftUI
extension NSImage {
static var empty: NSImage { NSImage(size: .zero) }
}
extension NSView {
@discardableResult
func constrainToSuperviewBounds() -> [NSLayoutConstraint] {
guard let superview = superview else {
preconditionFailure("superview has to be set first")
}
var result = [NSLayoutConstraint]()
result.append(
contentsOf: NSLayoutConstraint.constraints(
withVisualFormat: "H:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil,
views: ["subview": self]))
result.append(
contentsOf: NSLayoutConstraint.constraints(
withVisualFormat: "V:|-0-[subview]-0-|", options: .directionLeadingToTrailing, metrics: nil,
views: ["subview": self]))
translatesAutoresizingMaskIntoConstraints = false
superview.addConstraints(result)
return result
}
}
extension NSEvent {
/// Events triggered by user interaction.
static let userInteractionEvents: [NSEvent.EventType] = {
var events: [NSEvent.EventType] = [
.leftMouseDown,
.leftMouseUp,
.rightMouseDown,
.rightMouseUp,
.leftMouseDragged,
.rightMouseDragged,
.keyDown,
.keyUp,
.scrollWheel,
.tabletPoint,
.otherMouseDown,
.otherMouseUp,
.otherMouseDragged,
.gesture,
.magnify,
.swipe,
.rotate,
.beginGesture,
.endGesture,
.smartMagnify,
.quickLook,
.directTouch,
]
if #available(macOS 10.10.3, *) {
events.append(.pressure)
}
return events
}()
/// Whether the event was triggered by user interaction.
var isUserInteraction: Bool { NSEvent.userInteractionEvents.contains(type) }
}
extension Bundle {
var appName: String {
string(forInfoDictionaryKey: "CFBundleDisplayName")
?? string(forInfoDictionaryKey: "CFBundleName")
?? string(forInfoDictionaryKey: "CFBundleExecutable")
?? "<Unknown App Name>"
}
private func string(forInfoDictionaryKey key: String) -> String? {
// `object(forInfoDictionaryKey:)` prefers localized info dictionary over the regular one automatically
object(forInfoDictionaryKey: key) as? String
}
}
/// A window that allows you to disable all user interactions via `isUserInteractionEnabled`.
///
/// Used to avoid breaking animations when the user clicks too fast. Disable user interactions during animations and you're set.
class UserInteractionPausableWindow: NSWindow { // swiftlint:disable:this final_class
var isUserInteractionEnabled = true
override func sendEvent(_ event: NSEvent) {
guard isUserInteractionEnabled || !event.isUserInteraction else {
return
}
super.sendEvent(event)
}
override func responds(to selector: Selector!) -> Bool {
// Deactivate toolbar interactions from the Main Menu.
if selector == #selector(NSWindow.toggleToolbarShown(_:)) {
return false
}
return super.responds(to: selector)
}
}
@available(macOS 10.15, *)
extension View {
/**
Equivalent to `.eraseToAnyPublisher()` from the Combine framework.
*/
func eraseToAnyView() -> AnyView {
AnyView(self)
}
}

View File

@ -0,0 +1,70 @@
//
// Ref: https://stackoverflow.com/a/71058587/4162914
// License: https://creativecommons.org/licenses/by-sa/4.0/
//
import SwiftUI
// MARK: - NSComboBox
// Ref: https://stackoverflow.com/a/71058587/4162914
@available(macOS 11.0, *)
struct ComboBox: NSViewRepresentable {
// The items that will show up in the pop-up menu:
var items: [String]
// The property on our parent view that gets synced to the current
// stringValue of the NSComboBox, whether the user typed it in or
// selected it from the list:
@Binding var text: String
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
func makeNSView(context: Context) -> NSComboBox {
let comboBox = NSComboBox()
comboBox.usesDataSource = false
comboBox.completes = false
comboBox.delegate = context.coordinator
comboBox.intercellSpacing = NSSize(width: 0.0, height: 10.0)
return comboBox
}
func updateNSView(_ nsView: NSComboBox, context: Context) {
nsView.removeAllItems()
nsView.addItems(withObjectValues: items)
// ComboBox doesn't automatically select the item matching its text;
// we must do that manually. But we need the delegate to ignore that
// selection-change or we'll get a "state modified during view update;
// will cause undefined behavior" warning.
context.coordinator.ignoreSelectionChanges = true
nsView.stringValue = text
nsView.selectItem(withObjectValue: text)
context.coordinator.ignoreSelectionChanges = false
}
class Coordinator: NSObject, NSComboBoxDelegate {
var parent: ComboBox
var ignoreSelectionChanges: Bool = false
init(_ parent: ComboBox) {
self.parent = parent
}
func comboBoxSelectionDidChange(_ notification: Notification) {
if !ignoreSelectionChanges,
let box: NSComboBox = notification.object as? NSComboBox,
let newStringValue: String = box.objectValueOfSelectedItem as? String
{
parent.text = newStringValue
}
}
func controlTextDidEndEditing(_ obj: Notification) {
if let textField = obj.object as? NSTextField {
parent.text = textField.stringValue
}
}
}
}

View File

@ -27,156 +27,6 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa
import InputMethodKit
private let kCheckUpdateAutomatically = "CheckUpdateAutomatically"
private let kNextUpdateCheckDateKey = "NextUpdateCheckDate"
private let kUpdateInfoEndpointKey = "UpdateInfoEndpoint"
private let kUpdateInfoSiteKey = "UpdateInfoSite"
private let kVersionDescription = "VersionDescription"
private let kNextCheckInterval: TimeInterval = 86400.0
private let kTimeoutInterval: TimeInterval = 60.0
struct VersionUpdateReport {
var siteUrl: URL?
var currentShortVersion: String = ""
var currentVersion: String = ""
var remoteShortVersion: String = ""
var remoteVersion: String = ""
var versionDescription: String = ""
}
enum VersionUpdateApiResult {
case shouldUpdate(report: VersionUpdateReport)
case noNeedToUpdate
case ignored
}
enum VersionUpdateApiError: Error, LocalizedError {
case connectionError(message: String)
var errorDescription: String? {
switch self {
case .connectionError(let message):
return String(
format: NSLocalizedString(
"There may be no internet connection or the server failed to respond.\n\nError message: %@",
comment: ""), message)
}
}
}
struct VersionUpdateApi {
static func check(
forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>) -> Void
) -> URLSessionTask? {
guard let infoDict = Bundle.main.infoDictionary,
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
let updateInfoURL = URL(string: updateInfoURLString)
else {
return nil
}
let request = URLRequest(
url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData,
timeoutInterval: kTimeoutInterval)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
forced
? callback(
.failure(
VersionUpdateApiError.connectionError(
message: error.localizedDescription)))
: callback(.success(.ignored))
}
return
}
do {
guard
let plist = try PropertyListSerialization.propertyList(
from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any],
let remoteVersion = plist[kCFBundleVersionKey] as? String,
let infoDict = Bundle.main.infoDictionary
else {
DispatchQueue.main.async {
forced
? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored))
}
return
}
// TODO: Validate info (e.g. bundle identifier)
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
let result = currentVersion.compare(
remoteVersion, options: .numeric, range: nil, locale: nil)
if result != .orderedAscending {
DispatchQueue.main.async {
forced
? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored))
}
IME.prtDebugIntel(
"vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available."
)
return
}
IME.prtDebugIntel(
"vChewingDebug: Update // New version detected, proceeding to the next phase.")
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
let siteInfoURL = URL(string: siteInfoURLString)
else {
DispatchQueue.main.async {
forced
? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored))
}
IME.prtDebugIntel(
"vChewingDebug: Update // Failed from retrieving / parsing URL intel.")
return
}
IME.prtDebugIntel(
"vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.")
var report = VersionUpdateReport(siteUrl: siteInfoURL)
var versionDescription = ""
let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any]
if let versionDescriptions = versionDescriptions {
var locale = "en"
let supportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"]
let preferredTags = Bundle.preferredLocalizations(from: supportedLocales)
if let first = preferredTags.first {
locale = first
}
versionDescription =
versionDescriptions[locale] as? String ?? versionDescriptions["en"]
as? String ?? ""
if !versionDescription.isEmpty {
versionDescription = "\n\n" + versionDescription
}
}
report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
report.currentVersion = currentVersion
report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? ""
report.remoteVersion = remoteVersion
report.versionDescription = versionDescription
DispatchQueue.main.async {
callback(.success(.shouldUpdate(report: report)))
}
IME.prtDebugIntel("vChewingDebug: Update // Callbck Complete.")
} catch {
DispatchQueue.main.async {
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored))
}
}
}
task.resume()
return task
}
}
@objc(AppDelegate)
class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelegate,
FSEventStreamHelperDelegate
@ -220,7 +70,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega
mgrPrefs.setMissingDefaults()
// 使
if (UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) != nil) == true {
if (UserDefaults.standard.object(forKey: VersionUpdateApi.kCheckUpdateAutomatically) != nil) == true {
checkForUpdate()
}
}
@ -262,18 +112,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, ctlNonModalAlertWindowDelega
// time for update?
if !forced {
if UserDefaults.standard.bool(forKey: kCheckUpdateAutomatically) == false {
if UserDefaults.standard.bool(forKey: VersionUpdateApi.kCheckUpdateAutomatically) == false {
return
}
let now = Date()
let date = UserDefaults.standard.object(forKey: kNextUpdateCheckDateKey) as? Date ?? now
let date = UserDefaults.standard.object(forKey: VersionUpdateApi.kNextUpdateCheckDateKey) as? Date ?? now
if now.compare(date) == .orderedAscending {
return
}
}
let nextUpdateDate = Date(timeInterval: kNextCheckInterval, since: Date())
UserDefaults.standard.set(nextUpdateDate, forKey: kNextUpdateCheckDateKey)
let nextUpdateDate = Date(timeInterval: VersionUpdateApi.kNextCheckInterval, since: Date())
UserDefaults.standard.set(nextUpdateDate, forKey: VersionUpdateApi.kNextUpdateCheckDateKey)
checkTask = VersionUpdateApi.check(forced: forced) { [self] result in
defer {

View File

@ -25,43 +25,30 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa
@objc class AppleKeyboardConverter: NSObject {
@objc class func isDynamicBaseKeyboardLayoutEnabled() -> Bool {
switch mgrPrefs.basisKeyboardLayout {
case "com.apple.keylayout.ZhuyinBopomofo":
return true
case "com.apple.keylayout.ZhuyinEten":
return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingdachen":
return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingmitac":
return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingibm":
return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingseigyou":
return true
case "org.atelierInmu.vChewing.keyLayouts.vchewingeten":
return true
case "org.unknown.keylayout.vChewingDachen":
return true
case "org.unknown.keylayout.vChewingFakeSeigyou":
return true
case "org.unknown.keylayout.vChewingETen":
return true
case "org.unknown.keylayout.vChewingIBM":
return true
case "org.unknown.keylayout.vChewingMiTAC":
return true
default:
return false
}
static let arrDynamicBasicKeyLayout: [String] = [
"com.apple.keylayout.ZhuyinBopomofo",
"com.apple.keylayout.ZhuyinEten",
"org.atelierInmu.vChewing.keyLayouts.vchewingdachen",
"org.atelierInmu.vChewing.keyLayouts.vchewingmitac",
"org.atelierInmu.vChewing.keyLayouts.vchewingibm",
"org.atelierInmu.vChewing.keyLayouts.vchewingseigyou",
"org.atelierInmu.vChewing.keyLayouts.vchewingeten",
"org.unknown.keylayout.vChewingDachen",
"org.unknown.keylayout.vChewingFakeSeigyou",
"org.unknown.keylayout.vChewingETen",
"org.unknown.keylayout.vChewingIBM",
"org.unknown.keylayout.vChewingMiTAC",
]
@objc class func isDynamicBasicKeyboardLayoutEnabled() -> Bool {
return AppleKeyboardConverter.arrDynamicBasicKeyLayout.contains(mgrPrefs.basicKeyboardLayout)
}
// Apple
@objc class func cnvApple2ABC(_ charCode: UniChar) -> UniChar {
var charCode = charCode
// OVMandarin OVMandarin
if self.isDynamicBaseKeyboardLayoutEnabled() {
if self.isDynamicBasicKeyboardLayoutEnabled() {
// Apple
switch mgrPrefs.basisKeyboardLayout {
switch mgrPrefs.basicKeyboardLayout {
case "com.apple.keylayout.ZhuyinBopomofo":
do {
if charCode == 97 { charCode = UniChar(65) }
@ -186,7 +173,7 @@ import Cocoa
// Alt
if charCode == 8212 { charCode = UniChar(45) }
// Apple
if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" {
if mgrPrefs.basicKeyboardLayout == "com.apple.keylayout.ZhuyinEten" {
if charCode == 65343 { charCode = UniChar(95) }
if charCode == 65306 { charCode = UniChar(58) }
if charCode == 65311 { charCode = UniChar(63) }
@ -199,9 +186,9 @@ import Cocoa
@objc class func cnvStringApple2ABC(_ strProcessed: String) -> String {
var strProcessed = strProcessed
if self.isDynamicBaseKeyboardLayoutEnabled() {
if self.isDynamicBasicKeyboardLayoutEnabled() {
// Apple
switch mgrPrefs.basisKeyboardLayout {
switch mgrPrefs.basicKeyboardLayout {
case "com.apple.keylayout.ZhuyinBopomofo":
do {
if strProcessed == "a" { strProcessed = "A" }
@ -326,7 +313,7 @@ import Cocoa
// Alt
if strProcessed == "" { strProcessed = "-" }
// Apple
if mgrPrefs.basisKeyboardLayout == "com.apple.keylayout.ZhuyinEten" {
if mgrPrefs.basicKeyboardLayout == "com.apple.keylayout.ZhuyinEten" {
if strProcessed == "_" { strProcessed = "_" }
if strProcessed == "" { strProcessed = ":" }
if strProcessed == "" { strProcessed = "?" }

View File

@ -243,7 +243,7 @@ class InputState: NSObject {
self.markerIndex = markerIndex
let begin = min(cursorIndex, markerIndex)
let end = max(cursorIndex, markerIndex)
markedRange = NSMakeRange(Int(begin), Int(end - begin))
markedRange = NSRange(location: Int(begin), length: Int(end - begin))
self.readings = readings
super.init(composingBuffer: composingBuffer, cursorIndex: cursorIndex)
}

View File

@ -46,9 +46,7 @@ static double FindHighestScore(const std::vector<Gramambular::NodeAnchor> &nodes
{
double score = ni->node->highestUnigramScore();
if (score > highestScore)
{
highestScore = score;
}
}
return highestScore + epsilon;
}
@ -98,14 +96,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
- (BOOL)isBuilderEmpty
{
if (_builder->grid().width() == 0)
{
return YES;
}
else
{
return NO;
}
return (_builder->grid().width() == 0);
}
- (void)setInputMode:(NSString *)value
@ -114,25 +105,17 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
vChewing::LMInstantiator *newLanguageModel;
vChewing::UserOverrideModel *newUserOverrideModel;
if ([value isKindOfClass:[NSString class]] && [value isEqual:imeModeCHS])
{
newInputMode = imeModeCHS;
newLanguageModel = [mgrLangModel lmCHS];
newUserOverrideModel = [mgrLangModel userOverrideModelCHS];
}
else
{
newInputMode = imeModeCHT;
newLanguageModel = [mgrLangModel lmCHT];
newUserOverrideModel = [mgrLangModel userOverrideModelCHT];
}
BOOL isCHS = [value isKindOfClass:[NSString class]] && [value isEqual:imeModeCHS];
newInputMode = isCHS ? imeModeCHS : imeModeCHT;
newLanguageModel = isCHS ? [mgrLangModel lmCHS] : [mgrLangModel lmCHT];
newUserOverrideModel = isCHS ? [mgrLangModel userOverrideModelCHS] : [mgrLangModel userOverrideModelCHT];
// Report the current Input Mode to ctlInputMethod:
ctlInputMethod.currentInputMode = newInputMode;
// Synchronize the Preference Setting "setPhraseReplacementEnabled" to the new LM.
// Synchronize the sub-languageModel state settings to the new LM.
newLanguageModel->setPhraseReplacementEnabled(mgrPrefs.phraseReplacementEnabled);
// Also other sub language models:
newLanguageModel->setSymbolEnabled(mgrPrefs.symbolInputEnabled);
newLanguageModel->setCNSEnabled(mgrPrefs.cns11643Enabled);
@ -151,24 +134,16 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
}
if (!_bpmfReadingBuffer->isEmpty())
{
_bpmfReadingBuffer->clear();
}
}
}
- (void)dealloc
{
// clean up everything
{ // clean up everything
if (_bpmfReadingBuffer)
{
delete _bpmfReadingBuffer;
}
if (_builder)
{
delete _builder;
}
}
- (instancetype)init
@ -196,36 +171,35 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
- (void)syncWithPreferences
{
NSInteger layout = mgrPrefs.keyboardLayout;
switch (layout)
switch (mgrPrefs.mandarinParser)
{
case KeyboardLayoutOfStandard:
case MandarinParserOfStandard:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::StandardLayout());
break;
case KeyboardLayoutOfEten:
case MandarinParserOfEten:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::ETenLayout());
break;
case KeyboardLayoutOfHsu:
case MandarinParserOfHsu:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::HsuLayout());
break;
case KeyboardLayoutOfEen26:
case MandarinParserOfEen26:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::ETen26Layout());
break;
case KeyboardLayoutOfIBM:
case MandarinParserOfIBM:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::IBMLayout());
break;
case KeyboardLayoutOfMiTAC:
case MandarinParserOfMiTAC:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::MiTACLayout());
break;
case KeyboardLayoutOfFakeSeigyou:
case MandarinParserOfFakeSeigyou:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::FakeSeigyouLayout());
break;
case KeyboardLayoutOfHanyuPinyin:
case MandarinParserOfHanyuPinyin:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::HanyuPinyinLayout());
break;
default:
_bpmfReadingBuffer->setKeyboardLayout(Mandarin::BopomofoKeyboardLayout::StandardLayout());
mgrPrefs.keyboardLayout = KeyboardLayoutOfStandard;
mgrPrefs.mandarinParser = MandarinParserOfStandard;
}
}
@ -241,21 +215,16 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
// in the user override model.
BOOL addToOverrideModel = YES;
if (selectedNode.spanningLength != [value count])
{
addToOverrideModel = NO;
}
if (addToOverrideModel)
{
double score = selectedNode.node->scoreForCandidate(stringValue);
if (score <= -12)
{ // 威注音的 SymbolLM 的 Score 是 -12。
if (score <= -12) // 威注音的 SymbolLM 的 Score 是 -12。
addToOverrideModel = NO;
}
}
if (addToOverrideModel)
{
_userOverrideModel->observe(_walkedNodes, cursorIndex, stringValue, [[NSDate date] timeIntervalSince1970]);
}
}
[self _walk];
@ -265,15 +234,11 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
for (auto node : _walkedNodes)
{
if (nextPosition >= cursorIndex)
{
break;
}
nextPosition += node.spanningLength;
}
if (nextPosition <= _builder->length())
{
_builder->setCursorIndex(nextPosition);
}
}
}
@ -284,13 +249,13 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
_walkedNodes.clear();
}
- (std::string)_currentLayout
- (std::string)_currentMandarinParser
{
NSString *keyboardLayoutName = mgrPrefs.keyboardLayoutName;
std::string layout = std::string(keyboardLayoutName.UTF8String) + std::string("_");
return layout;
return std::string(mgrPrefs.mandarinParserName.UTF8String) + std::string("_");
}
// MARK: - Handling Input
- (BOOL)handleInput:(keyParser *)input
state:(InputState *)inState
stateCallback:(void (^)(InputState *))stateCallback
@ -302,18 +267,14 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
// if the inputText is empty, it's a function key combination, we ignore it
if (!input.inputText.length)
{
return NO;
}
// if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it
BOOL isFunctionKey =
([input isCommandHold] || [input isOptionHotKey] || [input isNumericPad]) || [input isControlHotKey];
if (![state isKindOfClass:[InputStateNotEmpty class]] &&
![state isKindOfClass:[InputStateAssociatedPhrases class]] && isFunctionKey)
{
return NO;
}
// Caps Lock processing: if Caps Lock is ON, temporarily disable bopomofo.
// Note: Alphanumerical mode processing.
@ -331,16 +292,12 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
// When shift is pressed, don't do further processing, since it outputs capital letter anyway.
if ([input isShiftHold])
{
return NO;
}
// if ASCII but not printable, don't use insertText:replacementRange: as many apps don't handle non-ASCII char
// insertions.
if (charCode < 0x80 && !isprint(charCode))
{
return NO;
}
// commit everything in the buffer.
InputStateCommitting *committingState =
@ -369,9 +326,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
// MARK: Handle Candidates
if ([state isKindOfClass:[InputStateChoosingCandidate class]])
{
return [self _handleCandidateState:state input:input stateCallback:stateCallback errorCallback:errorCallback];
}
// MARK: Handle Associated Phrases
if ([state isKindOfClass:[InputStateAssociatedPhrases class]])
@ -381,9 +336,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
stateCallback:stateCallback
errorCallback:errorCallback];
if (result)
{
return YES;
}
state = [[InputStateEmpty alloc] init];
stateCallback(state);
}
@ -396,9 +349,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
input:input
stateCallback:stateCallback
errorCallback:errorCallback])
{
return YES;
}
state = [marking convertToInputting];
stateCallback(state);
}
@ -426,14 +377,14 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
}
// see if we have composition if Enter/Space is hit and buffer is not empty
// this is bit-OR'ed so that the tone marker key is also taken into account
// we use "OR" conditioning so that the tone marker key is also taken into account
composeReading |= (!_bpmfReadingBuffer->isEmpty() && ([input isSpace] || [input isEnter]));
if (composeReading)
{
// combine the reading
std::string reading = _bpmfReadingBuffer->syllable().composedString();
// see if we have a unigram for this
// see if we have an unigram for this
if (!_languageModel->hasUnigramsForKey(reading))
{
[IME prtDebugIntel:@"B49C0979"];
@ -493,9 +444,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
(InputStateAssociatedPhrases *)[self buildAssociatePhraseStateWithKey:text
useVerticalMode:input.useVerticalMode];
if (associatedPhrases)
{
stateCallback(associatedPhrases);
}
else
{
InputStateEmpty *empty = [[InputStateEmpty alloc] init];
@ -504,9 +453,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
}
}
else
{
stateCallback(choosingCandidates);
}
}
// and tells the client that the key is consumed
@ -558,82 +505,54 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
// MARK: Esc
if ([input isESC])
{
return [self _handleEscWithState:state stateCallback:stateCallback errorCallback:errorCallback];
}
// MARK: Cursor backward
if ([input isCursorBackward] || emacsKey == vChewingEmacsKeyBackward)
{
return [self _handleBackwardWithState:state
input:input
stateCallback:stateCallback
errorCallback:errorCallback];
}
// MARK: Cursor forward
if ([input isCursorForward] || emacsKey == vChewingEmacsKeyForward)
{
return [self _handleForwardWithState:state input:input stateCallback:stateCallback errorCallback:errorCallback];
}
// MARK: Home
if ([input isHome] || emacsKey == vChewingEmacsKeyHome)
{
return [self _handleHomeWithState:state stateCallback:stateCallback errorCallback:errorCallback];
}
// MARK: End
if ([input isEnd] || emacsKey == vChewingEmacsKeyEnd)
{
return [self _handleEndWithState:state stateCallback:stateCallback errorCallback:errorCallback];
}
// MARK: Ctrl+PgLf or Shift+PgLf
if ([input isControlHold] || [input isShiftHold])
{
if ([input isOptionHold] && [input isLeft])
{
return [self _handleHomeWithState:state stateCallback:stateCallback errorCallback:errorCallback];
}
}
if (([input isControlHold] || [input isShiftHold]) && ([input isOptionHold] && [input isLeft]))
return [self _handleHomeWithState:state stateCallback:stateCallback errorCallback:errorCallback];
// MARK: Ctrl+PgRt or Shift+PgRt
if ([input isControlHold] || [input isShiftHold])
{
if ([input isOptionHold] && [input isRight])
{
return [self _handleEndWithState:state stateCallback:stateCallback errorCallback:errorCallback];
}
}
if (([input isControlHold] || [input isShiftHold]) && ([input isOptionHold] && [input isRight]))
return [self _handleEndWithState:state stateCallback:stateCallback errorCallback:errorCallback];
// MARK: AbsorbedArrowKey
if ([input isAbsorbedArrowKey] || [input isExtraChooseCandidateKey] || [input isExtraChooseCandidateKeyReverse])
{
return [self _handleAbsorbedArrowKeyWithState:state stateCallback:stateCallback errorCallback:errorCallback];
}
// MARK: Backspace
if ([input isBackSpace])
{
return [self _handleBackspaceWithState:state stateCallback:stateCallback errorCallback:errorCallback];
}
// MARK: Delete
if ([input isDelete] || emacsKey == vChewingEmacsKeyDelete)
{
return [self _handleDeleteWithState:state stateCallback:stateCallback errorCallback:errorCallback];
}
// MARK: Enter
if ([input isEnter])
{
return ([input isControlHold] && [input isCommandHold])
? [self _handleCtrlCommandEnterWithState:state
stateCallback:stateCallback
errorCallback:errorCallback]
: [self _handleEnterWithState:state stateCallback:stateCallback errorCallback:errorCallback];
}
// MARK: Punctuation list
if ([input isSymbolMenuPhysicalKey] && ![input isShiftHold])
@ -679,32 +598,24 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
// if nothing is matched, see if it's a punctuation key for current layout.
std::string punctuationNamePrefix;
if ([input isOptionHold])
{
punctuationNamePrefix = std::string("_alt_punctuation_");
}
else if ([input isControlHold])
{
punctuationNamePrefix = std::string("_ctrl_punctuation_");
}
else if (mgrPrefs.halfWidthPunctuationEnabled)
{
punctuationNamePrefix = std::string("_half_punctuation_");
}
else
{
punctuationNamePrefix = std::string("_punctuation_");
}
std::string layout = [self _currentLayout];
std::string customPunctuation = punctuationNamePrefix + layout + std::string(1, (char)charCode);
std::string parser = [self _currentMandarinParser];
std::string customPunctuation = punctuationNamePrefix + parser + std::string(1, (char)charCode);
if ([self _handlePunctuation:customPunctuation
state:state
usingVerticalMode:input.useVerticalMode
stateCallback:stateCallback
errorCallback:errorCallback])
{
return YES;
}
// if nothing is matched, see if it's a punctuation key.
std::string punctuation = punctuationNamePrefix + std::string(1, (char)charCode);
@ -713,9 +624,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
usingVerticalMode:input.useVerticalMode
stateCallback:stateCallback
errorCallback:errorCallback])
{
return YES;
}
// Lukhnos 這裡的處理反而會使得 Apple 倚天注音動態鍵盤佈局「敲不了半形大寫英文」的缺點曝露無疑,所以注釋掉。
// 至於他試圖用這種處理來解決的上游 UPR293
@ -728,9 +637,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
usingVerticalMode:input.useVerticalMode
stateCallback:stateCallback
errorCallback:errorCallback])
{
return YES;
}
}
// still nothing, then we update the composing buffer (some app has strange behavior if we don't do this, "thinking"
@ -754,9 +661,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (![state isKindOfClass:[InputStateInputting class]])
{
return NO;
}
BOOL escToClearInputBufferEnabled = mgrPrefs.escToCleanInputBuffer;
@ -800,9 +705,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (![state isKindOfClass:[InputStateInputting class]])
{
return NO;
}
if (!_bpmfReadingBuffer->isEmpty())
{
@ -859,9 +762,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (![state isKindOfClass:[InputStateInputting class]])
{
return NO;
}
if (!_bpmfReadingBuffer->isEmpty())
{
@ -917,9 +818,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (![state isKindOfClass:[InputStateInputting class]])
{
return NO;
}
if (!_bpmfReadingBuffer->isEmpty())
{
@ -950,9 +849,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (![state isKindOfClass:[InputStateInputting class]])
{
return NO;
}
if (!_bpmfReadingBuffer->isEmpty())
{
@ -983,9 +880,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (![state isKindOfClass:[InputStateInputting class]])
{
return NO;
}
if (!_bpmfReadingBuffer->isEmpty())
{
@ -1001,9 +896,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (![state isKindOfClass:[InputStateInputting class]])
{
return NO;
}
if (_bpmfReadingBuffer->isEmpty())
{
@ -1021,9 +914,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
}
}
else
{
_bpmfReadingBuffer->backspace();
}
if (_bpmfReadingBuffer->isEmpty() && !_builder->length())
{
@ -1043,9 +934,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (![state isKindOfClass:[InputStateInputting class]])
{
return NO;
}
if (_bpmfReadingBuffer->isEmpty())
{
@ -1106,9 +995,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (![state isKindOfClass:[InputStateInputting class]])
{
return NO;
}
[self clear];
@ -1128,9 +1015,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback:(void (^)(void))errorCallback
{
if (!_languageModel->hasUnigramsForKey(customPunctuation))
{
return NO;
}
NSString *poppedText;
if (_bpmfReadingBuffer->isEmpty())
@ -1165,9 +1050,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
stateCallback(empty);
}
else
{
stateCallback(candidateState);
}
}
return YES;
}
@ -1218,9 +1101,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
stateCallback(inputting);
}
else
{
stateCallback(marking);
}
}
else
{
@ -1249,9 +1130,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
stateCallback(inputting);
}
else
{
stateCallback(marking);
}
}
else
{
@ -1324,7 +1203,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
if ([input isTab])
{
BOOL updated =
mgrPrefs.specifyTabKeyBehavior
mgrPrefs.specifyShiftTabKeyBehavior
? ([input isShiftHold] ? [ctlCandidateCurrent showPreviousPage] : [ctlCandidateCurrent showNextPage])
: ([input isShiftHold] ? [ctlCandidateCurrent highlightPreviousCandidate]
: [ctlCandidateCurrent highlightNextCandidate]);
@ -1338,7 +1217,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
if ([input isSpace])
{
BOOL updated = mgrPrefs.specifySpaceKeyBehavior
BOOL updated = mgrPrefs.specifyShiftSpaceKeyBehavior
? ([input isShiftHold] ? [ctlCandidateCurrent highlightNextCandidate]
: [ctlCandidateCurrent showNextPage])
: ([input isShiftHold] ? [ctlCandidateCurrent showNextPage]
@ -1495,9 +1374,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback();
}
else
{
ctlCandidateCurrent.selectedCandidateIndex = 0;
}
return YES;
}
@ -1505,18 +1382,12 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
NSArray *candidates;
if ([state isKindOfClass:[InputStateChoosingCandidate class]])
{
candidates = [(InputStateChoosingCandidate *)state candidates];
}
else if ([state isKindOfClass:[InputStateAssociatedPhrases class]])
{
candidates = [(InputStateAssociatedPhrases *)state candidates];
}
if (!candidates)
{
return NO;
}
if (([input isEnd] || input.emacsKey == vChewingEmacsKeyEnd) && candidates.count > 0)
{
@ -1526,30 +1397,23 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
errorCallback();
}
else
{
ctlCandidateCurrent.selectedCandidateIndex = candidates.count - 1;
}
return YES;
}
if ([state isKindOfClass:[InputStateAssociatedPhrases class]])
{
if (![input isShiftHold])
{
return NO;
}
}
NSInteger index = NSNotFound;
NSString *match;
if ([state isKindOfClass:[InputStateAssociatedPhrases class]])
{
match = input.inputTextIgnoringModifiers;
}
else
{
match = inputText;
}
for (NSUInteger j = 0, c = [ctlCandidateCurrent.keyLabels count]; j < c; j++)
{
@ -1572,31 +1436,22 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
}
if ([state isKindOfClass:[InputStateAssociatedPhrases class]])
{
return NO;
}
if (mgrPrefs.useSCPCTypingMode)
{
std::string layout = [self _currentLayout];
std::string punctuationNamePrefix;
if ([input isOptionHold])
{
punctuationNamePrefix = std::string("_alt_punctuation_");
}
else if ([input isControlHold])
{
punctuationNamePrefix = std::string("_ctrl_punctuation_");
}
else if (mgrPrefs.halfWidthPunctuationEnabled)
{
punctuationNamePrefix = std::string("_half_punctuation_");
}
else
{
punctuationNamePrefix = std::string("_punctuation_");
}
std::string customPunctuation = punctuationNamePrefix + layout + std::string(1, (char)charCode);
std::string parser = [self _currentMandarinParser];
std::string customPunctuation = punctuationNamePrefix + parser + std::string(1, (char)charCode);
std::string punctuation = punctuationNamePrefix + std::string(1, (char)charCode);
BOOL shouldAutoSelectCandidate = _bpmfReadingBuffer->isValidKey((char)charCode) ||
@ -1607,9 +1462,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
{
std::string letter = std::string("_letter_") + std::string(1, (char)charCode);
if (_languageModel->hasUnigramsForKey(letter))
{
shouldAutoSelectCandidate = YES;
}
}
if (shouldAutoSelectCandidate)
@ -1746,7 +1599,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
- (void)_walk
{
// retrieve the most likely trellis, i.e. a Maximum Likelihood Estimation
// of the best possible Mandarain characters given the input syllables,
// of the best possible Mandarin characters given the input syllables,
// using the Viterbi algorithm implemented in the Gramambular library
Gramambular::Walker walker(&_builder->grid());
@ -1813,9 +1666,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
const std::vector<Gramambular::KeyValuePair> &candidates = (*ni).node->candidates();
for (std::vector<Gramambular::KeyValuePair>::const_iterator ci = candidates.begin(), ce = candidates.end();
ci != ce; ++ci)
{
[candidatesArray addObject:[NSString stringWithUTF8String:(*ci).value.c_str()]];
}
}
InputStateChoosingCandidate *state =
@ -1831,9 +1682,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
size_t cursorIndex = _builder->cursorIndex();
// MS Phonetics IME style, phrase is *after* the cursor, i.e. cursor is always *before* the phrase
if ((mgrPrefs.selectPhraseAfterCursorAsCandidate && (cursorIndex < _builder->length())) || !cursorIndex)
{
++cursorIndex;
}
return cursorIndex;
}
@ -1843,9 +1692,7 @@ static NSString *const kGraphVizOutputfile = @"/tmp/vChewing-visualization.dot";
NSMutableArray *readingsArray = [[NSMutableArray alloc] init];
std::vector<std::string> v = _builder->readings();
for (std::vector<std::string>::iterator it_i = v.begin(); it_i != v.end(); ++it_i)
{
[readingsArray addObject:[NSString stringWithUTF8String:it_i->c_str()]];
}
return readingsArray;
}

View File

@ -47,7 +47,7 @@ public class FSEventStreamHelper: NSObject {
self.dispatchQueue = queue
}
private var stream: FSEventStreamRef? = nil
private var stream: FSEventStreamRef?
public func start() -> Bool {
if stream != nil {
@ -59,7 +59,7 @@ public class FSEventStreamHelper: NSObject {
let stream = FSEventStreamCreate(
nil,
{
(stream, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds) in
(_, clientCallBackInfo, eventCount, eventPaths, eventFlags, eventIds) in
let helper = Unmanaged<FSEventStreamHelper>.fromOpaque(clientCallBackInfo!)
.takeUnretainedValue()
let pathsBase = eventPaths.assumingMemoryBound(to: UnsafePointer<CChar>.self)

View File

@ -22,10 +22,11 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Carbon
import Cocoa
@objc public class IME: NSObject {
static let arrSupportedLocales = ["en", "zh-Hant", "zh-Hans", "ja"]
static let dlgOpenPath = NSOpenPanel()
// MARK: -
@ -84,7 +85,9 @@ import Cocoa
// MARK: - Open a phrase data file.
static func openPhraseFile(userFileAt path: String) {
func checkIfUserFilesExist() -> Bool {
if !mgrLangModel.checkIfUserLanguageModelFilesExist() {
if !mgrLangModel.chkUserLMFilesExist(InputMode.imeModeCHS)
|| !mgrLangModel.chkUserLMFilesExist(InputMode.imeModeCHT)
{
let content = String(
format: NSLocalizedString(
"Please check the permission at \"%@\".", comment: ""),
@ -224,4 +227,120 @@ import Cocoa
return 0
}
// MARK: - ASCII
struct CarbonKeyboardLayout {
var strName: String = ""
var strValue: String = ""
}
static let arrWhitelistedKeyLayoutsASCII: [String] = [
"com.apple.keylayout.ABC",
"com.apple.keylayout.ABC-AZERTY",
"com.apple.keylayout.ABC-QWERTZ",
"com.apple.keylayout.British",
"com.apple.keylayout.Colemak",
"com.apple.keylayout.Dvorak",
"com.apple.keylayout.Dvorak-Left",
"com.apple.keylayout.DVORAK-QWERTYCMD",
"com.apple.keylayout.Dvorak-Right",
]
static var arrEnumerateSystemKeyboardLayouts: [IME.CarbonKeyboardLayout] {
// macOS
var arrKeyLayouts: [IME.CarbonKeyboardLayout] = []
arrKeyLayouts += [
IME.CarbonKeyboardLayout.init(
strName: NSLocalizedString("Apple Chewing - Dachen", comment: ""),
strValue: "com.apple.keylayout.ZhuyinBopomofo"),
IME.CarbonKeyboardLayout.init(
strName: NSLocalizedString("Apple Chewing - Eten Traditional", comment: ""),
strValue: "com.apple.keylayout.ZhuyinEten"),
]
// ASCII
var arrKeyLayoutsMACV: [IME.CarbonKeyboardLayout] = []
var arrKeyLayoutsASCII: [IME.CarbonKeyboardLayout] = []
let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
for source in list {
if let ptrCategory = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) {
let category = Unmanaged<CFString>.fromOpaque(ptrCategory).takeUnretainedValue()
if category != kTISCategoryKeyboardInputSource {
continue
}
} else {
continue
}
if let ptrASCIICapable = TISGetInputSourceProperty(
source, kTISPropertyInputSourceIsASCIICapable)
{
let asciiCapable = Unmanaged<CFBoolean>.fromOpaque(ptrASCIICapable)
.takeUnretainedValue()
if asciiCapable != kCFBooleanTrue {
continue
}
} else {
continue
}
if let ptrSourceType = TISGetInputSourceProperty(source, kTISPropertyInputSourceType) {
let sourceType = Unmanaged<CFString>.fromOpaque(ptrSourceType).takeUnretainedValue()
if sourceType != kTISTypeKeyboardLayout {
continue
}
} else {
continue
}
guard let ptrSourceID = TISGetInputSourceProperty(source, kTISPropertyInputSourceID),
let localizedNamePtr = TISGetInputSourceProperty(source, kTISPropertyLocalizedName)
else {
continue
}
let sourceID = String(Unmanaged<CFString>.fromOpaque(ptrSourceID).takeUnretainedValue())
let localizedName = String(
Unmanaged<CFString>.fromOpaque(localizedNamePtr).takeUnretainedValue())
if sourceID.contains("vChewing") {
arrKeyLayoutsMACV += [
IME.CarbonKeyboardLayout.init(strName: localizedName, strValue: sourceID)
]
}
if IME.arrWhitelistedKeyLayoutsASCII.contains(sourceID) {
arrKeyLayoutsASCII += [
IME.CarbonKeyboardLayout.init(strName: localizedName, strValue: sourceID)
]
}
}
arrKeyLayouts += arrKeyLayoutsMACV
arrKeyLayouts += arrKeyLayoutsASCII
return arrKeyLayouts
}
}
// MARK: - Root Extensions
// Extend the RangeReplaceableCollection to allow it clean duplicated characters.
// Ref: https://stackoverflow.com/questions/25738817/
extension RangeReplaceableCollection where Element: Hashable {
var charDeDuplicate: Self {
var set = Set<Element>()
return filter { set.insert($0).inserted }
}
}
// MARK: - Error Extension
extension String: Error {}
extension String: LocalizedError {
public var errorDescription: String? { return self }
}
// MARK: - Ensuring trailing slash of a string:
extension String {
mutating func ensureTrailingSlash() {
if !self.hasSuffix("/") {
self += "/"
}
}
}

View File

@ -0,0 +1,175 @@
// Copyright (c) 2011 and onwards The OpenVanilla Project (MIT License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. 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 above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
struct VersionUpdateReport {
var siteUrl: URL?
var currentShortVersion: String = ""
var currentVersion: String = ""
var remoteShortVersion: String = ""
var remoteVersion: String = ""
var versionDescription: String = ""
}
enum VersionUpdateApiResult {
case shouldUpdate(report: VersionUpdateReport)
case noNeedToUpdate
case ignored
}
enum VersionUpdateApiError: Error, LocalizedError {
case connectionError(message: String)
var errorDescription: String? {
switch self {
case .connectionError(let message):
return String(
format: NSLocalizedString(
"There may be no internet connection or the server failed to respond.\n\nError message: %@",
comment: ""), message)
}
}
}
struct VersionUpdateApi {
static let kCheckUpdateAutomatically = "CheckUpdateAutomatically"
static let kNextUpdateCheckDateKey = "NextUpdateCheckDate"
static let kUpdateInfoEndpointKey = "UpdateInfoEndpoint"
static let kUpdateInfoSiteKey = "UpdateInfoSite"
static let kVersionDescription = "VersionDescription"
static let kNextCheckInterval: TimeInterval = 86400.0
static let kTimeoutInterval: TimeInterval = 60.0
static func check(
forced: Bool, callback: @escaping (Result<VersionUpdateApiResult, Error>) -> Void
) -> URLSessionTask? {
guard let infoDict = Bundle.main.infoDictionary,
let updateInfoURLString = infoDict[kUpdateInfoEndpointKey] as? String,
let updateInfoURL = URL(string: updateInfoURLString)
else {
return nil
}
let request = URLRequest(
url: updateInfoURL, cachePolicy: .reloadIgnoringLocalCacheData,
timeoutInterval: kTimeoutInterval)
let task = URLSession.shared.dataTask(with: request) { data, _, error in
if let error = error {
DispatchQueue.main.async {
forced
? callback(
.failure(
VersionUpdateApiError.connectionError(
message: error.localizedDescription)))
: callback(.success(.ignored))
}
return
}
do {
guard
let plist = try PropertyListSerialization.propertyList(
from: data ?? Data(), options: [], format: nil) as? [AnyHashable: Any],
let remoteVersion = plist[kCFBundleVersionKey] as? String,
let infoDict = Bundle.main.infoDictionary
else {
DispatchQueue.main.async {
forced
? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored))
}
return
}
// TODO: Validate info (e.g. bundle identifier)
// TODO: Use HTML to display change log, need a new key like UpdateInfoChangeLogURL for this
let currentVersion = infoDict[kCFBundleVersionKey as String] as? String ?? ""
let result = currentVersion.compare(
remoteVersion, options: .numeric, range: nil, locale: nil)
if result != .orderedAscending {
DispatchQueue.main.async {
forced
? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored))
}
IME.prtDebugIntel(
"vChewingDebug: Update // Order is not Ascending, assuming that there's no new version available."
)
return
}
IME.prtDebugIntel(
"vChewingDebug: Update // New version detected, proceeding to the next phase.")
guard let siteInfoURLString = plist[kUpdateInfoSiteKey] as? String,
let siteInfoURL = URL(string: siteInfoURLString)
else {
DispatchQueue.main.async {
forced
? callback(.success(.noNeedToUpdate))
: callback(.success(.ignored))
}
IME.prtDebugIntel(
"vChewingDebug: Update // Failed from retrieving / parsing URL intel.")
return
}
IME.prtDebugIntel(
"vChewingDebug: Update // URL intel retrieved, proceeding to the next phase.")
var report = VersionUpdateReport(siteUrl: siteInfoURL)
var versionDescription = ""
let versionDescriptions = plist[kVersionDescription] as? [AnyHashable: Any]
if let versionDescriptions = versionDescriptions {
var locale = "en"
let preferredTags = Bundle.preferredLocalizations(from: IME.arrSupportedLocales)
if let first = preferredTags.first {
locale = first
}
versionDescription =
versionDescriptions[locale] as? String ?? versionDescriptions["en"]
as? String ?? ""
if !versionDescription.isEmpty {
versionDescription = "\n\n" + versionDescription
}
}
report.currentShortVersion = infoDict["CFBundleShortVersionString"] as? String ?? ""
report.currentVersion = currentVersion
report.remoteShortVersion = plist["CFBundleShortVersionString"] as? String ?? ""
report.remoteVersion = remoteVersion
report.versionDescription = versionDescription
DispatchQueue.main.async {
callback(.success(.shouldUpdate(report: report)))
}
IME.prtDebugIntel("vChewingDebug: Update // Callbck Complete.")
} catch {
DispatchQueue.main.async {
forced ? callback(.success(.noNeedToUpdate)) : callback(.success(.ignored))
}
}
}
task.resume()
return task
}
}

View File

@ -49,7 +49,7 @@ class ctlInputMethod: IMKInputController {
// MARK: -
private var currentCandidateClient: Any?
private var currentClient: Any?
private var keyHandler: KeyHandler = KeyHandler()
private var state: InputState = InputState.Empty()
@ -65,8 +65,9 @@ class ctlInputMethod: IMKInputController {
// MARK: - Keyboard Layout Specifier
@objc func setKeyLayout() {
let client = client().self as IMKTextInput
client.overrideKeyboard(withKeyboardNamed: mgrPrefs.basisKeyboardLayout)
if let client = currentClient {
(client as? IMKTextInput)?.overrideKeyboard(withKeyboardNamed: mgrPrefs.basicKeyboardLayout)
}
}
// MARK: - IMKInputController methods
@ -76,24 +77,38 @@ class ctlInputMethod: IMKInputController {
keyHandler.delegate = self
}
// MARK: - KeyHandler Reset Command
func resetKeyHandler() {
if let currentClient = currentClient {
keyHandler.clear()
self.handle(state: InputState.Empty(), client: currentClient)
}
}
// MARK: - IMKStateSetting protocol methods
override func activateServer(_ client: Any!) {
UserDefaults.standard.synchronize()
// Override the keyboard layout to the basic one.
setKeyLayout()
// reset the state
currentCandidateClient = nil
currentClient = client
keyHandler.clear()
keyHandler.syncWithPreferences()
self.handle(state: .Empty(), client: client)
if let bundleCheckID = (client as? IMKTextInput)?.bundleIdentifier() {
if bundleCheckID != Bundle.main.bundleIdentifier {
// Override the keyboard layout to the basic one.
setKeyLayout()
self.handle(state: .Empty(), client: client)
}
}
(NSApp.delegate as? AppDelegate)?.checkForUpdate()
}
override func deactivateServer(_ client: Any!) {
keyHandler.clear()
currentClient = nil
self.handle(state: .Empty(), client: client)
self.handle(state: .Deactivated(), client: client)
}
@ -110,14 +125,18 @@ class ctlInputMethod: IMKInputController {
}
mgrLangModel.loadDataModel(newInputMode)
// Remember to override the keyboard layout again -- treat this as an activate event.
setKeyLayout()
if keyHandler.inputMode != newInputMode {
UserDefaults.standard.synchronize()
keyHandler.clear()
keyHandler.inputMode = newInputMode
self.handle(state: .Empty(), client: client)
if let bundleCheckID = (client as? IMKTextInput)?.bundleIdentifier() {
if bundleCheckID != Bundle.main.bundleIdentifier {
// Remember to override the keyboard layout again -- treat this as an activate event.
setKeyLayout()
self.handle(state: .Empty(), client: client)
}
}
}
//
@ -225,7 +244,7 @@ extension ctlInputMethod {
}
private func handle(state: InputState.Deactivated, previous: InputState, client: Any?) {
currentCandidateClient = nil
currentClient = nil
ctlCandidateCurrent?.delegate = nil
ctlCandidateCurrent?.visible = false
@ -235,8 +254,8 @@ extension ctlInputMethod {
commit(text: previous.composingBuffer, client: client)
}
(client as? IMKTextInput)?.setMarkedText(
"", selectionRange: NSMakeRange(0, 0),
replacementRange: NSMakeRange(NSNotFound, NSNotFound))
"", selectionRange: NSRange(location: 0, length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound))
}
private func handle(state: InputState.Empty, previous: InputState, client: Any?) {
@ -251,8 +270,8 @@ extension ctlInputMethod {
commit(text: previous.composingBuffer, client: client)
}
client.setMarkedText(
"", selectionRange: NSMakeRange(0, 0),
replacementRange: NSMakeRange(NSNotFound, NSNotFound))
"", selectionRange: NSRange(location: 0, length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound))
}
private func handle(
@ -266,8 +285,8 @@ extension ctlInputMethod {
}
client.setMarkedText(
"", selectionRange: NSMakeRange(0, 0),
replacementRange: NSMakeRange(NSNotFound, NSNotFound))
"", selectionRange: NSRange(location: 0, length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound))
}
private func handle(state: InputState.Committing, previous: InputState, client: Any?) {
@ -283,8 +302,8 @@ extension ctlInputMethod {
commit(text: poppedText, client: client)
}
client.setMarkedText(
"", selectionRange: NSMakeRange(0, 0),
replacementRange: NSMakeRange(NSNotFound, NSNotFound))
"", selectionRange: NSRange(location: 0, length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound))
}
private func handle(state: InputState.Inputting, previous: InputState, client: Any?) {
@ -303,8 +322,8 @@ extension ctlInputMethod {
// the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound,
// i.e. the client app needs to take care of where to put this composing buffer
client.setMarkedText(
state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0),
replacementRange: NSMakeRange(NSNotFound, NSNotFound))
state.attributedString, selectionRange: NSRange(location: Int(state.cursorIndex), length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound))
if !state.tooltip.isEmpty {
show(
tooltip: state.tooltip, composingBuffer: state.composingBuffer,
@ -322,8 +341,8 @@ extension ctlInputMethod {
// the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound,
// i.e. the client app needs to take care of where to put this composing buffer
client.setMarkedText(
state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0),
replacementRange: NSMakeRange(NSNotFound, NSNotFound))
state.attributedString, selectionRange: NSRange(location: Int(state.cursorIndex), length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound))
if state.tooltip.isEmpty {
hideTooltip()
@ -344,8 +363,8 @@ extension ctlInputMethod {
// the selection range is where the cursor is, with the length being 0 and replacement range NSNotFound,
// i.e. the client app needs to take care of where to put this composing buffer
client.setMarkedText(
state.attributedString, selectionRange: NSMakeRange(Int(state.cursorIndex), 0),
replacementRange: NSMakeRange(NSNotFound, NSNotFound))
state.attributedString, selectionRange: NSRange(location: Int(state.cursorIndex), length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound))
show(candidateWindowWith: state, client: client)
}
@ -356,8 +375,8 @@ extension ctlInputMethod {
return
}
client.setMarkedText(
"", selectionRange: NSMakeRange(0, 0),
replacementRange: NSMakeRange(NSNotFound, NSNotFound))
"", selectionRange: NSRange(location: 0, length: 0),
replacementRange: NSRange(location: NSNotFound, length: NSNotFound))
show(candidateWindowWith: state, client: client)
}
}
@ -441,11 +460,11 @@ extension ctlInputMethod {
ctlCandidateCurrent?.delegate = self
ctlCandidateCurrent?.reloadData()
currentCandidateClient = client
currentClient = client
ctlCandidateCurrent?.visible = true
var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0)
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
var cursor: Int = 0
if let state = state as? InputState.ChoosingCandidate {
@ -463,20 +482,18 @@ extension ctlInputMethod {
if useVerticalMode {
ctlCandidateCurrent?.set(
windowTopLeftPoint: NSMakePoint(
lineHeightRect.origin.x + lineHeightRect.size.width + 4.0,
lineHeightRect.origin.y - 4.0),
windowTopLeftPoint: NSPoint(
x: lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, y: lineHeightRect.origin.y - 4.0),
bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0)
} else {
ctlCandidateCurrent?.set(
windowTopLeftPoint: NSMakePoint(
lineHeightRect.origin.x, lineHeightRect.origin.y - 4.0),
windowTopLeftPoint: NSPoint(x: lineHeightRect.origin.x, y: lineHeightRect.origin.y - 4.0),
bottomOutOfScreenAdjustmentHeight: lineHeightRect.size.height + 4.0)
}
}
private func show(tooltip: String, composingBuffer: String, cursorIndex: UInt, client: Any!) {
var lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0)
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
var cursor: Int = Int(cursorIndex)
if cursor == composingBuffer.count && cursor != 0 {
cursor -= 1
@ -522,14 +539,17 @@ extension ctlInputMethod: KeyHandlerDelegate {
let refInputModeReversed: InputMode =
(keyHandler.inputMode == InputMode.imeModeCHT)
? InputMode.imeModeCHS : InputMode.imeModeCHT
mgrLangModel.writeUserPhrase(
if !mgrLangModel.writeUserPhrase(
state.userPhrase, inputMode: keyHandler.inputMode,
areWeDuplicating: state.chkIfUserPhraseExists,
areWeDeleting: ctlInputMethod.areWeDeleting)
mgrLangModel.writeUserPhrase(
state.userPhraseConverted, inputMode: refInputModeReversed,
areWeDuplicating: false,
areWeDeleting: ctlInputMethod.areWeDeleting)
|| !mgrLangModel.writeUserPhrase(
state.userPhraseConverted, inputMode: refInputModeReversed,
areWeDuplicating: false,
areWeDeleting: ctlInputMethod.areWeDeleting)
{
return false
}
return true
}
}
@ -558,7 +578,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
}
func ctlCandidate(_ controller: ctlCandidate, didSelectCandidateAtIndex index: UInt) {
let client = currentCandidateClient
let client = currentClient
if let state = state as? InputState.SymbolTable,
let node = state.node.children?[Int(index)]
@ -566,7 +586,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
if let children = node.children, !children.isEmpty {
self.handle(
state: .SymbolTable(node: node, useVerticalMode: state.useVerticalMode),
client: currentCandidateClient)
client: currentClient)
} else {
self.handle(state: .Committing(poppedText: node.title), client: client)
self.handle(state: .Empty(), client: client)
@ -603,7 +623,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
if let state = state as? InputState.AssociatedPhrases {
let selectedValue = state.candidates[Int(index)]
handle(state: .Committing(poppedText: selectedValue), client: currentCandidateClient)
handle(state: .Committing(poppedText: selectedValue), client: currentClient)
if mgrPrefs.associatedPhrasesEnabled,
let associatePhrases = keyHandler.buildAssociatePhraseState(
withKey: selectedValue, useVerticalMode: state.useVerticalMode)

View File

@ -123,10 +123,14 @@ extension ctlInputMethod {
menu.addItem(NSMenuItem.separator()) // ---------------------
menu.addItem(
withTitle: NSLocalizedString("vChewing Preferences…", comment: ""),
action: #selector(showPreferences(_:)), keyEquivalent: "")
if !optionKeyPressed {
if optionKeyPressed {
menu.addItem(
withTitle: NSLocalizedString("vChewing Preferences…", comment: ""),
action: #selector(showLegacyPreferences(_:)), keyEquivalent: "")
} else {
menu.addItem(
withTitle: NSLocalizedString("vChewing Preferences…", comment: ""),
action: #selector(showPreferences(_:)), keyEquivalent: "")
menu.addItem(
withTitle: NSLocalizedString("Check for Updates…", comment: ""),
action: #selector(checkForUpdate(_:)), keyEquivalent: "")
@ -152,6 +156,20 @@ extension ctlInputMethod {
// MARK: - IME Menu Items
@objc override func showPreferences(_ sender: Any?) {
if #available(macOS 11.0, *) {
NSApp.setActivationPolicy(.accessory)
ctlPrefUI.shared.controller.show(preferencePane: Preferences.PaneIdentifier(rawValue: "General"))
ctlPrefUI.shared.controller.window?.level = .floating
} else {
showPrefWindowTraditional()
}
}
@objc func showLegacyPreferences(_ sender: Any?) {
showPrefWindowTraditional()
}
private func showPrefWindowTraditional() {
(NSApp.delegate as? AppDelegate)?.showPreferences()
NSApp.activate(ignoringOtherApps: true)
}
@ -163,6 +181,7 @@ extension ctlInputMethod {
mgrPrefs.toggleSCPCTypingModeEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "")))
resetKeyHandler()
}
@objc func toggleChineseConverter(_ sender: Any?) {
@ -172,6 +191,7 @@ extension ctlInputMethod {
mgrPrefs.toggleChineseConversionEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "")))
resetKeyHandler()
}
@objc func toggleShiftJISShinjitaiOutput(_ sender: Any?) {
@ -181,6 +201,7 @@ extension ctlInputMethod {
mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "")))
resetKeyHandler()
}
@objc func toggleHalfWidthPunctuation(_ sender: Any?) {
@ -191,6 +212,7 @@ extension ctlInputMethod {
mgrPrefs.toggleHalfWidthPunctuationEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "")))
resetKeyHandler()
}
@objc func toggleCNS11643Enabled(_ sender: Any?) {
@ -200,6 +222,7 @@ extension ctlInputMethod {
mgrPrefs.toggleCNS11643Enabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "")))
resetKeyHandler()
}
@objc func toggleSymbolEnabled(_ sender: Any?) {
@ -209,6 +232,7 @@ extension ctlInputMethod {
mgrPrefs.toggleSymbolInputEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "")))
resetKeyHandler()
}
@objc func toggleAssociatedPhrasesEnabled(_ sender: Any?) {
@ -219,6 +243,7 @@ extension ctlInputMethod {
mgrPrefs.toggleAssociatedPhrasesEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "")))
resetKeyHandler()
}
@objc func togglePhraseReplacement(_ sender: Any?) {
@ -228,6 +253,7 @@ extension ctlInputMethod {
mgrPrefs.togglePhraseReplacementEnabled()
? NSLocalizedString("NotificationSwitchON", comment: "")
: NSLocalizedString("NotificationSwitchOFF", comment: "")))
resetKeyHandler()
}
@objc func selfUninstall(_ sender: Any?) {

View File

@ -26,38 +26,40 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Cocoa
private let kIsDebugModeEnabled = "_DebugMode"
private let kUserDataFolderSpecified = "UserDataFolderSpecified"
private let kCheckUpdateAutomatically = "CheckUpdateAutomatically"
private let kKeyboardLayoutPreference = "KeyboardLayout"
private let kBasisKeyboardLayoutPreference = "BasisKeyboardLayout"
private let kShowPageButtonsInCandidateWindow = "ShowPageButtonsInCandidateWindow"
private let kCandidateListTextSize = "CandidateListTextSize"
private let kAppleLanguagesPreferences = "AppleLanguages"
private let kShouldAutoReloadUserDataFiles = "ShouldAutoReloadUserDataFiles"
private let kSelectPhraseAfterCursorAsCandidatePreference = "SelectPhraseAfterCursorAsCandidate"
private let kUseHorizontalCandidateListPreference = "UseHorizontalCandidateList"
private let kComposingBufferSizePreference = "ComposingBufferSize"
private let kChooseCandidateUsingSpace = "ChooseCandidateUsingSpace"
private let kCNS11643Enabled = "CNS11643Enabled"
private let kSymbolInputEnabled = "SymbolInputEnabled"
private let kChineseConversionEnabled = "ChineseConversionEnabled"
private let kShiftJISShinjitaiOutputEnabled = "ShiftJISShinjitaiOutputEnabled"
private let kHalfWidthPunctuationEnabled = "HalfWidthPunctuationEnable"
private let kMoveCursorAfterSelectingCandidate = "MoveCursorAfterSelectingCandidate"
private let kEscToCleanInputBuffer = "EscToCleanInputBuffer"
private let kSpecifyTabKeyBehavior = "SpecifyTabKeyBehavior"
private let kSpecifySpaceKeyBehavior = "SpecifySpaceKeyBehavior"
private let kUseSCPCTypingMode = "UseSCPCTypingMode"
private let kMaxCandidateLength = "MaxCandidateLength"
private let kShouldNotFartInLieuOfBeep = "ShouldNotFartInLieuOfBeep"
struct UserDef {
static let kIsDebugModeEnabled = "_DebugMode"
static let kUserDataFolderSpecified = "UserDataFolderSpecified"
static let kCheckUpdateAutomatically = "CheckUpdateAutomatically"
static let kMandarinParser = "MandarinParser"
static let kBasicKeyboardLayout = "BasicKeyboardLayout"
static let kShowPageButtonsInCandidateWindow = "ShowPageButtonsInCandidateWindow"
static let kCandidateListTextSize = "CandidateListTextSize"
static let kAppleLanguages = "AppleLanguages"
static let kShouldAutoReloadUserDataFiles = "ShouldAutoReloadUserDataFiles"
static let kSelectPhraseAfterCursorAsCandidate = "SelectPhraseAfterCursorAsCandidate"
static let kUseHorizontalCandidateList = "UseHorizontalCandidateList"
static let kComposingBufferSize = "ComposingBufferSize"
static let kChooseCandidateUsingSpace = "ChooseCandidateUsingSpace"
static let kCNS11643Enabled = "CNS11643Enabled"
static let kSymbolInputEnabled = "SymbolInputEnabled"
static let kChineseConversionEnabled = "ChineseConversionEnabled"
static let kShiftJISShinjitaiOutputEnabled = "ShiftJISShinjitaiOutputEnabled"
static let kHalfWidthPunctuationEnabled = "HalfWidthPunctuationEnable"
static let kMoveCursorAfterSelectingCandidate = "MoveCursorAfterSelectingCandidate"
static let kEscToCleanInputBuffer = "EscToCleanInputBuffer"
static let kSpecifyShiftTabKeyBehavior = "SpecifyShiftTabKeyBehavior"
static let kSpecifyShiftSpaceKeyBehavior = "SpecifyShiftSpaceKeyBehavior"
static let kUseSCPCTypingMode = "UseSCPCTypingMode"
static let kMaxCandidateLength = "MaxCandidateLength"
static let kShouldNotFartInLieuOfBeep = "ShouldNotFartInLieuOfBeep"
private let kCandidateTextFontName = "CandidateTextFontName"
private let kCandidateKeyLabelFontName = "CandidateKeyLabelFontName"
private let kCandidateKeys = "CandidateKeys"
static let kCandidateTextFontName = "CandidateTextFontName"
static let kCandidateKeyLabelFontName = "CandidateKeyLabelFontName"
static let kCandidateKeys = "CandidateKeys"
private let kAssociatedPhrasesEnabled = "AssociatedPhrasesEnabled"
private let kPhraseReplacementEnabled = "PhraseReplacementEnabled"
static let kAssociatedPhrasesEnabled = "AssociatedPhrasesEnabled"
static let kPhraseReplacementEnabled = "PhraseReplacementEnabled"
}
private let kDefaultCandidateListTextSize: CGFloat = 18
private let kMinKeyLabelSize: CGFloat = 10
@ -75,7 +77,17 @@ private let kMaxComposingBufferSize = 30
private let kDefaultKeys = "123456789"
// MARK: Property wrappers
// MARK: - UserDefaults extension.
@objc extension UserDefaults {
func setDefault(_ value: Any?, forKey defaultName: String) {
if self.object(forKey: defaultName) == nil {
self.set(value, forKey: defaultName)
}
}
}
// MARK: - Property wrappers
@propertyWrapper
struct UserDefault<Value> {
@ -155,7 +167,7 @@ struct ComposingBufferSize {
// MARK: -
@objc enum KeyboardLayout: Int {
@objc enum MandarinParser: Int {
case ofStandard = 0
case ofEten = 1
case ofHsu = 2
@ -191,248 +203,169 @@ struct ComposingBufferSize {
@objc public class mgrPrefs: NSObject {
static var allKeys: [String] {
[
kIsDebugModeEnabled,
kUserDataFolderSpecified,
kKeyboardLayoutPreference,
kBasisKeyboardLayoutPreference,
kShowPageButtonsInCandidateWindow,
kCandidateListTextSize,
kAppleLanguagesPreferences,
kShouldAutoReloadUserDataFiles,
kSelectPhraseAfterCursorAsCandidatePreference,
kUseHorizontalCandidateListPreference,
kComposingBufferSizePreference,
kChooseCandidateUsingSpace,
kCNS11643Enabled,
kSymbolInputEnabled,
kChineseConversionEnabled,
kShiftJISShinjitaiOutputEnabled,
kHalfWidthPunctuationEnabled,
kSpecifyTabKeyBehavior,
kSpecifySpaceKeyBehavior,
kEscToCleanInputBuffer,
kCandidateTextFontName,
kCandidateKeyLabelFontName,
kCandidateKeys,
kMoveCursorAfterSelectingCandidate,
kPhraseReplacementEnabled,
kUseSCPCTypingMode,
kMaxCandidateLength,
kShouldNotFartInLieuOfBeep,
kAssociatedPhrasesEnabled,
UserDef.kIsDebugModeEnabled,
UserDef.kUserDataFolderSpecified,
UserDef.kMandarinParser,
UserDef.kBasicKeyboardLayout,
UserDef.kShowPageButtonsInCandidateWindow,
UserDef.kCandidateListTextSize,
UserDef.kAppleLanguages,
UserDef.kShouldAutoReloadUserDataFiles,
UserDef.kSelectPhraseAfterCursorAsCandidate,
UserDef.kUseHorizontalCandidateList,
UserDef.kComposingBufferSize,
UserDef.kChooseCandidateUsingSpace,
UserDef.kCNS11643Enabled,
UserDef.kSymbolInputEnabled,
UserDef.kChineseConversionEnabled,
UserDef.kShiftJISShinjitaiOutputEnabled,
UserDef.kHalfWidthPunctuationEnabled,
UserDef.kSpecifyShiftTabKeyBehavior,
UserDef.kSpecifyShiftSpaceKeyBehavior,
UserDef.kEscToCleanInputBuffer,
UserDef.kCandidateTextFontName,
UserDef.kCandidateKeyLabelFontName,
UserDef.kCandidateKeys,
UserDef.kMoveCursorAfterSelectingCandidate,
UserDef.kPhraseReplacementEnabled,
UserDef.kUseSCPCTypingMode,
UserDef.kMaxCandidateLength,
UserDef.kShouldNotFartInLieuOfBeep,
UserDef.kAssociatedPhrasesEnabled,
]
}
// MARK: - Preferences Module plist
@objc public static func setMissingDefaults() {
// Preferences Module plist private
//
if UserDefaults.standard.object(forKey: kIsDebugModeEnabled) == nil {
UserDefaults.standard.set(mgrPrefs.isDebugModeEnabled, forKey: kIsDebugModeEnabled)
}
//
if UserDefaults.standard.object(forKey: kCheckUpdateAutomatically) == nil {
UserDefaults.standard.set(false, forKey: kCheckUpdateAutomatically)
}
//
if UserDefaults.standard.object(forKey: kShowPageButtonsInCandidateWindow) == nil {
UserDefaults.standard.set(
mgrPrefs.showPageButtonsInCandidateWindow, forKey: kShowPageButtonsInCandidateWindow
)
}
//
if UserDefaults.standard.object(forKey: kSymbolInputEnabled) == nil {
UserDefaults.standard.set(mgrPrefs.symbolInputEnabled, forKey: kSymbolInputEnabled)
}
// 18
if UserDefaults.standard.object(forKey: kCandidateListTextSize) == nil {
UserDefaults.standard.set(
mgrPrefs.candidateListTextSize, forKey: kCandidateListTextSize)
}
// true
if UserDefaults.standard.object(forKey: kChooseCandidateUsingSpace) == nil {
UserDefaults.standard.set(
mgrPrefs.chooseCandidateUsingSpace, forKey: kChooseCandidateUsingSpace)
}
// 使
if UserDefaults.standard.object(forKey: kShouldAutoReloadUserDataFiles) == nil {
UserDefaults.standard.set(
mgrPrefs.shouldAutoReloadUserDataFiles, forKey: kShouldAutoReloadUserDataFiles)
}
// Tab
if UserDefaults.standard.object(forKey: kSpecifyTabKeyBehavior) == nil {
UserDefaults.standard.set(
mgrPrefs.specifyTabKeyBehavior, forKey: kSpecifyTabKeyBehavior)
}
// Space
if UserDefaults.standard.object(forKey: kSpecifySpaceKeyBehavior) == nil {
UserDefaults.standard.set(
mgrPrefs.specifySpaceKeyBehavior, forKey: kSpecifySpaceKeyBehavior)
}
// false
if UserDefaults.standard.object(forKey: kUseSCPCTypingMode) == nil {
UserDefaults.standard.set(mgrPrefs.useSCPCTypingMode, forKey: kUseSCPCTypingMode)
}
// false
if UserDefaults.standard.object(forKey: kAssociatedPhrasesEnabled) == nil {
UserDefaults.standard.set(
mgrPrefs.associatedPhrasesEnabled, forKey: kAssociatedPhrasesEnabled)
}
// 0
if UserDefaults.standard.object(forKey: kSelectPhraseAfterCursorAsCandidatePreference)
== nil
{
UserDefaults.standard.set(
mgrPrefs.selectPhraseAfterCursorAsCandidate,
forKey: kSelectPhraseAfterCursorAsCandidatePreference)
}
//
if UserDefaults.standard.object(forKey: kMoveCursorAfterSelectingCandidate) == nil {
UserDefaults.standard.set(
mgrPrefs.moveCursorAfterSelectingCandidate,
forKey: kMoveCursorAfterSelectingCandidate)
}
//
if UserDefaults.standard.object(forKey: kUseHorizontalCandidateListPreference) == nil {
UserDefaults.standard.set(
mgrPrefs.useHorizontalCandidateList, forKey: kUseHorizontalCandidateListPreference)
}
//
if UserDefaults.standard.object(forKey: kCNS11643Enabled) == nil {
UserDefaults.standard.set(mgrPrefs.cns11643Enabled, forKey: kCNS11643Enabled)
}
//
if UserDefaults.standard.object(forKey: kChineseConversionEnabled) == nil {
UserDefaults.standard.set(
mgrPrefs.chineseConversionEnabled, forKey: kChineseConversionEnabled)
}
// JIS
if UserDefaults.standard.object(forKey: kShiftJISShinjitaiOutputEnabled) == nil {
UserDefaults.standard.set(
mgrPrefs.shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled)
}
//
if UserDefaults.standard.object(forKey: kPhraseReplacementEnabled) == nil {
UserDefaults.standard.set(
mgrPrefs.phraseReplacementEnabled, forKey: kPhraseReplacementEnabled)
}
//
if UserDefaults.standard.object(forKey: kShouldNotFartInLieuOfBeep) == nil {
UserDefaults.standard.set(
mgrPrefs.shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep)
}
UserDefaults.standard.setDefault(mgrPrefs.isDebugModeEnabled, forKey: UserDef.kIsDebugModeEnabled)
UserDefaults.standard.setDefault(mgrPrefs.checkUpdateAutomatically, forKey: UserDef.kCheckUpdateAutomatically)
UserDefaults.standard.setDefault(
mgrPrefs.showPageButtonsInCandidateWindow, forKey: UserDef.kShowPageButtonsInCandidateWindow)
UserDefaults.standard.setDefault(mgrPrefs.symbolInputEnabled, forKey: UserDef.kSymbolInputEnabled)
UserDefaults.standard.setDefault(mgrPrefs.candidateListTextSize, forKey: UserDef.kCandidateListTextSize)
UserDefaults.standard.setDefault(mgrPrefs.chooseCandidateUsingSpace, forKey: UserDef.kChooseCandidateUsingSpace)
UserDefaults.standard.setDefault(
mgrPrefs.shouldAutoReloadUserDataFiles, forKey: UserDef.kShouldAutoReloadUserDataFiles)
UserDefaults.standard.setDefault(
mgrPrefs.specifyShiftTabKeyBehavior, forKey: UserDef.kSpecifyShiftTabKeyBehavior)
UserDefaults.standard.setDefault(
mgrPrefs.specifyShiftSpaceKeyBehavior, forKey: UserDef.kSpecifyShiftSpaceKeyBehavior)
UserDefaults.standard.setDefault(mgrPrefs.useSCPCTypingMode, forKey: UserDef.kUseSCPCTypingMode)
UserDefaults.standard.setDefault(mgrPrefs.associatedPhrasesEnabled, forKey: UserDef.kAssociatedPhrasesEnabled)
UserDefaults.standard.setDefault(
mgrPrefs.selectPhraseAfterCursorAsCandidate, forKey: UserDef.kSelectPhraseAfterCursorAsCandidate)
UserDefaults.standard.setDefault(
mgrPrefs.moveCursorAfterSelectingCandidate, forKey: UserDef.kMoveCursorAfterSelectingCandidate)
UserDefaults.standard.setDefault(
mgrPrefs.useHorizontalCandidateList, forKey: UserDef.kUseHorizontalCandidateList)
UserDefaults.standard.setDefault(mgrPrefs.cns11643Enabled, forKey: UserDef.kCNS11643Enabled)
UserDefaults.standard.setDefault(mgrPrefs.chineseConversionEnabled, forKey: UserDef.kChineseConversionEnabled)
UserDefaults.standard.setDefault(
mgrPrefs.shiftJISShinjitaiOutputEnabled, forKey: UserDef.kShiftJISShinjitaiOutputEnabled)
UserDefaults.standard.setDefault(mgrPrefs.phraseReplacementEnabled, forKey: UserDef.kPhraseReplacementEnabled)
UserDefaults.standard.setDefault(mgrPrefs.shouldNotFartInLieuOfBeep, forKey: UserDef.kShouldNotFartInLieuOfBeep)
UserDefaults.standard.synchronize()
}
@UserDefault(key: kIsDebugModeEnabled, defaultValue: false)
@UserDefault(key: UserDef.kIsDebugModeEnabled, defaultValue: false)
@objc static var isDebugModeEnabled: Bool
@UserDefault(key: kUserDataFolderSpecified, defaultValue: "")
@UserDefault(key: UserDef.kCheckUpdateAutomatically, defaultValue: false)
@objc static var checkUpdateAutomatically: Bool
@UserDefault(key: UserDef.kUserDataFolderSpecified, defaultValue: "")
@objc static var userDataFolderSpecified: String
@objc static func ifSpecifiedUserDataPathExistsInPlist() -> Bool {
UserDefaults.standard.object(forKey: kUserDataFolderSpecified) != nil
UserDefaults.standard.object(forKey: UserDef.kUserDataFolderSpecified) != nil
}
@UserDefault(key: kAppleLanguagesPreferences, defaultValue: [])
@objc static func resetSpecifiedUserDataFolder() {
UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified")
IME.initLangModels(userOnly: true)
}
@UserDefault(key: UserDef.kAppleLanguages, defaultValue: [])
@objc static var appleLanguages: [String]
@UserDefault(key: kKeyboardLayoutPreference, defaultValue: 0)
@objc static var keyboardLayout: Int
@UserDefault(key: UserDef.kMandarinParser, defaultValue: 0)
@objc static var mandarinParser: Int
@objc static var keyboardLayoutName: String {
(KeyboardLayout(rawValue: self.keyboardLayout) ?? KeyboardLayout.ofStandard).name
@objc static var mandarinParserName: String {
(MandarinParser(rawValue: self.mandarinParser) ?? MandarinParser.ofStandard).name
}
@UserDefault(
key: kBasisKeyboardLayoutPreference, defaultValue: "com.apple.keylayout.ZhuyinBopomofo")
@objc static var basisKeyboardLayout: String
key: UserDef.kBasicKeyboardLayout, defaultValue: "com.apple.keylayout.ZhuyinBopomofo")
@objc static var basicKeyboardLayout: String
@UserDefault(key: kShowPageButtonsInCandidateWindow, defaultValue: true)
@UserDefault(key: UserDef.kShowPageButtonsInCandidateWindow, defaultValue: true)
@objc static var showPageButtonsInCandidateWindow: Bool
@CandidateListTextSize(key: kCandidateListTextSize)
@CandidateListTextSize(key: UserDef.kCandidateListTextSize)
@objc static var candidateListTextSize: CGFloat
@UserDefault(key: kShouldAutoReloadUserDataFiles, defaultValue: true)
@UserDefault(key: UserDef.kShouldAutoReloadUserDataFiles, defaultValue: true)
@objc static var shouldAutoReloadUserDataFiles: Bool
@UserDefault(key: kSelectPhraseAfterCursorAsCandidatePreference, defaultValue: false)
@UserDefault(key: UserDef.kSelectPhraseAfterCursorAsCandidate, defaultValue: false)
@objc static var selectPhraseAfterCursorAsCandidate: Bool
@UserDefault(key: kMoveCursorAfterSelectingCandidate, defaultValue: false)
@UserDefault(key: UserDef.kMoveCursorAfterSelectingCandidate, defaultValue: true)
@objc static var moveCursorAfterSelectingCandidate: Bool
@UserDefault(key: kUseHorizontalCandidateListPreference, defaultValue: true)
@UserDefault(key: UserDef.kUseHorizontalCandidateList, defaultValue: true)
@objc static var useHorizontalCandidateList: Bool
@ComposingBufferSize(key: kComposingBufferSizePreference)
@ComposingBufferSize(key: UserDef.kComposingBufferSize)
@objc static var composingBufferSize: Int
@UserDefault(key: kChooseCandidateUsingSpace, defaultValue: true)
@UserDefault(key: UserDef.kChooseCandidateUsingSpace, defaultValue: true)
@objc static var chooseCandidateUsingSpace: Bool
@UserDefault(key: kUseSCPCTypingMode, defaultValue: false)
@UserDefault(key: UserDef.kUseSCPCTypingMode, defaultValue: false)
@objc static var useSCPCTypingMode: Bool
@objc static func toggleSCPCTypingModeEnabled() -> Bool {
useSCPCTypingMode = !useSCPCTypingMode
UserDefaults.standard.set(useSCPCTypingMode, forKey: kUseSCPCTypingMode)
UserDefaults.standard.set(useSCPCTypingMode, forKey: UserDef.kUseSCPCTypingMode)
return useSCPCTypingMode
}
@UserDefault(key: kMaxCandidateLength, defaultValue: kDefaultComposingBufferSize * 2)
@UserDefault(key: UserDef.kMaxCandidateLength, defaultValue: kDefaultComposingBufferSize * 2)
@objc static var maxCandidateLength: Int
@UserDefault(key: kShouldNotFartInLieuOfBeep, defaultValue: true)
@UserDefault(key: UserDef.kShouldNotFartInLieuOfBeep, defaultValue: true)
@objc static var shouldNotFartInLieuOfBeep: Bool
@objc static func toggleShouldNotFartInLieuOfBeep() -> Bool {
shouldNotFartInLieuOfBeep = !shouldNotFartInLieuOfBeep
UserDefaults.standard.set(shouldNotFartInLieuOfBeep, forKey: kShouldNotFartInLieuOfBeep)
UserDefaults.standard.set(shouldNotFartInLieuOfBeep, forKey: UserDef.kShouldNotFartInLieuOfBeep)
return shouldNotFartInLieuOfBeep
}
@UserDefault(key: kCNS11643Enabled, defaultValue: false)
@UserDefault(key: UserDef.kCNS11643Enabled, defaultValue: false)
@objc static var cns11643Enabled: Bool
@objc static func toggleCNS11643Enabled() -> Bool {
cns11643Enabled = !cns11643Enabled
mgrLangModel.setCNSEnabled(cns11643Enabled) //
UserDefaults.standard.set(cns11643Enabled, forKey: kCNS11643Enabled)
UserDefaults.standard.set(cns11643Enabled, forKey: UserDef.kCNS11643Enabled)
return cns11643Enabled
}
@UserDefault(key: kSymbolInputEnabled, defaultValue: true)
@UserDefault(key: UserDef.kSymbolInputEnabled, defaultValue: true)
@objc static var symbolInputEnabled: Bool
@objc static func toggleSymbolInputEnabled() -> Bool {
symbolInputEnabled = !symbolInputEnabled
mgrLangModel.setSymbolEnabled(symbolInputEnabled) //
UserDefaults.standard.set(symbolInputEnabled, forKey: kSymbolInputEnabled)
UserDefaults.standard.set(symbolInputEnabled, forKey: UserDef.kSymbolInputEnabled)
return symbolInputEnabled
}
@UserDefault(key: kChineseConversionEnabled, defaultValue: false)
@UserDefault(key: UserDef.kChineseConversionEnabled, defaultValue: false)
@objc static var chineseConversionEnabled: Bool
@objc @discardableResult static func toggleChineseConversionEnabled() -> Bool {
@ -441,13 +374,13 @@ struct ComposingBufferSize {
if chineseConversionEnabled && shiftJISShinjitaiOutputEnabled {
self.toggleShiftJISShinjitaiOutputEnabled()
UserDefaults.standard.set(
shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled)
shiftJISShinjitaiOutputEnabled, forKey: UserDef.kShiftJISShinjitaiOutputEnabled)
}
UserDefaults.standard.set(chineseConversionEnabled, forKey: kChineseConversionEnabled)
UserDefaults.standard.set(chineseConversionEnabled, forKey: UserDef.kChineseConversionEnabled)
return chineseConversionEnabled
}
@UserDefault(key: kShiftJISShinjitaiOutputEnabled, defaultValue: false)
@UserDefault(key: UserDef.kShiftJISShinjitaiOutputEnabled, defaultValue: false)
@objc static var shiftJISShinjitaiOutputEnabled: Bool
@objc @discardableResult static func toggleShiftJISShinjitaiOutputEnabled() -> Bool {
@ -457,11 +390,11 @@ struct ComposingBufferSize {
self.toggleChineseConversionEnabled()
}
UserDefaults.standard.set(
shiftJISShinjitaiOutputEnabled, forKey: kShiftJISShinjitaiOutputEnabled)
shiftJISShinjitaiOutputEnabled, forKey: UserDef.kShiftJISShinjitaiOutputEnabled)
return shiftJISShinjitaiOutputEnabled
}
@UserDefault(key: kHalfWidthPunctuationEnabled, defaultValue: false)
@UserDefault(key: UserDef.kHalfWidthPunctuationEnabled, defaultValue: false)
@objc static var halfWidthPunctuationEnabled: Bool
@objc static func toggleHalfWidthPunctuationEnabled() -> Bool {
@ -469,23 +402,23 @@ struct ComposingBufferSize {
return halfWidthPunctuationEnabled
}
@UserDefault(key: kEscToCleanInputBuffer, defaultValue: true)
@UserDefault(key: UserDef.kEscToCleanInputBuffer, defaultValue: true)
@objc static var escToCleanInputBuffer: Bool
@UserDefault(key: kSpecifyTabKeyBehavior, defaultValue: false)
@objc static var specifyTabKeyBehavior: Bool
@UserDefault(key: UserDef.kSpecifyShiftTabKeyBehavior, defaultValue: false)
@objc static var specifyShiftTabKeyBehavior: Bool
@UserDefault(key: kSpecifySpaceKeyBehavior, defaultValue: false)
@objc static var specifySpaceKeyBehavior: Bool
@UserDefault(key: UserDef.kSpecifyShiftSpaceKeyBehavior, defaultValue: false)
@objc static var specifyShiftSpaceKeyBehavior: Bool
// MARK: - Optional settings
@UserDefault(key: kCandidateTextFontName, defaultValue: nil)
@UserDefault(key: UserDef.kCandidateTextFontName, defaultValue: nil)
@objc static var candidateTextFontName: String?
@UserDefault(key: kCandidateKeyLabelFontName, defaultValue: nil)
@UserDefault(key: UserDef.kCandidateKeyLabelFontName, defaultValue: nil)
@objc static var candidateKeyLabelFontName: String?
@UserDefault(key: kCandidateKeys, defaultValue: kDefaultKeys)
@UserDefault(key: UserDef.kCandidateKeys, defaultValue: kDefaultKeys)
@objc static var candidateKeys: String
@objc static var defaultCandidateKeys: String {
@ -548,22 +481,22 @@ struct ComposingBufferSize {
}
@UserDefault(key: kPhraseReplacementEnabled, defaultValue: false)
@UserDefault(key: UserDef.kPhraseReplacementEnabled, defaultValue: false)
@objc static var phraseReplacementEnabled: Bool
@objc static func togglePhraseReplacementEnabled() -> Bool {
phraseReplacementEnabled = !phraseReplacementEnabled
mgrLangModel.setPhraseReplacementEnabled(phraseReplacementEnabled)
UserDefaults.standard.set(phraseReplacementEnabled, forKey: kPhraseReplacementEnabled)
UserDefaults.standard.set(phraseReplacementEnabled, forKey: UserDef.kPhraseReplacementEnabled)
return phraseReplacementEnabled
}
@UserDefault(key: kAssociatedPhrasesEnabled, defaultValue: false)
@UserDefault(key: UserDef.kAssociatedPhrasesEnabled, defaultValue: false)
@objc static var associatedPhrasesEnabled: Bool
@objc static func toggleAssociatedPhrasesEnabled() -> Bool {
associatedPhrasesEnabled = !associatedPhrasesEnabled
UserDefaults.standard.set(associatedPhrasesEnabled, forKey: kAssociatedPhrasesEnabled)
UserDefaults.standard.set(associatedPhrasesEnabled, forKey: UserDef.kAssociatedPhrasesEnabled)
return associatedPhrasesEnabled
}

View File

@ -35,29 +35,15 @@ NS_ASSUME_NONNULL_BEGIN
+ (void)loadUserPhrases;
+ (void)loadUserAssociatedPhrases;
+ (void)loadUserPhraseReplacement;
+ (BOOL)checkIfUserLanguageModelFilesExist;
+ (BOOL)checkIfUserDataFolderExists;
+ (BOOL)checkIfSpecifiedUserDataFolderValid:(NSString *)folderPath;
+ (NSString *)dataFolderPath:(bool)isDefaultFolder NS_SWIFT_NAME(dataFolderPath(isDefaultFolder:));
+ (BOOL)checkIfUserPhraseExist:(NSString *)userPhrase
inputMode:(InputMode)mode
key:(NSString *)key NS_SWIFT_NAME(checkIfUserPhraseExist(userPhrase:mode:key:));
+ (BOOL)writeUserPhrase:(NSString *)userPhrase
inputMode:(InputMode)mode
areWeDuplicating:(BOOL)areWeDuplicating
areWeDeleting:(BOOL)areWeDeleting;
+ (void)consolidateGivenFile:(NSString *)path shouldCheckPragma:(BOOL)shouldCheckPragma;
+ (void)setPhraseReplacementEnabled:(BOOL)phraseReplacementEnabled;
+ (void)setCNSEnabled:(BOOL)cnsEnabled;
+ (void)setSymbolEnabled:(BOOL)symbolEnabled;
+ (NSString *)specifyBundleDataPath:(NSString *)filename;
+ (NSString *)userPhrasesDataPath:(InputMode)mode;
+ (NSString *)userSymbolDataPath:(InputMode)mode;
+ (NSString *)userAssociatedPhrasesDataPath:(InputMode)mode;
+ (NSString *)excludedPhrasesDataPath:(InputMode)mode;
+ (NSString *)phraseReplacementDataPath:(InputMode)mode;
@end
/// The following methods are merely for testing.

View File

@ -37,28 +37,16 @@ static vChewing::LMInstantiator gLangModelCHS;
static vChewing::UserOverrideModel gUserOverrideModelCHT(kUserOverrideModelCapacity, kObservedOverrideHalflife);
static vChewing::UserOverrideModel gUserOverrideModelCHS(kUserOverrideModelCapacity, kObservedOverrideHalflife);
static NSString *const kUserDataTemplateName = @"template-data";
static NSString *const kUserAssDataTemplateName = @"template-data";
static NSString *const kExcludedPhrasesvChewingTemplateName = @"template-exclude-phrases";
static NSString *const kPhraseReplacementTemplateName = @"template-phrases-replacement";
static NSString *const kUserSymbolDataTemplateName = @"template-user-symbol-data";
static NSString *const kTemplateExtension = @".txt";
@implementation mgrLangModel
// 這個函數無法遷移至 Swift
static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing::LMInstantiator &lm)
{
Class cls = NSClassFromString(@"ctlInputMethod");
NSString *dataPath = [[NSBundle bundleForClass:cls] pathForResource:filenameWithoutExtension ofType:@"txt"];
NSString *dataPath = [mgrLangModel getBundleDataPath:filenameWithoutExtension];
lm.loadLanguageModel([dataPath UTF8String]);
}
+ (NSString *)specifyBundleDataPath:(NSString *)filenameWithoutExtension;
{
Class cls = NSClassFromString(@"ctlInputMethod");
return [[NSBundle bundleForClass:cls] pathForResource:filenameWithoutExtension ofType:@"txt"];
}
// 這個函數無法遷移至 Swift
+ (void)loadDataModels
{
if (!gLangModelCHT.isDataModelLoaded())
@ -67,15 +55,15 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing
}
if (!gLangModelCHT.isMiscDataLoaded())
{
gLangModelCHT.loadMiscData([[self specifyBundleDataPath:@"data-zhuyinwen"] UTF8String]);
gLangModelCHT.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]);
}
if (!gLangModelCHT.isSymbolDataLoaded())
{
gLangModelCHT.loadSymbolData([[self specifyBundleDataPath:@"data-symbols"] UTF8String]);
gLangModelCHT.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]);
}
if (!gLangModelCHT.isCNSDataLoaded())
{
gLangModelCHT.loadCNSData([[self specifyBundleDataPath:@"char-kanji-cns"] UTF8String]);
gLangModelCHT.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]);
}
// -----------------
if (!gLangModelCHS.isDataModelLoaded())
@ -84,18 +72,19 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing
}
if (!gLangModelCHS.isMiscDataLoaded())
{
gLangModelCHS.loadMiscData([[self specifyBundleDataPath:@"data-zhuyinwen"] UTF8String]);
gLangModelCHS.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]);
}
if (!gLangModelCHS.isSymbolDataLoaded())
{
gLangModelCHS.loadSymbolData([[self specifyBundleDataPath:@"data-symbols"] UTF8String]);
gLangModelCHS.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]);
}
if (!gLangModelCHS.isCNSDataLoaded())
{
gLangModelCHS.loadCNSData([[self specifyBundleDataPath:@"char-kanji-cns"] UTF8String]);
gLangModelCHS.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]);
}
}
// 這個函數無法遷移至 Swift
+ (void)loadDataModel:(InputMode)mode
{
if ([mode isEqualToString:imeModeCHT])
@ -106,15 +95,15 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing
}
if (!gLangModelCHT.isMiscDataLoaded())
{
gLangModelCHT.loadMiscData([[self specifyBundleDataPath:@"data-zhuyinwen"] UTF8String]);
gLangModelCHT.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]);
}
if (!gLangModelCHT.isSymbolDataLoaded())
{
gLangModelCHT.loadSymbolData([[self specifyBundleDataPath:@"data-symbols"] UTF8String]);
gLangModelCHT.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]);
}
if (!gLangModelCHT.isCNSDataLoaded())
{
gLangModelCHT.loadCNSData([[self specifyBundleDataPath:@"char-kanji-cns"] UTF8String]);
gLangModelCHT.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]);
}
}
@ -126,19 +115,20 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing
}
if (!gLangModelCHS.isMiscDataLoaded())
{
gLangModelCHS.loadMiscData([[self specifyBundleDataPath:@"data-zhuyinwen"] UTF8String]);
gLangModelCHS.loadMiscData([[self getBundleDataPath:@"data-zhuyinwen"] UTF8String]);
}
if (!gLangModelCHS.isSymbolDataLoaded())
{
gLangModelCHS.loadSymbolData([[self specifyBundleDataPath:@"data-symbols"] UTF8String]);
gLangModelCHS.loadSymbolData([[self getBundleDataPath:@"data-symbols"] UTF8String]);
}
if (!gLangModelCHS.isCNSDataLoaded())
{
gLangModelCHS.loadCNSData([[self specifyBundleDataPath:@"char-kanji-cns"] UTF8String]);
gLangModelCHS.loadCNSData([[self getBundleDataPath:@"char-kanji-cns"] UTF8String]);
}
}
}
// 這個函數無法遷移至 Swift
+ (void)loadUserPhrases
{
gLangModelCHT.loadUserPhrases([[self userPhrasesDataPath:imeModeCHT] UTF8String],
@ -149,136 +139,21 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing
gLangModelCHS.loadUserSymbolData([[self userSymbolDataPath:imeModeCHS] UTF8String]);
}
// 這個函數無法遷移至 Swift
+ (void)loadUserAssociatedPhrases
{
gLangModelCHT.loadUserAssociatedPhrases([[self userAssociatedPhrasesDataPath:imeModeCHT] UTF8String]);
gLangModelCHS.loadUserAssociatedPhrases([[self userAssociatedPhrasesDataPath:imeModeCHS] UTF8String]);
}
// 這個函數無法遷移至 Swift
+ (void)loadUserPhraseReplacement
{
gLangModelCHT.loadPhraseReplacementMap([[self phraseReplacementDataPath:imeModeCHT] UTF8String]);
gLangModelCHS.loadPhraseReplacementMap([[self phraseReplacementDataPath:imeModeCHS] UTF8String]);
}
+ (BOOL)checkIfUserDataFolderExists
{
NSString *folderPath = [self dataFolderPath:false];
BOOL isFolder = NO;
BOOL folderExist = [[NSFileManager defaultManager] fileExistsAtPath:folderPath isDirectory:&isFolder];
if (folderExist && !isFolder)
{
NSError *error = nil;
[[NSFileManager defaultManager] removeItemAtPath:folderPath error:&error];
if (error)
{
NSLog(@"Failed to remove folder %@", error);
return NO;
}
folderExist = NO;
}
if (!folderExist)
{
NSError *error = nil;
[[NSFileManager defaultManager] createDirectoryAtPath:folderPath
withIntermediateDirectories:YES
attributes:nil
error:&error];
if (error)
{
NSLog(@"Failed to create folder %@", error);
return NO;
}
}
return YES;
}
+ (BOOL)checkIfSpecifiedUserDataFolderValid:(NSString *)folderPath
{
BOOL isFolder = NO;
BOOL folderExist = [[NSFileManager defaultManager] fileExistsAtPath:folderPath isDirectory:&isFolder];
if ((folderExist && !isFolder) || (!folderExist))
{
return NO;
}
return YES;
}
+ (BOOL)ensureFileExists:(NSString *)filePath
populateWithTemplate:(NSString *)templateBasename
extension:(NSString *)ext
{
if (![[NSFileManager defaultManager] fileExistsAtPath:filePath])
{
NSURL *templateURL = [[NSBundle mainBundle] URLForResource:templateBasename withExtension:ext];
NSData *templateData;
if (templateURL)
{
templateData = [NSData dataWithContentsOfURL:templateURL];
}
else
{
templateData = [@"" dataUsingEncoding:NSUTF8StringEncoding];
}
BOOL result = [templateData writeToFile:filePath atomically:YES];
if (!result)
{
NSLog(@"Failed to write file");
return NO;
}
}
return YES;
}
+ (BOOL)checkIfUserLanguageModelFilesExist
{
if (![self checkIfUserDataFolderExists])
return NO;
if (![self ensureFileExists:[self userPhrasesDataPath:imeModeCHS]
populateWithTemplate:kUserDataTemplateName
extension:kTemplateExtension])
return NO;
if (![self ensureFileExists:[self userPhrasesDataPath:imeModeCHT]
populateWithTemplate:kUserDataTemplateName
extension:kTemplateExtension])
return NO;
if (![self ensureFileExists:[self userAssociatedPhrasesDataPath:imeModeCHS]
populateWithTemplate:kUserAssDataTemplateName
extension:kTemplateExtension])
return NO;
if (![self ensureFileExists:[self userAssociatedPhrasesDataPath:imeModeCHT]
populateWithTemplate:kUserAssDataTemplateName
extension:kTemplateExtension])
return NO;
if (![self ensureFileExists:[self excludedPhrasesDataPath:imeModeCHS]
populateWithTemplate:kExcludedPhrasesvChewingTemplateName
extension:kTemplateExtension])
return NO;
if (![self ensureFileExists:[self excludedPhrasesDataPath:imeModeCHT]
populateWithTemplate:kExcludedPhrasesvChewingTemplateName
extension:kTemplateExtension])
return NO;
if (![self ensureFileExists:[self phraseReplacementDataPath:imeModeCHS]
populateWithTemplate:kPhraseReplacementTemplateName
extension:kTemplateExtension])
return NO;
if (![self ensureFileExists:[self phraseReplacementDataPath:imeModeCHT]
populateWithTemplate:kPhraseReplacementTemplateName
extension:kTemplateExtension])
return NO;
if (![self ensureFileExists:[self userSymbolDataPath:imeModeCHT]
populateWithTemplate:kUserSymbolDataTemplateName
extension:kTemplateExtension])
return NO;
if (![self ensureFileExists:[self userSymbolDataPath:imeModeCHS]
populateWithTemplate:kUserSymbolDataTemplateName
extension:kTemplateExtension])
return NO;
return YES;
}
// 這個函數無法遷移至 Swift
+ (BOOL)checkIfUserPhraseExist:(NSString *)userPhrase
inputMode:(InputMode)mode
key:(NSString *)key NS_SWIFT_NAME(checkIfUserPhraseExist(userPhrase:mode:key:))
@ -297,144 +172,51 @@ static void LTLoadLanguageModelFile(NSString *filenameWithoutExtension, vChewing
return NO;
}
+ (BOOL)writeUserPhrase:(NSString *)userPhrase
inputMode:(InputMode)mode
areWeDuplicating:(BOOL)areWeDuplicating
areWeDeleting:(BOOL)areWeDeleting
// 這個函數無法遷移至 Swift
+ (void)consolidateGivenFile:(NSString *)path shouldCheckPragma:(BOOL)shouldCheckPragma
{
if (![self checkIfUserLanguageModelFilesExist])
{
return NO;
}
// BOOL addLineBreakAtFront = NO;
NSString *path = areWeDeleting ? [self excludedPhrasesDataPath:mode] : [self userPhrasesDataPath:mode];
NSMutableString *currentMarkedPhrase = [NSMutableString string];
// if (addLineBreakAtFront) {
// [currentMarkedPhrase appendString:@"\n"];
// }
[currentMarkedPhrase appendString:userPhrase];
if (areWeDuplicating && !areWeDeleting)
{
// Do not use ASCII characters to comment here.
// Otherwise, it will be scrambled by cnvHYPYtoBPMF module shipped in the vChewing Phrase Editor.
[currentMarkedPhrase appendString:@"\t#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"];
}
[currentMarkedPhrase appendString:@"\n"];
NSFileHandle *writeFile = [NSFileHandle fileHandleForUpdatingAtPath:path];
if (!writeFile)
{
return NO;
}
[writeFile seekToEndOfFile];
NSData *data = [currentMarkedPhrase dataUsingEncoding:NSUTF8StringEncoding];
[writeFile writeData:data];
[writeFile closeFile];
// We enforce the format consolidation here, since the pragma header will let the UserPhraseLM bypasses the
// consolidating process on load.
vChewing::LMConsolidator::ConsolidateContent([path UTF8String], false);
// We use FSEventStream to monitor the change of the user phrase folder,
// so we don't have to load data here unless FSEventStream is disabled by user.
if (!mgrPrefs.shouldAutoReloadUserDataFiles)
{
[self loadUserPhrases];
}
return YES;
}
+ (NSString *)dataFolderPath:(bool)isDefaultFolder
{
// 此處不能用「~」來取代當前使用者目錄名稱。不然的話,一旦輸入法被系統的沙箱干預的話,則反而會定位到沙箱目錄內。
NSString *appSupportPath = [NSFileManager.defaultManager URLsForDirectory:NSApplicationSupportDirectory
inDomains:NSUserDomainMask][0].path;
NSString *userDictPath = [appSupportPath stringByAppendingPathComponent:@"vChewing"].stringByExpandingTildeInPath;
if (mgrPrefs.userDataFolderSpecified.stringByExpandingTildeInPath == userDictPath || isDefaultFolder)
{
return userDictPath;
}
if ([mgrPrefs ifSpecifiedUserDataPathExistsInPlist])
{
if ([self checkIfSpecifiedUserDataFolderValid:mgrPrefs.userDataFolderSpecified.stringByExpandingTildeInPath])
{
return mgrPrefs.userDataFolderSpecified.stringByExpandingTildeInPath;
}
else
{
[NSUserDefaults.standardUserDefaults removeObjectForKey:@"UserDataFolderSpecified"];
}
}
return userDictPath;
}
+ (NSString *)userPhrasesDataPath:(InputMode)mode;
{
NSString *fileName = [mode isEqualToString:imeModeCHT] ? @"userdata-cht.txt" : @"userdata-chs.txt";
return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName];
}
+ (NSString *)userSymbolDataPath:(InputMode)mode;
{
NSString *fileName =
[mode isEqualToString:imeModeCHT] ? @"usersymbolphrases-cht.txt" : @"usersymbolphrases-chs.txt";
return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName];
}
+ (NSString *)userAssociatedPhrasesDataPath:(InputMode)mode;
{
NSString *fileName =
[mode isEqualToString:imeModeCHT] ? @"associatedPhrases-cht.txt" : @"associatedPhrases-chs.txt";
return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName];
}
+ (NSString *)excludedPhrasesDataPath:(InputMode)mode;
{
NSString *fileName = [mode isEqualToString:imeModeCHT] ? @"exclude-phrases-cht.txt" : @"exclude-phrases-chs.txt";
return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName];
}
+ (NSString *)phraseReplacementDataPath:(InputMode)mode;
{
NSString *fileName =
[mode isEqualToString:imeModeCHT] ? @"phrases-replacement-cht.txt" : @"phrases-replacement-chs.txt";
return [[self dataFolderPath:false] stringByAppendingPathComponent:fileName];
vChewing::LMConsolidator::ConsolidateContent([path UTF8String], shouldCheckPragma);
}
// 這個函數無法遷移至 Swift
+ (vChewing::LMInstantiator *)lmCHT
{
return &gLangModelCHT;
}
// 這個函數無法遷移至 Swift
+ (vChewing::LMInstantiator *)lmCHS
{
return &gLangModelCHS;
}
// 這個函數無法遷移至 Swift
+ (vChewing::UserOverrideModel *)userOverrideModelCHT
{
return &gUserOverrideModelCHT;
}
// 這個函數無法遷移至 Swift
+ (vChewing::UserOverrideModel *)userOverrideModelCHS
{
return &gUserOverrideModelCHS;
}
// 這個函數無法遷移至 Swift
+ (void)setPhraseReplacementEnabled:(BOOL)phraseReplacementEnabled
{
gLangModelCHT.setPhraseReplacementEnabled(phraseReplacementEnabled);
gLangModelCHS.setPhraseReplacementEnabled(phraseReplacementEnabled);
}
// 這個函數無法遷移至 Swift
+ (void)setCNSEnabled:(BOOL)cnsEnabled
{
gLangModelCHT.setCNSEnabled(cnsEnabled);
gLangModelCHS.setCNSEnabled(cnsEnabled);
}
// 這個函數無法遷移至 Swift
+ (void)setSymbolEnabled:(BOOL)symbolEnabled
{
gLangModelCHT.setSymbolEnabled(symbolEnabled);

View File

@ -0,0 +1,242 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. 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 above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
@objc extension mgrLangModel {
// MARK: -
static func getBundleDataPath(_ filenameSansExt: String) -> String {
return Bundle.main.path(forResource: filenameSansExt, ofType: "txt")!
}
// MARK: - 使
// Swift appendingPathComponent URL .path
static func userPhrasesDataPath(_ mode: InputMode) -> String {
let fileName = (mode == InputMode.imeModeCHT) ? "userdata-cht.txt" : "userdata-chs.txt"
return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
}
static func userSymbolDataPath(_ mode: InputMode) -> String {
let fileName = (mode == InputMode.imeModeCHT) ? "usersymbolphrases-cht.txt" : "usersymbolphrases-chs.txt"
return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
}
static func userAssociatedPhrasesDataPath(_ mode: InputMode) -> String {
let fileName = (mode == InputMode.imeModeCHT) ? "associatedPhrases-cht.txt" : "associatedPhrases-chs.txt"
return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
}
static func excludedPhrasesDataPath(_ mode: InputMode) -> String {
let fileName = (mode == InputMode.imeModeCHT) ? "exclude-phrases-cht.txt" : "exclude-phrases-chs.txt"
return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
}
static func phraseReplacementDataPath(_ mode: InputMode) -> String {
let fileName = (mode == InputMode.imeModeCHT) ? "phrases-replacement-cht.txt" : "phrases-replacement-chs.txt"
return URL(fileURLWithPath: self.dataFolderPath(isDefaultFolder: false)).appendingPathComponent(fileName).path
}
// MARK: - 使
static func ensureFileExists(
_ filePath: String, populateWithTemplate templateBasename: String = "1145141919810",
extension ext: String = "txt"
) -> Bool {
if !FileManager.default.fileExists(atPath: filePath) {
let templateURL = Bundle.main.url(forResource: templateBasename, withExtension: ext)
var templateData = Data("".utf8)
if templateBasename != "" {
do {
try templateData = Data(contentsOf: templateURL ?? URL(fileURLWithPath: ""))
} catch {
templateData = Data("".utf8)
}
do {
try templateData.write(to: URL(fileURLWithPath: filePath))
} catch {
IME.prtDebugIntel("Failed to write file")
return false
}
}
}
return true
}
static func chkUserLMFilesExist(_ mode: InputMode) -> Bool {
if !self.checkIfUserDataFolderExists() {
return false
}
if !ensureFileExists(userPhrasesDataPath(mode))
|| !ensureFileExists(userAssociatedPhrasesDataPath(mode))
|| !ensureFileExists(excludedPhrasesDataPath(mode))
|| !ensureFileExists(phraseReplacementDataPath(mode))
|| !ensureFileExists(userSymbolDataPath(mode))
{
return false
}
return true
}
// MARK: - 使
//
static func checkIfSpecifiedUserDataFolderValid(_ folderPath: String?) -> Bool {
var isFolder = ObjCBool(false)
let folderExist = FileManager.default.fileExists(atPath: folderPath ?? "", isDirectory: &isFolder)
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
//
//
var folderPath = folderPath // Convert the incoming constant to a variable.
if isFolder.boolValue {
folderPath?.ensureTrailingSlash()
}
let isFolderWritable = FileManager.default.isWritableFile(atPath: folderPath ?? "")
if ((folderExist && !isFolder.boolValue) || !folderExist) || !isFolderWritable {
return false
}
return true
}
//
//
static func checkIfUserDataFolderExists() -> Bool {
let folderPath = mgrLangModel.dataFolderPath(isDefaultFolder: false)
var isFolder = ObjCBool(false)
var folderExist = FileManager.default.fileExists(atPath: folderPath, isDirectory: &isFolder)
// The above "&" mutates the "isFolder" value to the real one received by the "folderExist".
//
//
//
if folderExist && !isFolder.boolValue {
do {
if self.dataFolderPath(isDefaultFolder: false)
== self.dataFolderPath(isDefaultFolder: true)
{
let formatter = DateFormatter.init()
formatter.dateFormat = "YYYYMMDD-HHMM'Hrs'-ss's'"
let dirAlternative = folderPath + formatter.string(from: Date())
try FileManager.default.moveItem(atPath: folderPath, toPath: dirAlternative)
} else {
throw folderPath
}
} catch {
print("Failed to make path available at: \(error)")
return false
}
folderExist = false
}
if !folderExist {
do {
try FileManager.default.createDirectory(
atPath: folderPath,
withIntermediateDirectories: true,
attributes: nil)
} catch {
print("Failed to create folder: \(error)")
return false
}
}
return true
}
// MARK: - 使 mgrPrefs
// mgrPrefs
static func dataFolderPath(isDefaultFolder: Bool) -> String {
let appSupportPath = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].path
var userDictPathSpecified = (mgrPrefs.userDataFolderSpecified as NSString).expandingTildeInPath
var userDictPathDefault =
(URL(fileURLWithPath: appSupportPath).appendingPathComponent("vChewing").path as NSString)
.expandingTildeInPath
userDictPathDefault.ensureTrailingSlash()
userDictPathSpecified.ensureTrailingSlash()
if (userDictPathSpecified == userDictPathDefault)
|| isDefaultFolder
{
return userDictPathDefault
}
if mgrPrefs.ifSpecifiedUserDataPathExistsInPlist() {
if mgrLangModel.checkIfSpecifiedUserDataFolderValid(userDictPathSpecified) {
return userDictPathSpecified
} else {
UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified")
}
}
return userDictPathDefault
}
// MARK: - 使
static func writeUserPhrase(
_ userPhrase: String?, inputMode mode: InputMode, areWeDuplicating: Bool, areWeDeleting: Bool
) -> Bool {
if var currentMarkedPhrase: String = userPhrase {
if !self.chkUserLMFilesExist(InputMode.imeModeCHS)
|| !self.chkUserLMFilesExist(InputMode.imeModeCHT)
{
return false
}
let path = areWeDeleting ? self.excludedPhrasesDataPath(mode) : self.userPhrasesDataPath(mode)
if areWeDuplicating && !areWeDeleting {
// Do not use ASCII characters to comment here.
// Otherwise, it will be scrambled by cnvHYPYtoBPMF
// module shipped in the vChewing Phrase Editor.
currentMarkedPhrase += "\t#𝙾𝚟𝚎𝚛𝚛𝚒𝚍𝚎"
}
currentMarkedPhrase += "\n"
if let writeFile = FileHandle(forUpdatingAtPath: path),
let data = currentMarkedPhrase.data(using: .utf8)
{
writeFile.seekToEndOfFile()
writeFile.write(data)
writeFile.closeFile()
} else {
return false
}
// We enforce the format consolidation here, since the pragma header
// will let the UserPhraseLM bypasses the consolidating process on load.
self.consolidate(givenFile: path, shouldCheckPragma: false)
// We use FSEventStream to monitor possible changes of the user phrase folder, hence the
// lack of the needs of manually load data here unless FSEventStream is disabled by user.
if !mgrPrefs.shouldAutoReloadUserDataFiles {
self.loadUserPhrases()
}
return true
}
return false
}
}

View File

@ -1,5 +1,6 @@
// Copyright (c) 2022 and onwards Isaac Xen (MIT License).
// All possible vChewing-specific modifications are (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

View File

@ -73,3 +73,71 @@
"catDoubleTableLines" = "DoubleTableLines";
"catFillingBlocks" = "FillingBlocks";
"catLineSegments" = "LineSegments";
// SwiftUI Preferences
"(Shift+)Space:" = "(Shift+)Space:";
"An accomodation for elder computer users." = "An accomodation for elder computer users.";
"Apple ABC (equivalent to English US)" = "Apple ABC (equivalent to English US)";
"Apple Chewing - Dachen" = "Apple Chewing - Dachen";
"Apple Chewing - Eten Traditional" = "Apple Chewing - Eten Traditional";
"Apple Dynamic Bopomofo Basic Keyboard Layouts (Dachen & Eten Traditional) must match the Dachen parser in order to be functional." = "Apple Dynamic Bopomofo Basic Keyboard Layouts (Dachen & Eten Traditional) must match the Dachen parser in order to be functional.";
"Auto-convert traditional Chinese glyphs to JIS Shinjitai characters" = "Auto-convert traditional Chinese glyphs to JIS Shinjitai characters";
"Auto-convert traditional Chinese glyphs to KangXi characters" = "Auto-convert traditional Chinese glyphs to KangXi characters";
"Automatically reload user data files if changes detected" = "Automatically reload user data files if changes detected";
"Basic Keyboard Layout:" = "Basic Keyboard Layout:";
"Candidate Layout:" = "Candidate Layout:";
"Candidate Size:" = "Candidate Size:";
"Change user interface language (will reboot the IME)." = "Change user interface language (will reboot the IME).";
"Check for updates automatically" = "Check for updates automatically";
"Choose candidate font size for better visual clarity." = "Choose candidate font size for better visual clarity.";
"Choose or hit Enter to confim your prefered keys for selecting candidates." = "Choose or hit Enter to confim your prefered keys for selecting candidates.";
"Choose the behavior of (Shift+)Space key in the candidate window." = "Choose the behavior of (Shift+)Space key in the candidate window.";
"Choose the behavior of (Shift+)Tab key in the candidate window." = "Choose the behavior of (Shift+)Tab key in the candidate window.";
"Choose the cursor position where you want to list possible candidates." = "Choose the cursor position where you want to list possible candidates.";
"Choose the macOS-level basic keyboard layout." = "Choose the macOS-level basic keyboard layout.";
"Choose the phonetic layout for Mandarin parser." = "Choose the phonetic layout for Mandarin parser.";
"Choose your desired user data folder path. Will be omitted if invalid." = "Choose your desired user data folder path. Will be omitted if invalid.";
"Choose your preferred layout of the candidate window." = "Choose your preferred layout of the candidate window.";
"Cursor Selection:" = "Cursor Selection:";
"Dachen (Microsoft Standard / Wang / 01, etc.)" = "Dachen (Microsoft Standard / Wang / 01, etc.)";
"Debug Mode" = "Debug Mode";
"Dictionary" = "Dictionary";
"Emulating select-candidate-per-character mode" = "Emulating select-candidate-per-character mode";
"Enable CNS11643 Support (2022-01-27)" = "Enable CNS11643 Support (2022-01-27)";
"Enable Space key for calling candidate window" = "Enable Space key for calling candidate window";
"Enable symbol input support (incl. certain emoji symbols)" = "Enable symbol input support (incl. certain emoji symbols)";
"English" = "English";
"Eten 26" = "Eten 26";
"Eten Traditional" = "Eten Traditional";
"Experience" = "Experience";
"Fake Seigyou" = "Fake Seigyou";
"Follow OS settings" = "Follow OS settings";
"for cycling candidates" = "for cycling candidates";
"for cycling pages" = "for cycling pages";
"General" = "General";
"Hanyu Pinyin with Numeral Intonation" = "Hanyu Pinyin with Numeral Intonation";
"Horizontal" = "Horizontal";
"Hsu" = "Hsu";
"IBM" = "IBM";
"Japanese" = "Japanese";
"Keyboard" = "Keyboard";
"Misc Settings:" = "Misc Settings:";
"MiTAC" = "MiTAC";
"Non-QWERTY alphanumeral keyboard layouts are for Hanyu Pinyin parser only." = "Non-QWERTY alphanumeral keyboard layouts are for Hanyu Pinyin parser only.";
"Output Settings:" = "Output Settings:";
"Phonetic Parser:" = "Phonetic Parser:";
"Push the cursor to the front of the phrase after selection" = "Push the cursor to the front of the phrase after selection";
"Selection Keys:" = "Selection Keys:";
"Show page buttons in candidate window" = "Show page buttons in candidate window";
"Simplified Chinese" = "Simplified Chinese";
"Space & ESC Key:" = "Space & ESC Key:";
"Space to +cycle candidates, Shift+Space to +cycle pages" = "Space to +cycle candidates, Shift+Space to +cycle pages";
"Space to +cycle pages, Shift+Space to +cycle candidates" = "Space to +cycle pages, Shift+Space to +cycle candidates";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "Stop farting (when typed phonetic combination is invalid, etc.)";
"to the front of the phrase (like Matsushita Hanin IME)" = "to the front of the phrase (like Matsushita Hanin IME)";
"to the rear of the phrase (like MS New-Phonetic IME)" = "to the rear of the phrase (like MS New-Phonetic IME)";
"Traditional Chinese" = "Traditional Chinese";
"Typing Style:" = "Typing Style:";
"UI Language:" = "UI Language:";
"Use ESC key to clear the entire input buffer" = "Use ESC key to clear the entire input buffer";
"Vertical" = "Vertical";

View File

@ -73,3 +73,71 @@
"catDoubleTableLines" = "DoubleTableLines";
"catFillingBlocks" = "FillingBlocks";
"catLineSegments" = "LineSegments";
// SwiftUI Preferences
"(Shift+)Space:" = "(Shift+)Space:";
"An accomodation for elder computer users." = "An accomodation for elder computer users.";
"Apple ABC (equivalent to English US)" = "Apple ABC (equivalent to English US)";
"Apple Chewing - Dachen" = "Apple Chewing - Dachen";
"Apple Chewing - Eten Traditional" = "Apple Chewing - Eten Traditional";
"Apple Dynamic Bopomofo Basic Keyboard Layouts (Dachen & Eten Traditional) must match the Dachen parser in order to be functional." = "Apple Dynamic Bopomofo Basic Keyboard Layouts (Dachen & Eten Traditional) must match the Dachen parser in order to be functional.";
"Auto-convert traditional Chinese glyphs to JIS Shinjitai characters" = "Auto-convert traditional Chinese glyphs to JIS Shinjitai characters";
"Auto-convert traditional Chinese glyphs to KangXi characters" = "Auto-convert traditional Chinese glyphs to KangXi characters";
"Automatically reload user data files if changes detected" = "Automatically reload user data files if changes detected";
"Basic Keyboard Layout:" = "Basic Keyboard Layout:";
"Candidate Layout:" = "Candidate Layout:";
"Candidate Size:" = "Candidate Size:";
"Change user interface language (will reboot the IME)." = "Change user interface language (will reboot the IME).";
"Check for updates automatically" = "Check for updates automatically";
"Choose candidate font size for better visual clarity." = "Choose candidate font size for better visual clarity.";
"Choose or hit Enter to confim your prefered keys for selecting candidates." = "Choose or hit Enter to confim your prefered keys for selecting candidates.";
"Choose the behavior of (Shift+)Space key in the candidate window." = "Choose the behavior of (Shift+)Space key in the candidate window.";
"Choose the behavior of (Shift+)Tab key in the candidate window." = "Choose the behavior of (Shift+)Tab key in the candidate window.";
"Choose the cursor position where you want to list possible candidates." = "Choose the cursor position where you want to list possible candidates.";
"Choose the macOS-level basic keyboard layout." = "Choose the macOS-level basic keyboard layout.";
"Choose the phonetic layout for Mandarin parser." = "Choose the phonetic layout for Mandarin parser.";
"Choose your desired user data folder path. Will be omitted if invalid." = "Choose your desired user data folder path. Will be omitted if invalid.";
"Choose your preferred layout of the candidate window." = "Choose your preferred layout of the candidate window.";
"Cursor Selection:" = "Cursor Selection:";
"Dachen (Microsoft Standard / Wang / 01, etc.)" = "Dachen (Microsoft Standard / Wang / 01, etc.)";
"Debug Mode" = "Debug Mode";
"Dictionary" = "Dictionary";
"Emulating select-candidate-per-character mode" = "Emulating select-candidate-per-character mode";
"Enable CNS11643 Support (2022-01-27)" = "Enable CNS11643 Support (2022-01-27)";
"Enable Space key for calling candidate window" = "Enable Space key for calling candidate window";
"Enable symbol input support (incl. certain emoji symbols)" = "Enable symbol input support (incl. certain emoji symbols)";
"English" = "English";
"Eten 26" = "Eten 26";
"Eten Traditional" = "Eten Traditional";
"Experience" = "Experience";
"Fake Seigyou" = "Fake Seigyou";
"Follow OS settings" = "Follow OS settings";
"for cycling candidates" = "for cycling candidates";
"for cycling pages" = "for cycling pages";
"General" = "General";
"Hanyu Pinyin with Numeral Intonation" = "Hanyu Pinyin with Numeral Intonation";
"Horizontal" = "Horizontal";
"Hsu" = "Hsu";
"IBM" = "IBM";
"Japanese" = "Japanese";
"Keyboard" = "Keyboard";
"Misc Settings:" = "Misc Settings:";
"MiTAC" = "MiTAC";
"Non-QWERTY alphanumeral keyboard layouts are for Hanyu Pinyin parser only." = "Non-QWERTY alphanumeral keyboard layouts are for Hanyu Pinyin parser only.";
"Output Settings:" = "Output Settings:";
"Phonetic Parser:" = "Phonetic Parser:";
"Push the cursor to the front of the phrase after selection" = "Push the cursor to the front of the phrase after selection";
"Selection Keys:" = "Selection Keys:";
"Show page buttons in candidate window" = "Show page buttons in candidate window";
"Simplified Chinese" = "Simplified Chinese";
"Space & ESC Key:" = "Space & ESC Key:";
"Space to +cycle candidates, Shift+Space to +cycle pages" = "Space to +cycle candidates, Shift+Space to +cycle pages";
"Space to +cycle pages, Shift+Space to +cycle candidates" = "Space to +cycle pages, Shift+Space to +cycle candidates";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "Stop farting (when typed phonetic combination is invalid, etc.)";
"to the front of the phrase (like Matsushita Hanin IME)" = "to the front of the phrase (like Matsushita Hanin IME)";
"to the rear of the phrase (like MS New-Phonetic IME)" = "to the rear of the phrase (like MS New-Phonetic IME)";
"Traditional Chinese" = "Traditional Chinese";
"Typing Style:" = "Typing Style:";
"UI Language:" = "UI Language:";
"Use ESC key to clear the entire input buffer" = "Use ESC key to clear the entire input buffer";
"Vertical" = "Vertical";

View File

@ -73,3 +73,71 @@
"catDoubleTableLines" = "双線";
"catFillingBlocks" = "ブロック";
"catLineSegments" = "線分";
// SwiftUI Preferences
"(Shift+)Space:" = "(Shift+)Space:";
"An accomodation for elder computer users." = "年配なるユーザーのために提供した機能である。";
"Apple ABC (equivalent to English US)" = "Apple ABC (Apple U.S. キーボードと同じ)";
"Apple Chewing - Dachen" = "Apple 大千注音キーボード";
"Apple Chewing - Eten Traditional" = "Apple 倚天傳統キーボード";
"Apple Dynamic Bopomofo Basic Keyboard Layouts (Dachen & Eten Traditional) must match the Dachen parser in order to be functional." = "Apple 動態注音キーボード (大千と倚天伝統) を使うには、共通語分析器の配列を大千と設定すべきである。";
"Auto-convert traditional Chinese glyphs to JIS Shinjitai characters" = "入力した繁体字を日文 JIS 新字体と自動変換";
"Auto-convert traditional Chinese glyphs to KangXi characters" = "入力した繁体字を康熙字体と自動変換";
"Automatically reload user data files if changes detected" = "ユーザー辞書データの変更を自動検出し、自動的に再読込";
"Basic Keyboard Layout:" = "基礎キーボード:";
"Candidate Layout:" = "入力候補陳列の仕様";
"Candidate Size:" = "候補文字の字号:";
"Change user interface language (will reboot the IME)." = "アプリ表示用言語をご指定下さい、そして入力アプリは自動的に再起動。";
"Check for updates automatically" = "アプリの更新通知を受く";
"Choose candidate font size for better visual clarity." = "入力候補陳列の候補文字の字号をご指定ください。";
"Choose or hit Enter to confim your prefered keys for selecting candidates." = "お好きなる言選り用キー陣列をご指定ください。新しい組み合わせは Enter で効かす。";
"Choose the behavior of (Shift+)Space key in the candidate window." = "入力候補陳列での (Shift+)Space キーの輪番切替対象をご指定ください。";
"Choose the behavior of (Shift+)Tab key in the candidate window." = "入力候補陳列での (Shift+)Tab キーの輪番切替対象をご指定ください。";
"Choose the cursor position where you want to list possible candidates." = "カーソルはどこで入力候補を呼び出すかとご指定ださい。";
"Choose the macOS-level basic keyboard layout." = "macOS 基礎キーボード配置をご指定ください。";
"Choose the phonetic layout for Mandarin parser." = "共通語分析器の注音配列をご指定ください。";
"Choose your desired user data folder path. Will be omitted if invalid." = "欲しがるユーザー辞書保存先をご指定ください。無効なる保存先設定は省かれる。";
"Choose your preferred layout of the candidate window." = "入力候補陳列の仕様をご指定ください。";
"Cursor Selection:" = "カーソル候補呼出:";
"Dachen (Microsoft Standard / Wang / 01, etc.)" = "大千配列 (Microsoft 標準・王安・零壹など)";
"Debug Mode" = "欠陥辿着モード";
"Dictionary" = "辞書設定";
"Emulating select-candidate-per-character mode" = "漢字1つづつ全候補選択入力モード";
"Enable CNS11643 Support (2022-01-27)" = "全字庫モード // 入力可能な漢字数を倍増す (2022-01-07)";
"Enable Space key for calling candidate window" = "Space キーで入力候補を呼び出す";
"Enable symbol input support (incl. certain emoji symbols)" = "僅かなる絵文字も含む符号入力サポートを起用";
"English" = "英語";
"Eten 26" = "倚天形忘れ配列 (26 キー)";
"Eten Traditional" = "倚天伝統配列";
"Experience" = "体験設定";
"Fake Seigyou" = "偽精業配列";
"Follow OS settings" = "システム設定に準ず";
"for cycling candidates" = "候補文字そのもの";
"for cycling pages" = "候補陳列ページ";
"General" = "全般設定";
"Hanyu Pinyin with Numeral Intonation" = "漢語弁音 (ローマ字+数字音調)";
"Horizontal" = "横型陳列";
"Hsu" = "許氏国音自然配列";
"IBM" = "IBM 配列";
"Japanese" = "和語";
"Keyboard" = "配列設定";
"Misc Settings:" = "他の設定:";
"MiTAC" = "神通配列";
"Non-QWERTY alphanumeral keyboard layouts are for Hanyu Pinyin parser only." = "QWERTY 以外の英数キーボードは漢語弁音以外の配列に不適用。";
"Output Settings:" = "出力設定:";
"Phonetic Parser:" = "注音配列:";
"Push the cursor to the front of the phrase after selection" = "候補選択の直後、すぐカーソルを単語の向こうに推し進める";
"Selection Keys:" = "言選り用キー:";
"Show page buttons in candidate window" = "入力候補陳列の側にページボタンを表示";
"Simplified Chinese" = "簡体中国語";
"Space & ESC Key:" = "ESC と Space:";
"Space to +cycle candidates, Shift+Space to +cycle pages" = "Shift+Space で次のページ、Space で次の候補文字を";
"Space to +cycle pages, Shift+Space to +cycle candidates" = "Space で次のページ、Shift+Space で次の候補文字を";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "マナーモード // 外すと入力間違った時に変な声が出る";
"to the front of the phrase (like Matsushita Hanin IME)" = "単語の前で // パナソニック漢音のやり方";
"to the rear of the phrase (like MS New-Phonetic IME)" = "単語の後で // Microsoft 新注音のやり方";
"Traditional Chinese" = "繁体中国語";
"Typing Style:" = "入力習慣:";
"UI Language:" = "表示用言語:";
"Use ESC key to clear the entire input buffer" = "ESC キーで入力緩衝列を消す";
"Vertical" = "縦型陳列";

View File

@ -73,3 +73,71 @@
"catDoubleTableLines" = "双线";
"catFillingBlocks" = "填色";
"catLineSegments" = "线段";
// SwiftUI Preferences
"(Shift+)Space:" = "(Shift+)空格键:";
"An accomodation for elder computer users." = "针对年长使用者的习惯而提供。";
"Apple ABC (equivalent to English US)" = "Apple ABC (与 Apple 美规键盘等价)";
"Apple Chewing - Dachen" = "Apple 大千注音键盘配列";
"Apple Chewing - Eten Traditional" = "Apple 倚天传统键盘配列";
"Apple Dynamic Bopomofo Basic Keyboard Layouts (Dachen & Eten Traditional) must match the Dachen parser in order to be functional." = "Apple 动态注音键盘布局(大千与倚天)要求普通话/国音分析器得配置为大千配列。";
"Auto-convert traditional Chinese glyphs to JIS Shinjitai characters" = "自动将繁体中文字转为日文 JIS 新字体";
"Auto-convert traditional Chinese glyphs to KangXi characters" = "自动将繁体中文字转为康熙字";
"Automatically reload user data files if changes detected" = "自动检测并载入使用者语汇档案变更";
"Basic Keyboard Layout:" = "基础键盘布局:";
"Candidate Layout:" = "候选字窗布局:";
"Candidate Size:" = "候选字窗字号:";
"Change user interface language (will reboot the IME)." = "变更输入法介面语言,会自动重启输入法。";
"Check for updates automatically" = "自动检查软体更新";
"Choose candidate font size for better visual clarity." = "变更候选字窗的字型大小。";
"Choose or hit Enter to confim your prefered keys for selecting candidates." = "请选择您所偏好的用来选字的按键组合。自订组合需敲 Enter 键生效。";
"Choose the behavior of (Shift+)Space key in the candidate window." = "指定 (Shift+)空格键 在选字窗内的轮替操作对象。";
"Choose the behavior of (Shift+)Tab key in the candidate window." = "指定 (Shift+)Tab 在选字窗内的轮替操作对象。";
"Choose the cursor position where you want to list possible candidates." = "请选择用以触发选字的游标相对位置。";
"Choose the macOS-level basic keyboard layout." = "请选择 macOS 基础键盘布局。";
"Choose the phonetic layout for Mandarin parser." = "请指定普通话/国音分析器所使用的注音配列。";
"Choose your desired user data folder path. Will be omitted if invalid." = "请在此指定您想指定的使用者语汇档案目录。无效值会被忽略。";
"Choose your preferred layout of the candidate window." = "选择您所偏好的候选字窗布局。";
"Cursor Selection:" = "选字游标:";
"Dachen (Microsoft Standard / Wang / 01, etc.)" = "大千配列 (微软标准/王安/零壹/仲鼎/国乔)";
"Debug Mode" = "侦错模式";
"Dictionary" = "辞典";
"Emulating select-candidate-per-character mode" = "模拟 90 年代前期注音逐字选字输入风格";
"Enable CNS11643 Support (2022-01-27)" = "启用 CNS11643 全字库支援 (2022-01-07)";
"Enable Space key for calling candidate window" = "敲空格键以呼出候选字窗";
"Enable symbol input support (incl. certain emoji symbols)" = "启用包括少许绘文字在内的符号输入支援";
"English" = "英语";
"Eten 26" = "倚天忘形配列 (26 键)";
"Eten Traditional" = "倚天传统配列";
"Experience" = "体验";
"Fake Seigyou" = "伪精业配列";
"Follow OS settings" = "依系统设定";
"for cycling candidates" = "轮替候选字";
"for cycling pages" = "轮替页面";
"General" = "通用";
"Hanyu Pinyin with Numeral Intonation" = "汉语拼音+数字标调";
"Horizontal" = "横向布局";
"Hsu" = "许氏国音自然配列";
"IBM" = "IBM 配列";
"Japanese" = "和语";
"Keyboard" = "键盘";
"Misc Settings:" = "杂项:";
"MiTAC" = "神通配列";
"Non-QWERTY alphanumeral keyboard layouts are for Hanyu Pinyin parser only." = "QWERTY 以外的英数布局是为了汉语拼音配列使用者而准备的。";
"Output Settings:" = "输出设定:";
"Phonetic Parser:" = "注音配列:";
"Push the cursor to the front of the phrase after selection" = "在选字后将游标置于该字词的前方";
"Selection Keys:" = "选字键:";
"Show page buttons in candidate window" = "在选字窗内显示翻页按钮";
"Simplified Chinese" = "简体中文";
"Space & ESC Key:" = "ESC 与空格键:";
"Space to +cycle candidates, Shift+Space to +cycle pages" = "Shift+空格键 换下一页,空格键 换选下一个后选字";
"Space to +cycle pages, Shift+Space to +cycle candidates" = "空格键 换下一页Shift+空格键 换选下一个后选字";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "不要放屁 // 例:当输入的音韵有误时,等";
"to the front of the phrase (like Matsushita Hanin IME)" = "将游标置于词语前方 // 松下汉音风格";
"to the rear of the phrase (like MS New-Phonetic IME)" = "将游标置于词语后方 // 微软新注音风格";
"Traditional Chinese" = "繁体中文";
"Typing Style:" = "输入风格:";
"UI Language:" = "介面语言:";
"Use ESC key to clear the entire input buffer" = "敲 ESC 键以清空整个组字缓冲区";
"Vertical" = "纵向布局";

View File

@ -73,3 +73,71 @@
"catDoubleTableLines" = "雙線";
"catFillingBlocks" = "填色";
"catLineSegments" = "線段";
// SwiftUI Preferences
"(Shift+)Space:" = "(Shift+)空格鍵:";
"An accomodation for elder computer users." = "針對年長使用者的習慣而提供。";
"Apple ABC (equivalent to English US)" = "Apple ABC (與 Apple 美規鍵盤等價)";
"Apple Chewing - Dachen" = "Apple 大千注音鍵盤佈局";
"Apple Chewing - Eten Traditional" = "Apple 倚天傳統鍵盤佈局";
"Apple Dynamic Bopomofo Basic Keyboard Layouts (Dachen & Eten Traditional) must match the Dachen parser in order to be functional." = "Apple 動態注音鍵盤佈局(大千與倚天)要求普通話/國音分析器得配置為大千配列。";
"Auto-convert traditional Chinese glyphs to JIS Shinjitai characters" = "自動將繁體中文字轉為日文 JIS 新字體";
"Auto-convert traditional Chinese glyphs to KangXi characters" = "自動將繁體中文字轉為康熙字";
"Automatically reload user data files if changes detected" = "自動檢測並載入使用者語彙檔案變更";
"Basic Keyboard Layout:" = "基礎鍵盤佈局:";
"Candidate Layout:" = "候選字窗佈局:";
"Candidate Size:" = "候選字窗字號:";
"Change user interface language (will reboot the IME)." = "變更輸入法介面語言,會自動重啟輸入法。";
"Check for updates automatically" = "自動檢查軟體更新";
"Choose candidate font size for better visual clarity." = "變更候選字窗的字型大小。";
"Choose or hit Enter to confim your prefered keys for selecting candidates." = "請選擇您所偏好的用來選字的按鍵組合。自訂組合需敲 Enter 鍵生效。";
"Choose the behavior of (Shift+)Space key in the candidate window." = "指定 (Shift+)空格鍵 在選字窗內的輪替操作對象。";
"Choose the behavior of (Shift+)Tab key in the candidate window." = "指定 (Shift+)Tab 在選字窗內的輪替操作對象。";
"Choose the cursor position where you want to list possible candidates." = "請選擇用以觸發選字的游標相對位置。";
"Choose the macOS-level basic keyboard layout." = "請選擇 macOS 基礎鍵盤佈局。";
"Choose the phonetic layout for Mandarin parser." = "請指定普通話/國音分析器所使用的注音配列。";
"Choose your desired user data folder path. Will be omitted if invalid." = "請在此指定您想指定的使用者語彙檔案目錄。無效值會被忽略。";
"Choose your preferred layout of the candidate window." = "選擇您所偏好的候選字窗佈局。";
"Cursor Selection:" = "選字游標:";
"Dachen (Microsoft Standard / Wang / 01, etc.)" = "大千配列 (微軟標準/王安/零壹/仲鼎/國喬)";
"Debug Mode" = "偵錯模式";
"Dictionary" = "辭典";
"Emulating select-candidate-per-character mode" = "模擬 90 年代前期注音逐字選字輸入風格";
"Enable CNS11643 Support (2022-01-27)" = "啟用 CNS11643 全字庫支援 (2022-01-07)";
"Enable Space key for calling candidate window" = "敲空格鍵以呼出候選字窗";
"Enable symbol input support (incl. certain emoji symbols)" = "啟用包括少許繪文字在內的符號輸入支援";
"English" = "英語";
"Eten 26" = "倚天忘形配列 (26 鍵)";
"Eten Traditional" = "倚天傳統配列";
"Experience" = "體驗";
"Fake Seigyou" = "偽精業配列";
"Follow OS settings" = "依系統設定";
"for cycling candidates" = "輪替候選字";
"for cycling pages" = "輪替頁面";
"General" = "通用";
"Hanyu Pinyin with Numeral Intonation" = "漢語拼音+數字標調";
"Horizontal" = "橫向佈局";
"Hsu" = "許氏國音自然配列";
"IBM" = "IBM 配列";
"Japanese" = "和語";
"Keyboard" = "鍵盤";
"Misc Settings:" = "雜項:";
"MiTAC" = "神通配列";
"Non-QWERTY alphanumeral keyboard layouts are for Hanyu Pinyin parser only." = "QWERTY 以外的英數佈局是為了漢語拼音配列使用者而準備的。";
"Output Settings:" = "輸出設定:";
"Phonetic Parser:" = "注音配列:";
"Push the cursor to the front of the phrase after selection" = "在選字後將游標置於該字詞的前方";
"Selection Keys:" = "選字鍵:";
"Show page buttons in candidate window" = "在選字窗內顯示翻頁按鈕";
"Simplified Chinese" = "簡體中文";
"Space & ESC Key:" = "ESC 與空格鍵:";
"Space to +cycle candidates, Shift+Space to +cycle pages" = "Shift+空格鍵 換下一頁,空格鍵 換選下一個後選字";
"Space to +cycle pages, Shift+Space to +cycle candidates" = "空格鍵 換下一頁Shift+空格鍵 換選下一個後選字";
"Stop farting (when typed phonetic combination is invalid, etc.)" = "不要放屁 // 例:當輸入的音韻有誤時,等";
"to the front of the phrase (like Matsushita Hanin IME)" = "將游標置於詞語前方 // 松下漢音風格";
"to the rear of the phrase (like MS New-Phonetic IME)" = "將游標置於詞語後方 // 微軟新注音風格";
"Traditional Chinese" = "繁體中文";
"Typing Style:" = "輸入風格:";
"UI Language:" = "介面語言:";
"Use ESC key to clear the entire input buffer" = "敲 ESC 鍵以清空整個組字緩衝區";
"Vertical" = "縱向佈局";

View File

@ -435,10 +435,9 @@ extension ctlCandidateHorizontal {
frameRect = window?.frame ?? NSRect.zero
let topLeftPoint = NSMakePoint(
frameRect.origin.x, frameRect.origin.y + frameRect.size.height)
let topLeftPoint = NSPoint(x: frameRect.origin.x, y: frameRect.origin.y + frameRect.size.height)
frameRect.size = newSize
frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height)
frameRect.origin = NSPoint(x: topLeftPoint.x, y: topLeftPoint.y - frameRect.size.height)
self.window?.setFrame(frameRect, display: false)
candidateView.setNeedsDisplay(candidateView.bounds)
}

View File

@ -440,10 +440,9 @@ extension ctlCandidateVertical {
frameRect = window?.frame ?? NSRect.zero
let topLeftPoint = NSMakePoint(
frameRect.origin.x, frameRect.origin.y + frameRect.size.height)
let topLeftPoint = NSPoint(x: frameRect.origin.x, y: frameRect.origin.y + frameRect.size.height)
frameRect.size = newSize
frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height)
frameRect.origin = NSPoint(x: topLeftPoint.x, y: topLeftPoint.y - frameRect.size.height)
self.window?.setFrame(frameRect, display: false)
candidateView.setNeedsDisplay(candidateView.bounds)
}

View File

@ -1,5 +1,6 @@
// Copyright (c) 2021 and onwards Weizhong Yang (MIT License).
// All possible vChewing-specific modifications are (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
@ -123,7 +124,7 @@ public class NotifierController: NSWindowController, NotifierWindowDelegate {
panel.titlebarAppearsTransparent = true
panel.titleVisibility = .hidden
panel.showsToolbarButton = false
panel.standardWindowButton(NSWindow.ButtonType.fullScreenButton)?.isHidden = true
panel.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true
panel.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isHidden = true
panel.standardWindowButton(NSWindow.ButtonType.closeButton)?.isHidden = true
panel.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true

View File

@ -0,0 +1,71 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. 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 above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
@available(macOS 11.0, *)
class ctlPrefUI {
lazy var controller = PreferencesWindowController(
panes: [
Preferences.Pane(
identifier: Preferences.PaneIdentifier(rawValue: "General"),
title: NSLocalizedString("General", comment: ""),
toolbarIcon: NSImage(
systemSymbolName: "wrench.and.screwdriver.fill", accessibilityDescription: "General Preferences")
?? NSImage(named: NSImage.homeTemplateName)!
) {
suiPrefPaneGeneral()
},
Preferences.Pane(
identifier: Preferences.PaneIdentifier(rawValue: "Experiences"),
title: NSLocalizedString("Experience", comment: ""),
toolbarIcon: NSImage(
systemSymbolName: "person.fill.questionmark", accessibilityDescription: "Experiences Preferences")
?? NSImage(named: NSImage.listViewTemplateName)!
) {
suiPrefPaneExperience()
},
Preferences.Pane(
identifier: Preferences.PaneIdentifier(rawValue: "Dictionary"),
title: NSLocalizedString("Dictionary", comment: ""),
toolbarIcon: NSImage(
systemSymbolName: "character.book.closed.fill", accessibilityDescription: "Dictionary Preferences")
?? NSImage(named: NSImage.bookmarksTemplateName)!
) {
suiPrefPaneDictionary()
},
Preferences.Pane(
identifier: Preferences.PaneIdentifier(rawValue: "Keyboard"),
title: NSLocalizedString("Keyboard", comment: ""),
toolbarIcon: NSImage(
systemSymbolName: "keyboard.macwindow", accessibilityDescription: "Keyboard Preferences")
?? NSImage(named: NSImage.actionTemplateName)!
) {
suiPrefPaneKeyboard()
},
],
style: .toolbarItems
)
static let shared = ctlPrefUI()
}

View File

@ -0,0 +1,137 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. 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 above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import SwiftUI
@available(macOS 11.0, *)
struct suiPrefPaneDictionary: View {
private var fdrDefault = mgrLangModel.dataFolderPath(isDefaultFolder: true)
@State private var tbxUserDataPathSpecified: String =
UserDefaults.standard.string(forKey: UserDef.kUserDataFolderSpecified)
?? mgrLangModel.dataFolderPath(isDefaultFolder: true)
@State private var selAutoReloadUserData: Bool = UserDefaults.standard.bool(
forKey: UserDef.kShouldAutoReloadUserDataFiles)
@State private var selEnableCNS11643: Bool = UserDefaults.standard.bool(forKey: UserDef.kCNS11643Enabled)
@State private var selEnableSymbolInputSupport: Bool = UserDefaults.standard.bool(
forKey: UserDef.kSymbolInputEnabled)
private let contentWidth: Double = {
switch mgrPrefs.appleLanguages[0] {
case "ja":
return 520
default:
if mgrPrefs.appleLanguages[0].contains("zh-Han") {
return 480
} else {
return 550
}
}
}()
var body: some View {
Preferences.Container(contentWidth: contentWidth) {
Preferences.Section(title: "", bottomDivider: true) {
Text(LocalizedStringKey("Choose your desired user data folder path. Will be omitted if invalid."))
HStack {
TextField(fdrDefault, text: $tbxUserDataPathSpecified).disabled(true)
.help(tbxUserDataPathSpecified)
Button {
IME.dlgOpenPath.title = NSLocalizedString(
"Choose your desired user data folder.", comment: "")
IME.dlgOpenPath.showsResizeIndicator = true
IME.dlgOpenPath.showsHiddenFiles = true
IME.dlgOpenPath.canChooseFiles = false
IME.dlgOpenPath.canChooseDirectories = true
let bolPreviousFolderValidity = mgrLangModel.checkIfSpecifiedUserDataFolderValid(
NSString(string: mgrPrefs.userDataFolderSpecified).expandingTildeInPath)
if let window = ctlPrefUI.shared.controller.window {
IME.dlgOpenPath.beginSheetModal(for: window) { result in
if result == NSApplication.ModalResponse.OK {
if IME.dlgOpenPath.url != nil {
// CommonDialog
//
var newPath = IME.dlgOpenPath.url!.path
newPath.ensureTrailingSlash()
if mgrLangModel.checkIfSpecifiedUserDataFolderValid(newPath) {
mgrPrefs.userDataFolderSpecified = newPath
tbxUserDataPathSpecified = mgrPrefs.userDataFolderSpecified
IME.initLangModels(userOnly: true)
} else {
clsSFX.beep()
if !bolPreviousFolderValidity {
mgrPrefs.resetSpecifiedUserDataFolder()
}
return
}
}
} else {
if !bolPreviousFolderValidity {
mgrPrefs.resetSpecifiedUserDataFolder()
}
return
}
}
} // End If self.window != nil
} label: {
Text("...")
}
Button {
mgrPrefs.resetSpecifiedUserDataFolder()
tbxUserDataPathSpecified = ""
} label: {
Text("")
}
}
Toggle(
LocalizedStringKey("Automatically reload user data files if changes detected"),
isOn: $selAutoReloadUserData
).controlSize(.small).onChange(of: selAutoReloadUserData) { (value) in
mgrPrefs.shouldAutoReloadUserDataFiles = value
}
Divider()
Toggle(LocalizedStringKey("Enable CNS11643 Support (2022-01-27)"), isOn: $selEnableCNS11643)
.onChange(of: selEnableCNS11643) { (value) in
mgrPrefs.cns11643Enabled = value
}
Toggle(
LocalizedStringKey("Enable symbol input support (incl. certain emoji symbols)"),
isOn: $selEnableSymbolInputSupport
)
.onChange(of: selEnableSymbolInputSupport) { (value) in
mgrPrefs.symbolInputEnabled = value
}
}
}
}
}
@available(macOS 11.0, *)
struct suiPrefPaneDictionary_Previews: PreviewProvider {
static var previews: some View {
suiPrefPaneDictionary()
}
}

View File

@ -0,0 +1,166 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. 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 above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
import SwiftUI
@available(macOS 11.0, *)
struct suiPrefPaneExperience: View {
@State private var selSelectionKeysList = mgrPrefs.suggestedCandidateKeys
@State private var selSelectionKeys =
(UserDefaults.standard.string(forKey: UserDef.kCandidateKeys) ?? mgrPrefs.defaultCandidateKeys) as String
@State private var selCursorPosition =
UserDefaults.standard.bool(
forKey: UserDef.kSelectPhraseAfterCursorAsCandidate) ? 1 : 0
@State private var selPushCursorAfterSelection = UserDefaults.standard.bool(
forKey: UserDef.kMoveCursorAfterSelectingCandidate)
@State private var selKeyBehaviorShiftTab =
UserDefaults.standard.bool(forKey: UserDef.kSpecifyShiftTabKeyBehavior) ? 1 : 0
@State private var selKeyBehaviorShiftSpace =
UserDefaults.standard.bool(
forKey: UserDef.kSpecifyShiftSpaceKeyBehavior) ? 1 : 0
@State private var selKeyBehaviorSpaceForCallingCandidate = UserDefaults.standard.bool(
forKey: UserDef.kChooseCandidateUsingSpace)
@State private var selKeyBehaviorESCForClearingTheBuffer = UserDefaults.standard.bool(
forKey: UserDef.kEscToCleanInputBuffer)
@State private var selEnableSCPCTypingMode = UserDefaults.standard.bool(forKey: UserDef.kUseSCPCTypingMode)
private let contentWidth: Double = {
switch mgrPrefs.appleLanguages[0] {
case "ja":
return 520
default:
if mgrPrefs.appleLanguages[0].contains("zh-Han") {
return 480
} else {
return 550
}
}
}()
var body: some View {
Preferences.Container(contentWidth: contentWidth) {
Preferences.Section(bottomDivider: true, label: { Text(LocalizedStringKey("Selection Keys:")) }) {
ComboBox(items: mgrPrefs.suggestedCandidateKeys, text: $selSelectionKeys).frame(width: 180).onChange(
of: selSelectionKeys
) { (value) in
let keys: String = (value.trimmingCharacters(in: .whitespacesAndNewlines) as String).charDeDuplicate
do {
try mgrPrefs.validate(candidateKeys: keys)
mgrPrefs.candidateKeys = keys
selSelectionKeys = mgrPrefs.candidateKeys
} catch mgrPrefs.CandidateKeyError.empty {
selSelectionKeys = mgrPrefs.candidateKeys
} catch {
if let window = ctlPrefUI.shared.controller.window {
let alert = NSAlert(error: error)
alert.beginSheetModal(for: window) { _ in
selSelectionKeys = mgrPrefs.candidateKeys
}
clsSFX.beep()
}
}
}
Text(
LocalizedStringKey(
"Choose or hit Enter to confim your prefered keys for selecting candidates."
)
)
.preferenceDescription()
}
Preferences.Section(bottomDivider: true, label: { Text(LocalizedStringKey("Cursor Selection:")) }) {
Picker("", selection: $selCursorPosition) {
Text(LocalizedStringKey("to the front of the phrase (like Matsushita Hanin IME)")).tag(0)
Text(LocalizedStringKey("to the rear of the phrase (like MS New-Phonetic IME)")).tag(1)
}.onChange(of: selCursorPosition) { (value) in
mgrPrefs.selectPhraseAfterCursorAsCandidate = (value == 1) ? true : false
}
.labelsHidden()
.pickerStyle(RadioGroupPickerStyle())
Text(LocalizedStringKey("Choose the cursor position where you want to list possible candidates."))
.preferenceDescription()
Toggle(
LocalizedStringKey("Push the cursor to the front of the phrase after selection"),
isOn: $selPushCursorAfterSelection
).onChange(of: selPushCursorAfterSelection) { (value) in
mgrPrefs.moveCursorAfterSelectingCandidate = value
}.controlSize(.small)
}
Preferences.Section(title: "(Shift+)Tab:", bottomDivider: true) {
Picker("", selection: $selKeyBehaviorShiftTab) {
Text(LocalizedStringKey("for cycling candidates")).tag(0)
Text(LocalizedStringKey("for cycling pages")).tag(1)
}.onChange(of: selKeyBehaviorShiftTab) { (value) in
mgrPrefs.specifyShiftTabKeyBehavior = (value == 1) ? true : false
}
.labelsHidden()
.horizontalRadioGroupLayout()
.pickerStyle(RadioGroupPickerStyle())
Text(LocalizedStringKey("Choose the behavior of (Shift+)Tab key in the candidate window."))
.preferenceDescription()
}
Preferences.Section(bottomDivider: true, label: { Text(LocalizedStringKey("(Shift+)Space:")) }) {
Picker("", selection: $selKeyBehaviorShiftSpace) {
Text(LocalizedStringKey("Space to +cycle candidates, Shift+Space to +cycle pages")).tag(0)
Text(LocalizedStringKey("Space to +cycle pages, Shift+Space to +cycle candidates")).tag(1)
}.onChange(of: selKeyBehaviorShiftSpace) { (value) in
mgrPrefs.specifyShiftSpaceKeyBehavior = (value == 1) ? true : false
}
.labelsHidden()
.pickerStyle(RadioGroupPickerStyle())
Text(LocalizedStringKey("Choose the behavior of (Shift+)Space key in the candidate window."))
.preferenceDescription()
}
Preferences.Section(bottomDivider: true, label: { Text(LocalizedStringKey("Space & ESC Key:")) }) {
Toggle(
LocalizedStringKey("Enable Space key for calling candidate window"),
isOn: $selKeyBehaviorSpaceForCallingCandidate
).onChange(of: selKeyBehaviorSpaceForCallingCandidate) { (value) in
mgrPrefs.chooseCandidateUsingSpace = value
}
Toggle(
LocalizedStringKey("Use ESC key to clear the entire input buffer"),
isOn: $selKeyBehaviorESCForClearingTheBuffer
).onChange(of: selKeyBehaviorESCForClearingTheBuffer) { (value) in
mgrPrefs.escToCleanInputBuffer = value
}
}
Preferences.Section(label: { Text(LocalizedStringKey("Typing Style:")) }) {
Toggle(
LocalizedStringKey("Emulating select-candidate-per-character mode"), isOn: $selEnableSCPCTypingMode
).onChange(of: selEnableSCPCTypingMode) { (value) in
mgrPrefs.useSCPCTypingMode = value
}
Text(LocalizedStringKey("An accomodation for elder computer users."))
.preferenceDescription()
}
}
}
}
@available(macOS 11.0, *)
struct suiPrefPaneExperience_Previews: PreviewProvider {
static var previews: some View {
suiPrefPaneExperience()
}
}

View File

@ -0,0 +1,168 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. 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 above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import Cocoa
import SwiftUI
@available(macOS 11.0, *)
struct suiPrefPaneGeneral: View {
@State private var selCandidateUIFontSize = UserDefaults.standard.integer(forKey: UserDef.kCandidateListTextSize)
@State private var selUILanguage: [String] =
IME.arrSupportedLocales.contains(
((UserDefaults.standard.object(forKey: UserDef.kAppleLanguages) == nil)
? ["auto"] : UserDefaults.standard.array(forKey: UserDef.kAppleLanguages) as? [String] ?? ["auto"])[0])
? ((UserDefaults.standard.object(forKey: UserDef.kAppleLanguages) == nil)
? ["auto"] : UserDefaults.standard.array(forKey: UserDef.kAppleLanguages) as? [String] ?? ["auto"])
: ["auto"]
@State private var selEnableHorizontalCandidateLayout = UserDefaults.standard.bool(
forKey: UserDef.kUseHorizontalCandidateList)
@State private var selShowPageButtonsInCandidateUI = UserDefaults.standard.bool(
forKey: UserDef.kShowPageButtonsInCandidateWindow)
@State private var selEnableKanjiConvToKangXi = UserDefaults.standard.bool(
forKey: UserDef.kChineseConversionEnabled)
@State private var selEnableKanjiConvToJIS = UserDefaults.standard.bool(
forKey: UserDef.kShiftJISShinjitaiOutputEnabled)
@State private var selEnableFartSuppressor = UserDefaults.standard.bool(forKey: UserDef.kShouldNotFartInLieuOfBeep)
@State private var selEnableAutoUpdateCheck = UserDefaults.standard.bool(forKey: UserDef.kCheckUpdateAutomatically)
@State private var selEnableDebugMode = UserDefaults.standard.bool(forKey: UserDef.kIsDebugModeEnabled)
private let contentWidth: Double = {
switch mgrPrefs.appleLanguages[0] {
case "ja":
return 465
default:
if mgrPrefs.appleLanguages[0].contains("zh-Han") {
return 450
} else {
return 550
}
}
}()
var body: some View {
Preferences.Container(contentWidth: contentWidth) {
Preferences.Section(bottomDivider: false, label: { Text(LocalizedStringKey("Candidate Size:")) }) {
Picker("", selection: $selCandidateUIFontSize) {
Text("12").tag(12)
Text("14").tag(14)
Text("16").tag(16)
Text("18").tag(18)
Text("24").tag(24)
Text("32").tag(32)
Text("64").tag(64)
Text("96").tag(96)
}.onChange(of: selCandidateUIFontSize) { (value) in
mgrPrefs.candidateListTextSize = CGFloat(value)
}
.labelsHidden()
.frame(width: 120.0)
Text(LocalizedStringKey("Choose candidate font size for better visual clarity."))
.preferenceDescription()
}
Preferences.Section(bottomDivider: false, label: { Text(LocalizedStringKey("UI Language:")) }) {
Picker(LocalizedStringKey("Follow OS settings"), selection: $selUILanguage) {
Text(LocalizedStringKey("Follow OS settings")).tag(["auto"])
Text(LocalizedStringKey("Simplified Chinese")).tag(["zh-Hans"])
Text(LocalizedStringKey("Traditional Chinese")).tag(["zh-Hant"])
Text(LocalizedStringKey("Japanese")).tag(["ja"])
Text(LocalizedStringKey("English")).tag(["en"])
}.onChange(of: selUILanguage) { (value) in
IME.prtDebugIntel(value[0])
if selUILanguage == mgrPrefs.appleLanguages
|| (selUILanguage[0] == "auto"
&& UserDefaults.standard.object(forKey: UserDef.kAppleLanguages) == nil)
{
return
}
if selUILanguage[0] != "auto" {
mgrPrefs.appleLanguages = value
} else {
UserDefaults.standard.removeObject(forKey: UserDef.kAppleLanguages)
}
NSLog("vChewing App self-terminated due to UI language change.")
NSApplication.shared.terminate(nil)
}
.labelsHidden()
.frame(width: 180.0)
Text(LocalizedStringKey("Change user interface language (will reboot the IME)."))
.preferenceDescription()
}
Preferences.Section(bottomDivider: true, label: { Text(LocalizedStringKey("Candidate Layout:")) }) {
Picker("", selection: $selEnableHorizontalCandidateLayout) {
Text(LocalizedStringKey("Vertical")).tag(false)
Text(LocalizedStringKey("Horizontal")).tag(true)
}.onChange(of: selEnableHorizontalCandidateLayout) { (value) in
mgrPrefs.useHorizontalCandidateList = value
}
.labelsHidden()
.horizontalRadioGroupLayout()
.pickerStyle(RadioGroupPickerStyle())
Text(LocalizedStringKey("Choose your preferred layout of the candidate window."))
.preferenceDescription()
Toggle(
LocalizedStringKey("Show page buttons in candidate window"), isOn: $selShowPageButtonsInCandidateUI
).controlSize(.small)
}
Preferences.Section(bottomDivider: true, label: { Text(LocalizedStringKey("Output Settings:")) }) {
Toggle(
LocalizedStringKey("Auto-convert traditional Chinese glyphs to KangXi characters"),
isOn: $selEnableKanjiConvToKangXi
).onChange(of: selEnableKanjiConvToKangXi) { (value) in
mgrPrefs.chineseConversionEnabled = value
}
Toggle(
LocalizedStringKey("Auto-convert traditional Chinese glyphs to JIS Shinjitai characters"),
isOn: $selEnableKanjiConvToJIS
).onChange(of: selEnableKanjiConvToJIS) { (value) in
mgrPrefs.shiftJISShinjitaiOutputEnabled = value
}
Toggle(
LocalizedStringKey("Stop farting (when typed phonetic combination is invalid, etc.)"),
isOn: $selEnableFartSuppressor
).onChange(of: selEnableFartSuppressor) { (value) in
mgrPrefs.shouldNotFartInLieuOfBeep = value
clsSFX.beep()
}
}
Preferences.Section(label: { Text(LocalizedStringKey("Misc Settings:")).controlSize(.small) }) {
Toggle(LocalizedStringKey("Check for updates automatically"), isOn: $selEnableAutoUpdateCheck)
.onChange(of: selEnableAutoUpdateCheck) { (value) in
mgrPrefs.checkUpdateAutomatically = value
}
.controlSize(.small)
Toggle(LocalizedStringKey("Debug Mode"), isOn: $selEnableDebugMode).controlSize(.small)
.onChange(of: selEnableDebugMode) { (value) in
mgrPrefs.isDebugModeEnabled = value
}
}
}
}
}
@available(macOS 11.0, *)
struct suiPrefPaneGeneral_Previews: PreviewProvider {
static var previews: some View {
suiPrefPaneGeneral()
}
}

View File

@ -0,0 +1,109 @@
// Copyright (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
1. The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
2. 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 above.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import SwiftUI
@available(macOS 11.0, *)
struct suiPrefPaneKeyboard: View {
@State private var selMandarinParser = UserDefaults.standard.integer(forKey: UserDef.kMandarinParser)
@State private var selBasicKeyboardLayout: String =
UserDefaults.standard.string(forKey: UserDef.kBasicKeyboardLayout) ?? mgrPrefs.basicKeyboardLayout
private let contentWidth: Double = {
switch mgrPrefs.appleLanguages[0] {
case "ja":
return 520
default:
if mgrPrefs.appleLanguages[0].contains("zh-Han") {
return 480
} else {
return 550
}
}
}()
var body: some View {
Preferences.Container(contentWidth: contentWidth) {
Preferences.Section(label: { Text(LocalizedStringKey("Phonetic Parser:")) }) {
Picker("", selection: $selMandarinParser) {
Text(LocalizedStringKey("Dachen (Microsoft Standard / Wang / 01, etc.)")).tag(0)
Text(LocalizedStringKey("Eten Traditional")).tag(1)
Text(LocalizedStringKey("Eten 26")).tag(3)
Text(LocalizedStringKey("IBM")).tag(4)
Text(LocalizedStringKey("Hsu")).tag(2)
Text(LocalizedStringKey("MiTAC")).tag(5)
Text(LocalizedStringKey("Fake Seigyou")).tag(6)
Text(LocalizedStringKey("Hanyu Pinyin with Numeral Intonation")).tag(10)
}.onChange(of: selMandarinParser) { (value) in
mgrPrefs.mandarinParser = value
}
.labelsHidden()
.frame(width: 320.0)
Text(LocalizedStringKey("Choose the phonetic layout for Mandarin parser."))
.preferenceDescription()
}
Preferences.Section(bottomDivider: true, label: { Text(LocalizedStringKey("Basic Keyboard Layout:")) }) {
HStack {
Picker("", selection: $selBasicKeyboardLayout) {
ForEach(0...(IME.arrEnumerateSystemKeyboardLayouts.count - 1), id: \.self) { id in
Text(IME.arrEnumerateSystemKeyboardLayouts[id].strName).tag(
IME.arrEnumerateSystemKeyboardLayouts[id].strValue)
}.id(UUID())
}.onChange(of: selBasicKeyboardLayout) { (value) in
mgrPrefs.basicKeyboardLayout = value
}
.labelsHidden()
.frame(width: 240.0)
}
Text(LocalizedStringKey("Choose the macOS-level basic keyboard layout."))
.preferenceDescription()
}
}
Divider()
Preferences.Container(contentWidth: contentWidth) {
Preferences.Section(title: "") {
VStack(alignment: .leading, spacing: 10) {
Text(
LocalizedStringKey(
"Non-QWERTY alphanumeral keyboard layouts are for Hanyu Pinyin parser only."
)
)
.preferenceDescription()
Text(
LocalizedStringKey(
"Apple Dynamic Bopomofo Basic Keyboard Layouts (Dachen & Eten Traditional) must match the Dachen parser in order to be functional."
)
)
.preferenceDescription()
}
}
}
}
}
@available(macOS 11.0, *)
struct suiPrefPaneKeyboard_Previews: PreviewProvider {
static var previews: some View {
suiPrefPaneKeyboard()
}
}

View File

@ -1,5 +1,6 @@
// Copyright (c) 2021 and onwards Weizhong Yang (MIT License).
// All possible vChewing-specific modifications are (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// All possible vChewing-specific modifications are of:
// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
/*
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

View File

@ -27,27 +27,19 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import Carbon
import Cocoa
// Extend the RangeReplaceableCollection to allow it clean duplicated characters.
extension RangeReplaceableCollection where Element: Hashable {
var charDeDuplicate: Self {
var set = Set<Element>()
return filter { set.insert($0).inserted }
}
}
// Please note that the class should be exposed using the same class name
// in Objective-C in order to let IMK to see the same class name as
// the "InputMethodServerPreferencesWindowControllerClass" in Info.plist.
@objc(ctlPrefWindow) class ctlPrefWindow: NSWindowController {
@IBOutlet weak var fontSizePopUpButton: NSPopUpButton!
@IBOutlet weak var uiLanguageButton: NSPopUpButton!
@IBOutlet weak var basisKeyboardLayoutButton: NSPopUpButton!
@IBOutlet weak var basicKeyboardLayoutButton: NSPopUpButton!
@IBOutlet weak var selectionKeyComboBox: NSComboBox!
@IBOutlet weak var chkTrad2KangXi: NSButton!
@IBOutlet weak var chkTrad2JISShinjitai: NSButton!
@IBOutlet weak var lblCurrentlySpecifiedUserDataFolder: NSTextFieldCell!
var currentLanguageSelectItem: NSMenuItem? = nil
var currentLanguageSelectItem: NSMenuItem?
override func windowDidLoad() {
super.windowDidLoad()
@ -56,8 +48,8 @@ extension RangeReplaceableCollection where Element: Hashable {
isDefaultFolder: true)
let languages = ["auto", "en", "zh-Hans", "zh-Hant", "ja"]
var autoMUISelectItem: NSMenuItem? = nil
var chosenLanguageItem: NSMenuItem? = nil
var autoMUISelectItem: NSMenuItem?
var chosenLanguageItem: NSMenuItem?
uiLanguageButton.menu?.removeAllItems()
let appleLanguages = mgrPrefs.appleLanguages
@ -82,25 +74,25 @@ extension RangeReplaceableCollection where Element: Hashable {
uiLanguageButton.select(currentLanguageSelectItem)
let list = TISCreateInputSourceList(nil, true).takeRetainedValue() as! [TISInputSource]
var usKeyboardLayoutItem: NSMenuItem? = nil
var chosenBaseKeyboardLayoutItem: NSMenuItem? = nil
var usKeyboardLayoutItem: NSMenuItem?
var chosenBaseKeyboardLayoutItem: NSMenuItem?
basisKeyboardLayoutButton.menu?.removeAllItems()
basicKeyboardLayoutButton.menu?.removeAllItems()
let itmAppleZhuyinBopomofo = NSMenuItem()
itmAppleZhuyinBopomofo.title = String(
format: NSLocalizedString("Apple Zhuyin Bopomofo (Dachen)", comment: ""))
itmAppleZhuyinBopomofo.representedObject = String(
"com.apple.keylayout.ZhuyinBopomofo")
basisKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinBopomofo)
basicKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinBopomofo)
let itmAppleZhuyinEten = NSMenuItem()
itmAppleZhuyinEten.title = String(
format: NSLocalizedString("Apple Zhuyin Eten (Traditional)", comment: ""))
itmAppleZhuyinEten.representedObject = String("com.apple.keylayout.ZhuyinEten")
basisKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinEten)
basicKeyboardLayoutButton.menu?.addItem(itmAppleZhuyinEten)
let basisKeyboardLayoutID = mgrPrefs.basisKeyboardLayout
let basicKeyboardLayoutID = mgrPrefs.basicKeyboardLayout
for source in list {
if let categoryPtr = TISGetInputSourceProperty(source, kTISPropertyInputSourceCategory) {
@ -150,13 +142,15 @@ extension RangeReplaceableCollection where Element: Hashable {
if sourceID == "com.apple.keylayout.US" {
usKeyboardLayoutItem = menuItem
}
if basisKeyboardLayoutID == sourceID {
if basicKeyboardLayoutID == sourceID {
chosenBaseKeyboardLayoutItem = menuItem
}
basisKeyboardLayoutButton.menu?.addItem(menuItem)
if IME.arrWhitelistedKeyLayoutsASCII.contains(sourceID) || sourceID.contains("vChewing") {
basicKeyboardLayoutButton.menu?.addItem(menuItem)
}
}
switch basisKeyboardLayoutID {
switch basicKeyboardLayoutID {
case "com.apple.keylayout.ZhuyinBopomofo":
chosenBaseKeyboardLayoutItem = itmAppleZhuyinBopomofo
case "com.apple.keylayout.ZhuyinEten":
@ -165,7 +159,7 @@ extension RangeReplaceableCollection where Element: Hashable {
break // nothing to do
}
basisKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem)
basicKeyboardLayoutButton.select(chosenBaseKeyboardLayoutItem ?? usKeyboardLayoutItem)
selectionKeyComboBox.usesDataSource = false
selectionKeyComboBox.removeAllItems()
@ -201,9 +195,9 @@ extension RangeReplaceableCollection where Element: Hashable {
}
}
@IBAction func updateBasisKeyboardLayoutAction(_ sender: Any) {
if let sourceID = basisKeyboardLayoutButton.selectedItem?.representedObject as? String {
mgrPrefs.basisKeyboardLayout = sourceID
@IBAction func updateBasicKeyboardLayoutAction(_ sender: Any) {
if let sourceID = basicKeyboardLayoutButton.selectedItem?.representedObject as? String {
mgrPrefs.basicKeyboardLayout = sourceID
}
}
@ -247,7 +241,7 @@ extension RangeReplaceableCollection where Element: Hashable {
} catch {
if let window = window {
let alert = NSAlert(error: error)
alert.beginSheetModal(for: window) { response in
alert.beginSheetModal(for: window) { _ in
self.selectionKeyComboBox.stringValue = mgrPrefs.candidateKeys
}
clsSFX.beep()
@ -256,8 +250,7 @@ extension RangeReplaceableCollection where Element: Hashable {
}
@IBAction func resetSpecifiedUserDataFolder(_ sender: Any) {
UserDefaults.standard.removeObject(forKey: "UserDataFolderSpecified")
IME.initLangModels(userOnly: true)
mgrPrefs.resetSpecifiedUserDataFolder()
}
@IBAction func chooseUserDataFolderToSpecify(_ sender: Any) {
@ -275,22 +268,24 @@ extension RangeReplaceableCollection where Element: Hashable {
IME.dlgOpenPath.beginSheetModal(for: self.window!) { result in
if result == NSApplication.ModalResponse.OK {
if IME.dlgOpenPath.url != nil {
if mgrLangModel.checkIfSpecifiedUserDataFolderValid(
IME.dlgOpenPath.url!.path)
{
mgrPrefs.userDataFolderSpecified = IME.dlgOpenPath.url!.path
// CommonDialog
//
var newPath = IME.dlgOpenPath.url!.path
newPath.ensureTrailingSlash()
if mgrLangModel.checkIfSpecifiedUserDataFolderValid(newPath) {
mgrPrefs.userDataFolderSpecified = newPath
IME.initLangModels(userOnly: true)
} else {
clsSFX.beep()
if !bolPreviousFolderValidity {
self.resetSpecifiedUserDataFolder(self)
mgrPrefs.resetSpecifiedUserDataFolder()
}
return
}
}
} else {
if !bolPreviousFolderValidity {
self.resetSpecifiedUserDataFolder(self)
mgrPrefs.resetSpecifiedUserDataFolder()
}
return
}

View File

@ -8,7 +8,7 @@
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="ctlPrefWindow">
<connections>
<outlet property="basisKeyboardLayoutButton" destination="124" id="135"/>
<outlet property="basicKeyboardLayoutButton" destination="124" id="135"/>
<outlet property="chkTrad2JISShinjitai" destination="h4r-Sp-LBI" id="pk8-xi-IJi"/>
<outlet property="chkTrad2KangXi" destination="5IL-zZ-CL9" id="PGS-Dy-BBY"/>
<outlet property="fontSizePopUpButton" destination="90" id="108"/>
@ -163,7 +163,7 @@
</connections>
</matrix>
<button verticalHuggingPriority="750" id="233">
<rect key="frame" x="169" y="25.5" width="245" height="17"/>
<rect key="frame" x="169" y="26.5" width="245" height="17"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="check" title="Show page buttons in candidate list" bezelStyle="regularSquare" imagePosition="left" controlSize="small" state="on" inset="2" id="shc-Nu-UsM">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
@ -234,22 +234,8 @@
<rect key="frame" x="3" y="3" width="445" height="134"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Yaj-QY-7xV" userLabel="chkCNSSupport">
<rect key="frame" x="19" y="104.5" width="406" height="17"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="KdE-Vd-Y50"/>
</constraints>
<buttonCell key="cell" type="check" title="Enable CNS11643 Support (2022-01-07)" bezelStyle="regularSquare" imagePosition="left" controlSize="small" inset="2" id="W24-T4-cg0">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="cellTitle"/>
</buttonCell>
<connections>
<action selector="toggleCNSSupport:" target="-2" id="mcY-ma-N0c"/>
<binding destination="32" name="value" keyPath="values.CNS11643Enabled" id="Pbx-Gt-upm"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5IL-zZ-CL9" userLabel="chkTrad2KangXi">
<rect key="frame" x="19" y="83.5" width="406" height="16"/>
<rect key="frame" x="19" y="99.5" width="406" height="16"/>
<buttonCell key="cell" type="check" title="Auto-convert traditional Chinese glyphs to KangXi characters" bezelStyle="regularSquare" imagePosition="left" controlSize="small" inset="2" id="BSK-bH-Gct">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="cellTitle"/>
@ -260,7 +246,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="h4r-Sp-LBI" userLabel="chkTrad2JISShinjitai">
<rect key="frame" x="19" y="62.5" width="406" height="16"/>
<rect key="frame" x="19" y="78.5" width="406" height="16"/>
<buttonCell key="cell" type="check" title="Auto-convert traditional Chinese glyphs to JIS Shinjitai characters" bezelStyle="regularSquare" imagePosition="left" controlSize="small" inset="2" id="eia-1F-Do0">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="cellTitle"/>
@ -271,7 +257,7 @@
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pYB-E5-4Nv">
<rect key="frame" x="19" y="41.5" width="406" height="16"/>
<rect key="frame" x="19" y="57.5" width="406" height="16"/>
<buttonCell key="cell" type="check" title="Stop farting (when typed phonetic combination is invalid, etc.)" bezelStyle="regularSquare" imagePosition="left" controlSize="small" inset="2" id="62u-jY-BRh">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="cellTitle"/>
@ -283,17 +269,14 @@
</button>
</subviews>
<constraints>
<constraint firstItem="Yaj-QY-7xV" firstAttribute="leading" secondItem="5IL-zZ-CL9" secondAttribute="leading" id="4Bc-Yl-MiB"/>
<constraint firstItem="5IL-zZ-CL9" firstAttribute="top" secondItem="Yaj-QY-7xV" secondAttribute="bottom" constant="6" symbolic="YES" id="7iI-Gm-cGt"/>
<constraint firstItem="h4r-Sp-LBI" firstAttribute="top" secondItem="5IL-zZ-CL9" secondAttribute="bottom" constant="6" symbolic="YES" id="8Pq-i7-vK8"/>
<constraint firstItem="Yaj-QY-7xV" firstAttribute="trailing" secondItem="5IL-zZ-CL9" secondAttribute="trailing" id="8QP-dq-cfD"/>
<constraint firstItem="pYB-E5-4Nv" firstAttribute="trailing" secondItem="h4r-Sp-LBI" secondAttribute="trailing" id="IWM-hB-AeP"/>
<constraint firstItem="Yaj-QY-7xV" firstAttribute="top" secondItem="brd-6J-saN" secondAttribute="top" constant="13.5" id="Qf7-x1-bcp"/>
<constraint firstAttribute="trailing" secondItem="Yaj-QY-7xV" secondAttribute="trailing" constant="20" symbolic="YES" id="jqe-y8-arB"/>
<constraint firstItem="5IL-zZ-CL9" firstAttribute="top" secondItem="brd-6J-saN" secondAttribute="top" constant="19.5" id="i7s-ez-iKF"/>
<constraint firstItem="pYB-E5-4Nv" firstAttribute="leading" secondItem="h4r-Sp-LBI" secondAttribute="leading" id="jsE-BB-mo7"/>
<constraint firstItem="Yaj-QY-7xV" firstAttribute="leading" secondItem="brd-6J-saN" secondAttribute="leading" constant="20" symbolic="YES" id="mjS-PE-C1d"/>
<constraint firstAttribute="trailing" secondItem="5IL-zZ-CL9" secondAttribute="trailing" constant="20" symbolic="YES" id="kZf-wi-ijB"/>
<constraint firstItem="pYB-E5-4Nv" firstAttribute="top" secondItem="h4r-Sp-LBI" secondAttribute="bottom" constant="6" symbolic="YES" id="unl-l5-LUF"/>
<constraint firstItem="5IL-zZ-CL9" firstAttribute="leading" secondItem="h4r-Sp-LBI" secondAttribute="leading" id="v0c-Cb-FH7"/>
<constraint firstItem="5IL-zZ-CL9" firstAttribute="leading" secondItem="brd-6J-saN" secondAttribute="leading" constant="20" symbolic="YES" id="xAB-f3-f0e"/>
<constraint firstItem="5IL-zZ-CL9" firstAttribute="trailing" secondItem="h4r-Sp-LBI" secondAttribute="trailing" id="z2C-jk-sNa"/>
</constraints>
</view>
@ -445,7 +428,7 @@
</column>
</cells>
<connections>
<binding destination="32" name="selectedTag" keyPath="values.SpecifyTabKeyBehavior" id="ZDp-6s-2lR"/>
<binding destination="32" name="selectedTag" keyPath="values.SpecifyShiftTabKeyBehavior" id="R1A-Ji-8i2"/>
</connections>
</matrix>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TMn-LX-3Ub">
@ -478,7 +461,7 @@
</column>
</cells>
<connections>
<binding destination="32" name="selectedTag" keyPath="values.SpecifySpaceKeyBehavior" id="yBI-fc-n36"/>
<binding destination="32" name="selectedTag" keyPath="values.SpecifyShiftSpaceKeyBehavior" id="Ajq-3d-Lh1"/>
</connections>
</matrix>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="J0f-Aw-dxC">
@ -666,9 +649,24 @@
<action selector="resetSpecifiedUserDataFolder:" target="-2" id="pqR-AG-4cm"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Yaj-QY-7xV" userLabel="chkCNSSupport">
<rect key="frame" x="19" y="249.5" width="406" height="17"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="KdE-Vd-Y50"/>
</constraints>
<buttonCell key="cell" type="check" title="Enable CNS11643 Support (2022-01-07)" bezelStyle="regularSquare" imagePosition="left" controlSize="small" inset="2" id="W24-T4-cg0">
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
<font key="font" metaFont="cellTitle"/>
</buttonCell>
<connections>
<action selector="toggleCNSSupport:" target="-2" id="mcY-ma-N0c"/>
<binding destination="32" name="value" keyPath="values.CNS11643Enabled" id="Pbx-Gt-upm"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="s7t-Kk-EPu" firstAttribute="bottom" secondItem="MPN-np-SbT" secondAttribute="bottom" id="0Fo-ya-hQ9"/>
<constraint firstItem="Yaj-QY-7xV" firstAttribute="top" secondItem="O4B-Z2-XYi" secondAttribute="bottom" constant="6" symbolic="YES" id="2RJ-v9-tO3"/>
<constraint firstAttribute="trailing" secondItem="FUV-qx-xkC" secondAttribute="trailing" constant="20" symbolic="YES" id="6QR-tj-5cH"/>
<constraint firstItem="MPN-np-SbT" firstAttribute="leading" secondItem="p7V-IN-OTr" secondAttribute="leading" constant="346" id="7xl-xX-CEQ"/>
<constraint firstItem="s7t-Kk-EPu" firstAttribute="leading" secondItem="MPN-np-SbT" secondAttribute="trailing" constant="-376" id="9at-E8-Bt1"/>
@ -682,7 +680,9 @@
<constraint firstItem="p7V-IN-OTr" firstAttribute="trailing" secondItem="O4B-Z2-XYi" secondAttribute="trailing" id="auY-l9-U7h"/>
<constraint firstItem="jXe-xz-9Sd" firstAttribute="leading" secondItem="MPN-np-SbT" secondAttribute="trailing" constant="-1" id="cYQ-Rx-tuG"/>
<constraint firstItem="s7t-Kk-EPu" firstAttribute="trailing" secondItem="p7V-IN-OTr" secondAttribute="trailing" constant="-60" id="eIn-kD-FGz"/>
<constraint firstItem="Yaj-QY-7xV" firstAttribute="leading" secondItem="O4B-Z2-XYi" secondAttribute="leading" id="kPh-vq-asr"/>
<constraint firstItem="p7V-IN-OTr" firstAttribute="leading" secondItem="O4B-Z2-XYi" secondAttribute="leading" id="mmu-SS-jac"/>
<constraint firstItem="Yaj-QY-7xV" firstAttribute="trailing" secondItem="O4B-Z2-XYi" secondAttribute="trailing" id="r6q-3h-41a"/>
<constraint firstItem="s7t-Kk-EPu" firstAttribute="trailing" secondItem="FUV-qx-xkC" secondAttribute="trailing" constant="-60" id="vIO-x1-7Q2"/>
<constraint firstItem="jXe-xz-9Sd" firstAttribute="baseline" secondItem="MPN-np-SbT" secondAttribute="baseline" id="wys-ML-2Q2"/>
</constraints>
@ -755,16 +755,16 @@
<items>
<menuItem title="Standard" state="on" id="6"/>
<menuItem title="ETen" tag="1" id="7"/>
<menuItem title="Hsu" tag="2" id="8"/>
<menuItem title="ETen26" tag="3" id="9"/>
<menuItem title="IBM" tag="4" id="137"/>
<menuItem title="Hsu" tag="2" id="8"/>
<menuItem title="MiTAC" tag="5" id="7fV-x8-WHQ"/>
<menuItem title="Fake Seigyou" tag="6" id="27F-8T-FkQ"/>
<menuItem title="Hanyu Pinyin" tag="10" id="10"/>
</items>
</menu>
<connections>
<binding destination="32" name="selectedTag" keyPath="values.KeyboardLayout" id="103"/>
<binding destination="32" name="selectedTag" keyPath="values.MandarinParser" id="n3r-9x-E3p"/>
</connections>
</popUpButtonCell>
</popUpButton>
@ -779,7 +779,7 @@
<menu key="menu" title="OtherViews" id="128"/>
</popUpButtonCell>
<connections>
<action selector="updateBasisKeyboardLayoutAction:" target="-2" id="136"/>
<action selector="updateBasicKeyboardLayoutAction:" target="-2" id="136"/>
</connections>
</popUpButton>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="125">

View File

@ -30,7 +30,27 @@
5B707CEC27D9F4870099EF99 /* OpenCC in Frameworks */ = {isa = PBXBuildFile; productRef = 5B707CEB27D9F4870099EF99 /* OpenCC */; };
5B73FB5E27B2BE1300E9BF49 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5B73FB6027B2BE1300E9BF49 /* InfoPlist.strings */; };
5B7BC4B027AFFBE800F66C24 /* frmPrefWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B7BC4AE27AFFBE800F66C24 /* frmPrefWindow.xib */; };
5BA9FD0F27FEDB6B002DE248 /* suiPrefPaneGeneral.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD0A27FEDB6B002DE248 /* suiPrefPaneGeneral.swift */; };
5BA9FD1027FEDB6B002DE248 /* suiPrefPaneKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD0B27FEDB6B002DE248 /* suiPrefPaneKeyboard.swift */; };
5BA9FD1127FEDB6B002DE248 /* ctlPrefUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD0C27FEDB6B002DE248 /* ctlPrefUI.swift */; };
5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD0D27FEDB6B002DE248 /* suiPrefPaneExperience.swift */; };
5BA9FD1327FEDB6B002DE248 /* suiPrefPaneDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD0E27FEDB6B002DE248 /* suiPrefPaneDictionary.swift */; };
5BA9FD3E27FEF3C8002DE248 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3127FEF3C8002DE248 /* Utilities.swift */; };
5BA9FD3F27FEF3C8002DE248 /* Pane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3227FEF3C8002DE248 /* Pane.swift */; };
5BA9FD4027FEF3C8002DE248 /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3327FEF3C8002DE248 /* Localization.swift */; };
5BA9FD4127FEF3C8002DE248 /* PreferencesStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3427FEF3C8002DE248 /* PreferencesStyle.swift */; };
5BA9FD4227FEF3C8002DE248 /* PreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3527FEF3C8002DE248 /* PreferencePane.swift */; };
5BA9FD4327FEF3C8002DE248 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3627FEF3C8002DE248 /* Preferences.swift */; };
5BA9FD4427FEF3C8002DE248 /* SegmentedControlStyleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3727FEF3C8002DE248 /* SegmentedControlStyleViewController.swift */; };
5BA9FD4527FEF3C9002DE248 /* ToolbarItemStyleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3827FEF3C8002DE248 /* ToolbarItemStyleViewController.swift */; };
5BA9FD4627FEF3C9002DE248 /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3927FEF3C8002DE248 /* Container.swift */; };
5BA9FD4727FEF3C9002DE248 /* PreferencesStyleController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3A27FEF3C8002DE248 /* PreferencesStyleController.swift */; };
5BA9FD4827FEF3C9002DE248 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3B27FEF3C8002DE248 /* PreferencesWindowController.swift */; };
5BA9FD4927FEF3C9002DE248 /* Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3C27FEF3C8002DE248 /* Section.swift */; };
5BA9FD4A27FEF3C9002DE248 /* PreferencesTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD3D27FEF3C8002DE248 /* PreferencesTabViewController.swift */; };
5BA9FD8B28006B41002DE248 /* VDKComboBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BA9FD8A28006B41002DE248 /* VDKComboBox.swift */; };
5BAD0CD527D701F6003D127F /* vChewingKeyLayout.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5B30F11227BA568800484E24 /* vChewingKeyLayout.bundle */; };
5BAEFAD028012565001F42C9 /* mgrLangModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BAEFACF28012565001F42C9 /* mgrLangModel.swift */; };
5BB802DA27FABA8300CF1C19 /* ctlInputMethod_Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */; };
5BBBB75F27AED54C0023B93A /* Beep.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75D27AED54C0023B93A /* Beep.m4a */; };
5BBBB76027AED54C0023B93A /* Fart.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 5BBBB75E27AED54C0023B93A /* Fart.m4a */; };
@ -50,6 +70,7 @@
5BD05C6827B2BBEF004C4F1D /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD05C6327B2BBEF004C4F1D /* Content.swift */; };
5BD05C6927B2BBEF004C4F1D /* WindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD05C6427B2BBEF004C4F1D /* WindowController.swift */; };
5BD05C6A27B2BBEF004C4F1D /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BD05C6527B2BBEF004C4F1D /* ViewController.swift */; };
5BDC1CFA27FDF1310052C2B9 /* apiUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */; };
5BDCBB2E27B4E67A00D0CC59 /* vChewingPhraseEditor.app in Resources */ = {isa = PBXBuildFile; fileRef = 5BD05BB827B2A429004C4F1D /* vChewingPhraseEditor.app */; };
5BE78BD927B3775B005EA1BE /* ctlAboutWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE78BD827B37750005EA1BE /* ctlAboutWindow.swift */; };
5BE78BDD27B3776D005EA1BE /* frmAboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5BE78BDA27B37764005EA1BE /* frmAboutWindow.xib */; };
@ -188,6 +209,26 @@
5B7BC4AF27AFFBE800F66C24 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/frmPrefWindow.xib; sourceTree = "<group>"; };
5B7BC4B227AFFC0B00F66C24 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/frmPrefWindow.strings; sourceTree = "<group>"; };
5B8F43ED27C9BC220069AC27 /* SymbolLM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SymbolLM.h; sourceTree = "<group>"; };
5BA9FD0A27FEDB6B002DE248 /* suiPrefPaneGeneral.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = suiPrefPaneGeneral.swift; sourceTree = "<group>"; };
5BA9FD0B27FEDB6B002DE248 /* suiPrefPaneKeyboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = suiPrefPaneKeyboard.swift; sourceTree = "<group>"; };
5BA9FD0C27FEDB6B002DE248 /* ctlPrefUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ctlPrefUI.swift; sourceTree = "<group>"; };
5BA9FD0D27FEDB6B002DE248 /* suiPrefPaneExperience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = suiPrefPaneExperience.swift; sourceTree = "<group>"; };
5BA9FD0E27FEDB6B002DE248 /* suiPrefPaneDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = suiPrefPaneDictionary.swift; sourceTree = "<group>"; };
5BA9FD3127FEF3C8002DE248 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
5BA9FD3227FEF3C8002DE248 /* Pane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pane.swift; sourceTree = "<group>"; };
5BA9FD3327FEF3C8002DE248 /* Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = "<group>"; };
5BA9FD3427FEF3C8002DE248 /* PreferencesStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesStyle.swift; sourceTree = "<group>"; };
5BA9FD3527FEF3C8002DE248 /* PreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencePane.swift; sourceTree = "<group>"; };
5BA9FD3627FEF3C8002DE248 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
5BA9FD3727FEF3C8002DE248 /* SegmentedControlStyleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentedControlStyleViewController.swift; sourceTree = "<group>"; };
5BA9FD3827FEF3C8002DE248 /* ToolbarItemStyleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolbarItemStyleViewController.swift; sourceTree = "<group>"; };
5BA9FD3927FEF3C8002DE248 /* Container.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = "<group>"; };
5BA9FD3A27FEF3C8002DE248 /* PreferencesStyleController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesStyleController.swift; sourceTree = "<group>"; };
5BA9FD3B27FEF3C8002DE248 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = "<group>"; };
5BA9FD3C27FEF3C8002DE248 /* Section.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Section.swift; sourceTree = "<group>"; };
5BA9FD3D27FEF3C8002DE248 /* PreferencesTabViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesTabViewController.swift; sourceTree = "<group>"; };
5BA9FD8A28006B41002DE248 /* VDKComboBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDKComboBox.swift; sourceTree = "<group>"; };
5BAEFACF28012565001F42C9 /* mgrLangModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mgrLangModel.swift; sourceTree = "<group>"; };
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlInputMethod_Menu.swift; sourceTree = "<group>"; };
5BBBB75D27AED54C0023B93A /* Beep.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Beep.m4a; sourceTree = "<group>"; };
5BBBB75E27AED54C0023B93A /* Fart.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Fart.m4a; sourceTree = "<group>"; };
@ -212,6 +253,7 @@
5BD05C6327B2BBEF004C4F1D /* Content.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = "<group>"; };
5BD05C6427B2BBEF004C4F1D /* WindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WindowController.swift; sourceTree = "<group>"; };
5BD05C6527B2BBEF004C4F1D /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = apiUpdate.swift; sourceTree = "<group>"; };
5BDCBB4227B4F6C600D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/MainMenu.strings"; sourceTree = "<group>"; };
5BDCBB4327B4F6C600D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/frmAboutWindow.strings"; sourceTree = "<group>"; };
5BDCBB4527B4F6C600D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/frmPrefWindow.strings"; sourceTree = "<group>"; };
@ -349,8 +391,10 @@
5B62A30127AE732800A19448 /* 3rdParty */ = {
isa = PBXGroup;
children = (
5BA9FD8C28006BA7002DE248 /* VDKComboBox */,
5B707CE627D9F43E0099EF99 /* OpenCCBridge */,
5B62A30227AE733500A19448 /* OVMandarin */,
5BA9FCEA27FED652002DE248 /* SindreSorhus */,
);
path = 3rdParty;
sourceTree = "<group>";
@ -410,6 +454,7 @@
5B62A32227AE756300A19448 /* IMEModules */ = {
isa = PBXGroup;
children = (
5BDC1CF927FDF1310052C2B9 /* apiUpdate.swift */,
D4A13D5927A59D5C003BE359 /* ctlInputMethod.swift */,
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */,
5B5E535127EF261400C6AA1E /* IME.swift */,
@ -435,6 +480,7 @@
D41355D6278D7409005E5CBD /* mgrLangModel.h */,
D495583A27A5C6C4006ADE1C /* mgrLangModel_Privates.h */,
D41355D7278D7409005E5CBD /* mgrLangModel.mm */,
5BAEFACF28012565001F42C9 /* mgrLangModel.swift */,
5B62A32527AE758000A19448 /* SubLanguageModels */,
);
path = LangModelRelated;
@ -479,6 +525,7 @@
isa = PBXGroup;
children = (
5B62A33E27AE7CD900A19448 /* CandidateUI */,
5BA9FD0927FED9F3002DE248 /* PrefUI */,
5B62A34227AE7CD900A19448 /* TooltipUI */,
5B62A34427AE7CD900A19448 /* NotifierUI */,
);
@ -561,6 +608,55 @@
path = OpenCCBridge;
sourceTree = "<group>";
};
5BA9FCEA27FED652002DE248 /* SindreSorhus */ = {
isa = PBXGroup;
children = (
5BA9FD3027FEF3C8002DE248 /* Preferences */,
);
name = SindreSorhus;
path = Source/3rdParty/SindreSorhus;
sourceTree = SOURCE_ROOT;
};
5BA9FD0927FED9F3002DE248 /* PrefUI */ = {
isa = PBXGroup;
children = (
5BA9FD0C27FEDB6B002DE248 /* ctlPrefUI.swift */,
5BA9FD0E27FEDB6B002DE248 /* suiPrefPaneDictionary.swift */,
5BA9FD0D27FEDB6B002DE248 /* suiPrefPaneExperience.swift */,
5BA9FD0A27FEDB6B002DE248 /* suiPrefPaneGeneral.swift */,
5BA9FD0B27FEDB6B002DE248 /* suiPrefPaneKeyboard.swift */,
);
path = PrefUI;
sourceTree = "<group>";
};
5BA9FD3027FEF3C8002DE248 /* Preferences */ = {
isa = PBXGroup;
children = (
5BA9FD3927FEF3C8002DE248 /* Container.swift */,
5BA9FD3327FEF3C8002DE248 /* Localization.swift */,
5BA9FD3227FEF3C8002DE248 /* Pane.swift */,
5BA9FD3527FEF3C8002DE248 /* PreferencePane.swift */,
5BA9FD3627FEF3C8002DE248 /* Preferences.swift */,
5BA9FD3427FEF3C8002DE248 /* PreferencesStyle.swift */,
5BA9FD3A27FEF3C8002DE248 /* PreferencesStyleController.swift */,
5BA9FD3D27FEF3C8002DE248 /* PreferencesTabViewController.swift */,
5BA9FD3B27FEF3C8002DE248 /* PreferencesWindowController.swift */,
5BA9FD3C27FEF3C8002DE248 /* Section.swift */,
5BA9FD3727FEF3C8002DE248 /* SegmentedControlStyleViewController.swift */,
5BA9FD3827FEF3C8002DE248 /* ToolbarItemStyleViewController.swift */,
5BA9FD3127FEF3C8002DE248 /* Utilities.swift */,
);
path = Preferences;
sourceTree = "<group>";
};
5BA9FD8C28006BA7002DE248 /* VDKComboBox */ = {
isa = PBXGroup;
children = (
5BA9FD8A28006B41002DE248 /* VDKComboBox.swift */,
);
path = VDKComboBox;
sourceTree = "<group>";
};
5BBBB75C27AED54C0023B93A /* SoundFiles */ = {
isa = PBXGroup;
children = (
@ -824,7 +920,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1320;
LastUpgradeCheck = 1320;
LastUpgradeCheck = 1330;
TargetAttributes = {
5BD05BB727B2A429004C4F1D = {
CreatedOnToolsVersion = 13.2;
@ -963,40 +1059,61 @@
files = (
5B707CE827D9F4590099EF99 /* OpenCCBridge.swift in Sources */,
D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */,
5BA9FD4527FEF3C9002DE248 /* ToolbarItemStyleViewController.swift in Sources */,
5BA9FD4127FEF3C8002DE248 /* PreferencesStyle.swift in Sources */,
5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */,
6ACC3D442793701600F1B140 /* ParselessPhraseDB.cpp in Sources */,
D461B792279DAC010070E734 /* InputState.swift in Sources */,
5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */,
D47B92C027972AD100458394 /* main.swift in Sources */,
D44FB74D2792189A003C80A6 /* PhraseReplacementMap.mm in Sources */,
D4A13D5A27A59F0B003BE359 /* ctlInputMethod.swift in Sources */,
5BA9FD4827FEF3C9002DE248 /* PreferencesWindowController.swift in Sources */,
D4E569DC27A34D0E00AC2CEF /* KeyHandler.mm in Sources */,
5BA9FD4627FEF3C9002DE248 /* Container.swift in Sources */,
D47F7DD0278C0897002F9DD7 /* ctlNonModalAlertWindow.swift in Sources */,
5B62A32F27AE78B000A19448 /* CoreLM.mm in Sources */,
5BE78BE027B38804005EA1BE /* LMConsolidator.mm in Sources */,
D456576E279E4F7B00DF6BC9 /* KeyParser.swift in Sources */,
5BA9FD1027FEDB6B002DE248 /* suiPrefPaneKeyboard.swift in Sources */,
5BA9FD4327FEF3C8002DE248 /* Preferences.swift in Sources */,
5BA9FD4427FEF3C8002DE248 /* SegmentedControlStyleViewController.swift in Sources */,
D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */,
5BA9FD4227FEF3C8002DE248 /* PreferencePane.swift in Sources */,
5BA9FD8B28006B41002DE248 /* VDKComboBox.swift in Sources */,
D47D73AC27A6CAE600255A50 /* AssociatedPhrases.mm in Sources */,
5BA9FD4A27FEF3C9002DE248 /* PreferencesTabViewController.swift in Sources */,
5B62A34A27AE7CD900A19448 /* NotifierController.swift in Sources */,
5B11328927B94CFB00E58451 /* AppleKeyboardConverter.swift in Sources */,
D41355DB278E6D17005E5CBD /* LMInstantiator.mm in Sources */,
5B62A32927AE77D100A19448 /* FSEventStreamHelper.swift in Sources */,
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */,
5B62A33627AE795800A19448 /* mgrPrefs.swift in Sources */,
5BAEFAD028012565001F42C9 /* mgrLangModel.swift in Sources */,
5B62A33827AE79CD00A19448 /* NSStringUtils.swift in Sources */,
5BA9FD0F27FEDB6B002DE248 /* suiPrefPaneGeneral.swift in Sources */,
5BA9FD4927FEF3C9002DE248 /* Section.swift in Sources */,
5BA9FD3E27FEF3C8002DE248 /* Utilities.swift in Sources */,
5BA9FD1127FEDB6B002DE248 /* ctlPrefUI.swift in Sources */,
5B62A33227AE792F00A19448 /* InputSourceHelper.swift in Sources */,
5B5E535227EF261400C6AA1E /* IME.swift in Sources */,
5B62A34927AE7CD900A19448 /* TooltipController.swift in Sources */,
6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */,
5B62A34827AE7CD900A19448 /* ctlCandidateVertical.swift in Sources */,
5BA9FD4027FEF3C8002DE248 /* Localization.swift in Sources */,
5BA9FD1327FEDB6B002DE248 /* suiPrefPaneDictionary.swift in Sources */,
6ACC3D452793701600F1B140 /* ParselessLM.cpp in Sources */,
5BBBB77A27AEDC690023B93A /* clsSFX.swift in Sources */,
5BA9FD4727FEF3C9002DE248 /* PreferencesStyleController.swift in Sources */,
5BF8423127BAA942008E7E4C /* vChewingKanjiConverter.swift in Sources */,
5B62A34627AE7CD900A19448 /* ctlCandidateHorizontal.swift in Sources */,
5B62A34727AE7CD900A19448 /* ctlCandidate.swift in Sources */,
5BA9FD3F27FEF3C8002DE248 /* Pane.swift in Sources */,
5BB802DA27FABA8300CF1C19 /* ctlInputMethod_Menu.swift in Sources */,
D41355DE278EA3ED005E5CBD /* UserPhrasesLM.mm in Sources */,
6ACC3D3F27914F2400F1B140 /* KeyValueBlobReader.cpp in Sources */,
D41355D8278D74B5005E5CBD /* mgrLangModel.mm in Sources */,
5BDC1CFA27FDF1310052C2B9 /* apiUpdate.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1317,6 +1434,7 @@
"$(OTHER_CFLAGS)",
"-fcxx-modules",
);
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
@ -1358,6 +1476,7 @@
"-fcxx-modules",
);
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Release;
};
@ -1423,7 +1542,7 @@
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SWIFT_OBJC_BRIDGING_HEADER = "Source/Headers/vChewing-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Osize";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
WRAPPER_EXTENSION = app;
};
@ -1485,7 +1604,7 @@
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SWIFT_OBJC_BRIDGING_HEADER = "Source/Headers/vChewing-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Osize";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
WRAPPER_EXTENSION = app;
};

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
LastUpgradeVersion = "1330"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
LastUpgradeVersion = "1330"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
LastUpgradeVersion = "1330"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
LastUpgradeVersion = "1330"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"