Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Input Source Pro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,13 @@
D60000722F20000000000072 /* AppURLActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000712F20000000000071 /* AppURLActionTests.swift */; };
D60000822F20000000000082 /* URLActivationSuppressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000812F20000000000081 /* URLActivationSuppressionTests.swift */; };
D60001022F20000000000102 /* AppKindComparisonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60001012F20000000000101 /* AppKindComparisonTests.swift */; };
C13120191255119A7A95E7C2 /* MarkdownPunctuationMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C131A22D5FAE13E5E9F14E9F /* MarkdownPunctuationMapping.swift */; };
C1310BA14D066C83C9331602 /* MarkdownModeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C131A06895BA856414E76CE3 /* MarkdownModeController.swift */; };
C131FE7BD70BFF2056A66384 /* MarkdownModeSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13161EC493FE46EA8BCD8CD /* MarkdownModeSettingsView.swift */; };
C1315059B7754D59E9CFFAD6 /* MarkdownPunctuationMappingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13142E5127B2ED3F334E51A /* MarkdownPunctuationMappingTests.swift */; };
C131285C8C1EBC5FDCF0111E /* PunctuationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1311723BF0607366B8F236C /* PunctuationServiceTests.swift */; };
C13103D84A17E0335FF5132B /* PermissionsVMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C131B63FBB110A4009BABB8E /* PermissionsVMTests.swift */; };
C131FE642ECFD99AA25588ED /* SettingsBackupPreferencesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C131984D932C971B3780C1D0 /* SettingsBackupPreferencesTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -367,6 +374,13 @@
D60000712F20000000000071 /* AppURLActionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLActionTests.swift; sourceTree = "<group>"; };
D60000812F20000000000081 /* URLActivationSuppressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLActivationSuppressionTests.swift; sourceTree = "<group>"; };
D60001012F20000000000101 /* AppKindComparisonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKindComparisonTests.swift; sourceTree = "<group>"; };
C131A22D5FAE13E5E9F14E9F /* MarkdownPunctuationMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownPunctuationMapping.swift; sourceTree = "<group>"; };
C131A06895BA856414E76CE3 /* MarkdownModeController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownModeController.swift; sourceTree = "<group>"; };
C13161EC493FE46EA8BCD8CD /* MarkdownModeSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownModeSettingsView.swift; sourceTree = "<group>"; };
C13142E5127B2ED3F334E51A /* MarkdownPunctuationMappingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownPunctuationMappingTests.swift; sourceTree = "<group>"; };
C1311723BF0607366B8F236C /* PunctuationServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PunctuationServiceTests.swift; sourceTree = "<group>"; };
C131B63FBB110A4009BABB8E /* PermissionsVMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsVMTests.swift; sourceTree = "<group>"; };
C131984D932C971B3780C1D0 /* SettingsBackupPreferencesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBackupPreferencesTests.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -484,6 +498,7 @@
4A2A175D280BA7FA00E13249 /* Utilities */ = {
isa = PBXGroup;
children = (
C131A22D5FAE13E5E9F14E9F /* MarkdownPunctuationMapping.swift */,
25C5A6B42E678D86005AB80E /* PunctuationService.swift */,
D598C7E228B74B16004747D1 /* Indicator */,
D598C7E428B74B48004747D1 /* AppKit */,
Expand Down Expand Up @@ -529,6 +544,7 @@
4A2A1770280BA7FA00E13249 /* Models */ = {
isa = PBXGroup;
children = (
C131A06895BA856414E76CE3 /* MarkdownModeController.swift */,
4A2A1773280BA7FA00E13249 /* IndicatorVM.swift */,
4AC2614A2870735B0032AAFE /* IndicatorVM+Triggers.swift */,
4A2A1774280BA7FA00E13249 /* PreferencesVM.swift */,
Expand Down Expand Up @@ -579,6 +595,7 @@
4A2A177A280BA7FA00E13249 /* Components */ = {
isa = PBXGroup;
children = (
C13161EC493FE46EA8BCD8CD /* MarkdownModeSettingsView.swift */,
4A2A177B280BA7FA00E13249 /* IndicatorView.swift */,
4A1BD90E287BD14000E4D8C2 /* CustomizedIndicatorView.swift */,
4AC450BE281D841300DA0329 /* PreferenceSection.swift */,
Expand Down Expand Up @@ -738,6 +755,10 @@
D60000042F20000000000004 /* Tests */ = {
isa = PBXGroup;
children = (
C131984D932C971B3780C1D0 /* SettingsBackupPreferencesTests.swift */,
C131B63FBB110A4009BABB8E /* PermissionsVMTests.swift */,
C1311723BF0607366B8F236C /* PunctuationServiceTests.swift */,
C13142E5127B2ED3F334E51A /* MarkdownPunctuationMappingTests.swift */,
D60001012F20000000000101 /* AppKindComparisonTests.swift */,
D60000512F20000000000051 /* InputSourceDeduplicationTests.swift */,
D60000112F20000000000011 /* BrowserRuleSelectionTests.swift */,
Expand Down Expand Up @@ -923,6 +944,9 @@
4A881A93288BC55E00B76498 /* PreferencesVM+KeyboardConfig.swift in Sources */,
4A2A1789280BA7FA00E13249 /* TISInputSource+Extension.swift in Sources */,
4AC50D6B283A72810034E894 /* NSColor.swift in Sources */,
C13120191255119A7A95E7C2 /* MarkdownPunctuationMapping.swift in Sources */,
C1310BA14D066C83C9331602 /* MarkdownModeController.swift in Sources */,
C131FE7BD70BFF2056A66384 /* MarkdownModeSettingsView.swift in Sources */,
25C5A6B52E678D86005AB80E /* PunctuationService.swift in Sources */,
4ACC9B812D2640F70002B8CE /* AddSwitchingGroupButton.swift in Sources */,
4A2A178C280BA7FA00E13249 /* CancelBag.swift in Sources */,
Expand Down Expand Up @@ -1063,6 +1087,10 @@
D60000122F20000000000012 /* BrowserRuleSelectionTests.swift in Sources */,
D60000322F20000000000032 /* BrowserRuleValidationTests.swift in Sources */,
D60000222F20000000000022 /* RuntimeRuleChangeNotifierTests.swift in Sources */,
C1315059B7754D59E9CFFAD6 /* MarkdownPunctuationMappingTests.swift in Sources */,
C131285C8C1EBC5FDCF0111E /* PunctuationServiceTests.swift in Sources */,
C13103D84A17E0335FF5132B /* PermissionsVMTests.swift in Sources */,
C131FE642ECFD99AA25588ED /* SettingsBackupPreferencesTests.swift in Sources */,
D60000032F20000000000003 /* TestHarnessTests.swift in Sources */,
D60000722F20000000000072 /* AppURLActionTests.swift in Sources */,
D60000822F20000000000082 /* URLActivationSuppressionTests.swift in Sources */,
Expand Down
66 changes: 54 additions & 12 deletions Input Source Pro/Models/IndicatorVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ final class IndicatorVM: ObservableObject {
let inputSourceVM: InputSourceVM
let permissionsVM: PermissionsVM
let punctuationService: PunctuationService
private let markdownModeController: MarkdownModeController

let logger = ISPLogger(category: String(describing: IndicatorVM.self))

Expand All @@ -29,6 +30,12 @@ final class IndicatorVM: ObservableObject {
@Published
private(set) var state: State

@Published
private(set) var isMarkdownModeEnabled = false

@Published
private(set) var markdownModeFailure: PunctuationService.Failure?

var actionSubject = PassthroughSubject<Action, Never>()

var refreshShortcutSubject = PassthroughSubject<Void, Never>()
Expand Down Expand Up @@ -72,7 +79,14 @@ final class IndicatorVM: ObservableObject {
self.preferencesVM = preferencesVM
self.applicationVM = applicationVM
self.inputSourceVM = inputSourceVM
self.punctuationService = PunctuationService(preferencesVM: preferencesVM)
let punctuationService = PunctuationService()
self.punctuationService = punctuationService
self.markdownModeController = MarkdownModeController(
service: punctuationService,
shouldEnableAppEnglish: applicationVM.appKind.map {
preferencesVM.getAppCustomization(app: $0.getApp())?.shouldForceEnglishPunctuation == true
} ?? false
)
state = .from(
preferencesVM: preferencesVM,
inputSourceChangeReason: .system,
Expand All @@ -82,7 +96,7 @@ final class IndicatorVM: ObservableObject {

clearAppKeyboardCacheIfNeed()
watchState()
watchPunctuationRules()
watchMarkdownMode()
watchFunctionKeyMode()
}

Expand All @@ -106,19 +120,47 @@ final class IndicatorVM: ObservableObject {
.store(in: cancelBag)
}

private func watchPunctuationRules() {
func setMarkdownModeEnabled(_ enabled: Bool) {
markdownModeController.setEnabled(enabled)
permissionsVM.refresh()
}

private func watchMarkdownMode() {
markdownModeController.$isEnabled
.assign(to: &$isMarkdownModeEnabled)
markdownModeController.$failure
.assign(to: &$markdownModeFailure)

markdownModeController.bindPreference(
values: preferencesVM.$preferences.map(\.isMarkdownModeEnabled).eraseToAnyPublisher(),
currentValue: { [preferencesVM] in preferencesVM.preferences.isMarkdownModeEnabled },
update: { [preferencesVM] enabled in
preferencesVM.update { $0.isMarkdownModeEnabled = enabled }
}
)
.store(in: cancelBag)

applicationVM.$appKind
.compactMap { $0 }
.sink { [weak self] appKind in
guard let self = self else { return }

let app = appKind.getApp()
if self.punctuationService.shouldEnableForApp(app) {
self.logger.debug { "Enabling English punctuation for app: \(app.localizedName ?? app.bundleIdentifier ?? "Unknown")" }
self.punctuationService.enable()
} else {
self.punctuationService.disable()
}
let shouldEnableAppEnglish = appKind.map {
self.preferencesVM.getAppCustomization(app: $0.getApp())?.shouldForceEnglishPunctuation == true
} ?? false
self.markdownModeController.appContextChanged(shouldEnableAppEnglish: shouldEnableAppEnglish)
}
.store(in: cancelBag)

Publishers.CombineLatest(permissionsVM.$isInputMonitoringEnabled, permissionsVM.$isAccessibilityEnabled)
.sink { [weak self] _, _ in
self?.markdownModeController.revalidateMarkdown()
}
.store(in: cancelBag)

NSWorkspace.shared.notificationCenter
.publisher(for: NSWorkspace.didLaunchApplicationNotification)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.markdownModeController.revalidateMarkdown()
}
.store(in: cancelBag)
}
Expand Down
99 changes: 99 additions & 0 deletions Input Source Pro/Models/MarkdownModeController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import Combine

@MainActor
final class MarkdownModeController: ObservableObject {
@Published private(set) var isEnabled = false
@Published private(set) var failure: PunctuationService.Failure?

var onPreferenceChange: ((Bool) -> Void)?

private let service: PunctuationService
private var shouldEnableAppEnglish: Bool
private var requestedEnabled = false
private var isStoppedForSafety = false
private var isUpdatingPreference = false

init(service: PunctuationService, shouldEnableAppEnglish: Bool) {
self.service = service
self.shouldEnableAppEnglish = shouldEnableAppEnglish
service.onSafetyShutdown = { [weak self] shutdown in
guard shutdown.mode == .markdown else { return }
self?.recordSafetyShutdown(shutdown.failure)
}
}

func bindPreference(
values: AnyPublisher<Bool, Never>,
currentValue: @escaping () -> Bool,
update: @escaping (Bool) -> Void
) -> AnyCancellable {
onPreferenceChange = { [weak self] enabled in
guard let self = self, currentValue() != enabled else { return }
self.isUpdatingPreference = true
update(enabled)
self.isUpdatingPreference = false
}
var didReceivePreference = false
return values.sink { [weak self] enabled in
guard let self = self, !self.isUpdatingPreference else { return }
guard !didReceivePreference || enabled != self.isEnabled else { return }
didReceivePreference = true
self.setEnabled(enabled)
}
}

func setEnabled(_ enabled: Bool) {
requestedEnabled = enabled
isStoppedForSafety = false
reconcile()
}

func appContextChanged(shouldEnableAppEnglish: Bool) {
self.shouldEnableAppEnglish = shouldEnableAppEnglish
reconcile()
}

func revalidateMarkdown() {
guard requestedEnabled else { return }
reconcile()
}

private func reconcile() {
if requestedEnabled {
let wasMarkdownActive = service.activeMode == .markdown
switch service.enable(mode: .markdown) {
case .success:
isEnabled = true
failure = nil
onPreferenceChange?(true)
case .failure(let failure):
self.failure = failure
requestedEnabled = false
isEnabled = false
isStoppedForSafety = wasMarkdownActive
applyAppEnglishRule()
onPreferenceChange?(false)
}
} else {
isEnabled = false
applyAppEnglishRule()
onPreferenceChange?(false)
}
}

private func applyAppEnglishRule() {
if !isStoppedForSafety && shouldEnableAppEnglish {
service.enable(mode: .appEnglish)
} else {
service.disable()
}
}

private func recordSafetyShutdown(_ failure: PunctuationService.Failure) {
isStoppedForSafety = true
requestedEnabled = false
isEnabled = false
self.failure = failure
onPreferenceChange?(false)
}
}
36 changes: 31 additions & 5 deletions Input Source Pro/Models/PermissionsVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,46 @@ final class PermissionsVM: ObservableObject {
}
}

@Published var isAccessibilityEnabled = PermissionsVM.checkAccessibility(prompt: false)
@Published var isInputMonitoringEnabled = PermissionsVM.checkInputMonitoring(prompt: false)
@Published var isAccessibilityEnabled: Bool
@Published var isInputMonitoringEnabled: Bool

init() {
private let accessibilityCheck: @MainActor () -> Bool
private let inputMonitoringCheck: @MainActor () -> Bool
private var activationSubscription: AnyCancellable?

init(
accessibilityCheck: @escaping @MainActor () -> Bool = { PermissionsVM.checkAccessibility(prompt: false) },
inputMonitoringCheck: @escaping @MainActor () -> Bool = { PermissionsVM.checkInputMonitoring(prompt: false) },
notificationCenter: NotificationCenter = .default
) {
self.accessibilityCheck = accessibilityCheck
self.inputMonitoringCheck = inputMonitoringCheck
isAccessibilityEnabled = accessibilityCheck()
isInputMonitoringEnabled = inputMonitoringCheck()
watchAccessibilityChange()
watchInputMonitoringChange()
activationSubscription = notificationCenter.publisher(for: NSApplication.didBecomeActiveNotification)
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in self?.refresh() }
}

func refresh() {
let accessibilityEnabled = accessibilityCheck()
let inputMonitoringEnabled = inputMonitoringCheck()
if isAccessibilityEnabled != accessibilityEnabled {
isAccessibilityEnabled = accessibilityEnabled
}
if isInputMonitoringEnabled != inputMonitoringEnabled {
isInputMonitoringEnabled = inputMonitoringEnabled
}
}

private func watchAccessibilityChange() {
guard !isAccessibilityEnabled else { return }

Timer
.interval(seconds: 1)
.map { _ in Self.checkAccessibility(prompt: false) }
.map { [accessibilityCheck] _ in accessibilityCheck() }
.filter { $0 }
.first()
.assign(to: &$isAccessibilityEnabled)
Expand All @@ -44,7 +70,7 @@ final class PermissionsVM: ObservableObject {

Timer
.interval(seconds: 1)
.map { _ in Self.checkInputMonitoring(prompt: false) }
.map { [inputMonitoringCheck] _ in inputMonitoringCheck() }
.filter { $0 }
.first()
.assign(to: &$isInputMonitoringEnabled)
Expand Down
12 changes: 12 additions & 0 deletions Input Source Pro/Models/PreferencesVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ struct Preferences {

static let isLaunchAtLogin = "isLaunchAtLogin"
static let isShowIconInMenuBar = "isShowIconInMenuBar"
static let isMarkdownModeEnabled = "isMarkdownModeEnabled"
static let isEnhancedModeEnabled = "isDetectSpotlightLikeApp"
static let isCJKVFixEnabled = "isCJKVFixEnabled"
static let cJKVFixStrategy = "cJKVFixStrategy"
Expand Down Expand Up @@ -335,6 +336,14 @@ struct Preferences {

fileprivate init() {}

init(markdownModeUserDefaults: UserDefaults) {
_isMarkdownModeEnabled = UserDefault(
wrappedValue: false,
Preferences.Key.isMarkdownModeEnabled,
userDefaults: markdownModeUserDefaults
)
}

@UserDefault(Preferences.Key.prevInstalledBuildVersion)
var prevInstalledBuildVersion = 0

Expand All @@ -346,6 +355,9 @@ struct Preferences {
@UserDefault(Preferences.Key.isShowIconInMenuBar)
var isShowIconInMenuBar = true

@UserDefault(Preferences.Key.isMarkdownModeEnabled)
var isMarkdownModeEnabled = false

@UserDefault(Preferences.Key.isEnhancedModeEnabled)
var isEnhancedModeEnabled = false

Expand Down
Loading