diff --git a/Input Source Pro.xcodeproj/project.pbxproj b/Input Source Pro.xcodeproj/project.pbxproj index 1c845b2..9c303ef 100644 --- a/Input Source Pro.xcodeproj/project.pbxproj +++ b/Input Source Pro.xcodeproj/project.pbxproj @@ -175,6 +175,7 @@ D60000622F20000000000062 /* AppURLAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000612F20000000000061 /* AppURLAction.swift */; }; D60000722F20000000000072 /* AppURLActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000712F20000000000071 /* AppURLActionTests.swift */; }; D60000822F20000000000082 /* URLActivationSuppressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60000812F20000000000081 /* URLActivationSuppressionTests.swift */; }; + D60001922F20000000000192 /* ActivateEventInputSourceChangeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60001912F20000000000191 /* ActivateEventInputSourceChangeTests.swift */; }; D60001022F20000000000102 /* AppKindComparisonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60001012F20000000000101 /* AppKindComparisonTests.swift */; }; /* End PBXBuildFile section */ @@ -366,6 +367,7 @@ D60000612F20000000000061 /* AppURLAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLAction.swift; sourceTree = ""; }; D60000712F20000000000071 /* AppURLActionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLActionTests.swift; sourceTree = ""; }; D60000812F20000000000081 /* URLActivationSuppressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLActivationSuppressionTests.swift; sourceTree = ""; }; + D60001912F20000000000191 /* ActivateEventInputSourceChangeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivateEventInputSourceChangeTests.swift; sourceTree = ""; }; D60001012F20000000000101 /* AppKindComparisonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppKindComparisonTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -746,6 +748,7 @@ D60000022F20000000000002 /* TestHarnessTests.swift */, D60000712F20000000000071 /* AppURLActionTests.swift */, D60000812F20000000000081 /* URLActivationSuppressionTests.swift */, + D60001912F20000000000191 /* ActivateEventInputSourceChangeTests.swift */, ); path = Tests; sourceTree = ""; @@ -1066,6 +1069,7 @@ D60000032F20000000000003 /* TestHarnessTests.swift in Sources */, D60000722F20000000000072 /* AppURLActionTests.swift in Sources */, D60000822F20000000000082 /* URLActivationSuppressionTests.swift in Sources */, + D60001922F20000000000192 /* ActivateEventInputSourceChangeTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Input Source Pro/Controllers/IndicatorWindowController+Activation.swift b/Input Source Pro/Controllers/IndicatorWindowController+Activation.swift index aef6659..ffb881c 100644 --- a/Input Source Pro/Controllers/IndicatorWindowController+Activation.swift +++ b/Input Source Pro/Controllers/IndicatorWindowController+Activation.swift @@ -63,6 +63,12 @@ extension IndicatorWindowController { let application = app.getApplication(preferencesVM: preferencesVM) let needActivateAtFirstTime = { + // App switch that keeps the same input source: don't announce it, + // let focus detection decide whether the indicator should be visible. + if event.isAppChangesWithUnchangedInputSource { + return false + } + if preferencesVM.preferences.isActiveWhenSwitchApp { return true } diff --git a/Input Source Pro/Controllers/IndicatorWindowController.swift b/Input Source Pro/Controllers/IndicatorWindowController.swift index 2a42081..a8866c5 100644 --- a/Input Source Pro/Controllers/IndicatorWindowController.swift +++ b/Input Source Pro/Controllers/IndicatorWindowController.swift @@ -73,6 +73,10 @@ class IndicatorWindowController: FloatWindowController { return self.alwaysOnPublisher(event: event, inputSource: inputSource, appKind: appKind) } else if preferencesVM.needDetectFocusedFieldChanges(app: app) { return self.autoShowPublisher(event: event, inputSource: inputSource, appKind: appKind) + } else if event.isAppChangesWithUnchangedInputSource { + // App switch that keeps the same input source: nothing switched, + // so don't pop the indicator up. + return self.justHidePublisher() } else { return self.autoHidePublisher(event: event, inputSource: inputSource, appKind: appKind) } diff --git a/Input Source Pro/Models/IndicatorVM+Triggers.swift b/Input Source Pro/Models/IndicatorVM+Triggers.swift index 5d950c1..6debf9f 100644 --- a/Input Source Pro/Models/IndicatorVM+Triggers.swift +++ b/Input Source Pro/Models/IndicatorVM+Triggers.swift @@ -8,13 +8,13 @@ extension IndicatorVM { enum ActivateEvent { case justHide case longMouseDown - case appChanges(current: AppKind?, prev: AppKind?) + case appChanges(current: AppKind?, prev: AppKind?, inputSourceDidChange: Bool) case inputSourceChanges(InputSource, InputSourceChangeReason) case functionKeyModeChanges(FKeyMode) func isAppChangesWithSameAppOrWebsite() -> Bool { switch self { - case let .appChanges(current, prev): + case let .appChanges(current, prev, _): return current?.isSameAppOrWebsite(with: prev) == true case .inputSourceChanges: return false @@ -27,6 +27,15 @@ extension IndicatorVM { } } + var isAppChangesWithUnchangedInputSource: Bool { + switch self { + case let .appChanges(_, _, inputSourceDidChange): + return !inputSourceDidChange + default: + return false + } + } + var isJustHide: Bool { switch self { case .justHide: return true @@ -73,7 +82,11 @@ extension IndicatorVM { .map { [weak self] previous, current -> ActivateEvent in if let preferencesVM = self?.preferencesVM { if previous?.appKind?.getId() != current.appKind?.getId() { - let event = ActivateEvent.appChanges(current: current.appKind, prev: previous?.appKind) + let event = ActivateEvent.appChanges( + current: current.appKind, + prev: previous?.appKind, + inputSourceDidChange: previous?.inputSource.persistentIdentifier != current.inputSource.persistentIdentifier + ) if preferencesVM.preferences.isActiveWhenSwitchApp || preferencesVM.preferences.isActiveWhenFocusedElementChangesEnabled { if preferencesVM.preferences.isHideWhenSwitchAppWithForceKeyboard { @@ -116,8 +129,8 @@ extension IndicatorVM { extension IndicatorVM.ActivateEvent: @preconcurrency CustomStringConvertible { var description: String { switch self { - case let .appChanges(current, prev): - return "appChanges(\(String(describing: current)), \(String(describing: prev))" + case let .appChanges(current, prev, inputSourceDidChange): + return "appChanges(\(String(describing: current)), \(String(describing: prev)), inputSourceDidChange: \(inputSourceDidChange))" case .inputSourceChanges: return "inputSourceChanges" case let .functionKeyModeChanges(mode): diff --git a/Input Source Pro/Models/IndicatorVM.swift b/Input Source Pro/Models/IndicatorVM.swift index fed9918..ca74f87 100644 --- a/Input Source Pro/Models/IndicatorVM.swift +++ b/Input Source Pro/Models/IndicatorVM.swift @@ -242,6 +242,17 @@ extension IndicatorVM { return state case let .appChanged(appKind): if let status = preferencesVM.getAppAutoSwitchKeyboard(appKind) { + // The target keyboard is already active: skip the redundant + // TIS select (and CJKV fix) so nothing "switches", and mark + // the reason as .noChanges so the indicator won't announce it. + if status.inputSource.persistentIdentifier == state.inputSource.persistentIdentifier { + return updateState( + appKind: appKind, + inputSource: state.inputSource, + inputSourceChangeReason: .noChanges + ) + } + inputSourceVM.select(inputSource: status.inputSource, app: appKind.getApp()) return updateState( diff --git a/Tests/ActivateEventInputSourceChangeTests.swift b/Tests/ActivateEventInputSourceChangeTests.swift new file mode 100644 index 0000000..b60527c --- /dev/null +++ b/Tests/ActivateEventInputSourceChangeTests.swift @@ -0,0 +1,41 @@ +import AppKit +import XCTest +@testable import Input_Source_Pro + +@MainActor +final class ActivateEventInputSourceChangeTests: XCTestCase { + private let app = NSRunningApplication.current + + private func appKind() -> AppKind { + .normal(app: app, info: (focusedElement: nil, isFocusOnInputContainer: true)) + } + + func testAppChangesWithUnchangedInputSourceIsFlagged() { + let event = IndicatorVM.ActivateEvent.appChanges( + current: appKind(), + prev: appKind(), + inputSourceDidChange: false + ) + + XCTAssertTrue(event.isAppChangesWithUnchangedInputSource) + XCTAssertTrue(event.isAppChangesWithSameAppOrWebsite()) + XCTAssertFalse(event.isJustHide) + } + + func testAppChangesWithChangedInputSourceIsNotFlagged() { + let event = IndicatorVM.ActivateEvent.appChanges( + current: appKind(), + prev: appKind(), + inputSourceDidChange: true + ) + + XCTAssertFalse(event.isAppChangesWithUnchangedInputSource) + XCTAssertTrue(event.isAppChangesWithSameAppOrWebsite()) + XCTAssertFalse(event.isJustHide) + } + + func testNonAppChangeEventsAreNotFlagged() { + XCTAssertFalse(IndicatorVM.ActivateEvent.justHide.isAppChangesWithUnchangedInputSource) + XCTAssertFalse(IndicatorVM.ActivateEvent.longMouseDown.isAppChangesWithUnchangedInputSource) + } +} \ No newline at end of file