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
4 changes: 4 additions & 0 deletions Input Source Pro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
D60001022F20000000000102 /* AppKindComparisonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D60001012F20000000000101 /* AppKindComparisonTests.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -365,6 +366,7 @@
D60000612F20000000000061 /* AppURLAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppURLAction.swift; sourceTree = "<group>"; };
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>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -736,6 +738,7 @@
D60000042F20000000000004 /* Tests */ = {
isa = PBXGroup;
children = (
D60001012F20000000000101 /* AppKindComparisonTests.swift */,
D60000512F20000000000051 /* InputSourceDeduplicationTests.swift */,
D60000112F20000000000011 /* BrowserRuleSelectionTests.swift */,
D60000312F20000000000031 /* BrowserRuleValidationTests.swift */,
Expand Down Expand Up @@ -1055,6 +1058,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D60001022F20000000000102 /* AppKindComparisonTests.swift in Sources */,
D60000522F20000000000052 /* InputSourceDeduplicationTests.swift in Sources */,
D60000122F20000000000012 /* BrowserRuleSelectionTests.swift in Sources */,
D60000322F20000000000032 /* BrowserRuleValidationTests.swift in Sources */,
Expand Down
7 changes: 4 additions & 3 deletions Input Source Pro/Controllers/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,16 @@ class StatusItemController {
var addBrowserRuleMenu: NSMenuItem? {
guard let appKind = applicationVM.appKind,
let browserInfo = appKind.getBrowserInfo(),
let host = browserInfo.url.host
let browserURL = browserInfo.url,
let host = browserURL.host
else { return nil }

let app = appKind.getApp()

var items: [NSMenuItem] = [
BrowserRuleMenuItem(
app: app,
url: browserInfo.url,
url: browserURL,
preferencesVM: preferencesVM,
inputSourceVM: inputSourceVM,
inputSource: nil
Expand All @@ -74,7 +75,7 @@ class StatusItemController {
items += InputSource.sources.map {
BrowserRuleMenuItem(
app: app,
url: browserInfo.url,
url: browserURL,
preferencesVM: preferencesVM,
inputSourceVM: inputSourceVM,
inputSource: $0
Expand Down
99 changes: 74 additions & 25 deletions Input Source Pro/Utilities/AppKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ enum AppKind {
focusedElement: UIElement?,
isFocusOnInputContainer: Bool,

url: URL,
url: URL?,
rule: BrowserRule?,
isFocusedOnAddressBar: Bool
)
Expand All @@ -28,9 +28,10 @@ enum AppKind {
return app.bundleId()
case let .browser(app, info):
if !info.isFocusedOnAddressBar,
info.url != .newtab,
let url = info.url,
url != .newtab,
let bundleId = app.bundleId(),
let addressId = info.rule?.id() ?? info.url.host
let addressId = info.rule?.id() ?? url.host
{
return "\(bundleId)_\(addressId)"
} else {
Expand Down Expand Up @@ -79,16 +80,55 @@ enum AppKind {
guard let otherKind = otherKind else { return false }
guard getApp() == otherKind.getApp() else { return false }

let isSameAddress = getBrowserInfo()?.url == otherKind.getBrowserInfo()?.url
let isSameAddressBar = getBrowserInfo()?.isFocusedOnAddressBar == otherKind.getBrowserInfo()?.isFocusedOnAddressBar
switch (getBrowserInfo(), otherKind.getBrowserInfo()) {
case (nil, nil):
return true
case let (current?, previous?):
if detectAddressBar,
current.isFocusedOnAddressBar != previous.isFocusedOnAddressBar
{
return false
}

// While the address bar stays focused, URL changes reflect omnibox
// edits or suggestions rather than navigation; treating them as
// navigation switches input sources mid-composition (issue #99).
if current.isFocusedOnAddressBar,
previous.isFocusedOnAddressBar
{
return true
}

return detectAddressBar ? (isSameAddressBar && isSameAddress) : isSameAddress
return current.url == previous.url
default:
return false
}
}
}

// MARK: - From

extension AppKind {
static func makeBrowserInfo(
focusedElement: UIElement?,
isFocusOnInputContainer: Bool,
url: URL?,
rule: BrowserRule?,
isFocusedOnAddressBar: Bool
) -> BrowserInfo? {
// An empty omnibox can expose no web-area URL, so address-bar focus
// alone constitutes a browser context (issue #99).
guard url != nil || isFocusedOnAddressBar else { return nil }

return (
focusedElement: focusedElement,
isFocusOnInputContainer: isFocusOnInputContainer,
url: url,
rule: rule,
isFocusedOnAddressBar: isFocusedOnAddressBar
)
}

static func from(_ app: NSRunningApplication?, preferencesVM: PreferencesVM) -> AppKind? {
if let app = app {
return .from(app, preferencesVM: preferencesVM)
Expand All @@ -101,29 +141,38 @@ extension AppKind {
let application = app.getApplication(preferencesVM: preferencesVM)
let focusedElement = app.focuedUIElement(application: application)
let isFocusOnInputContainer = UIElement.isInputContainer(focusedElement)
let isBrowserEnabled = preferencesVM.isBrowserAndEnabled(app)

if let url = preferencesVM.getBrowserURL(app.bundleIdentifier, application: application)?.removeFragment() {
let rule = preferencesVM.getBrowserRule(url: url)
let isFocusOnBrowserAddress = preferencesVM.isFocusOnBrowserAddress(app: app, focusedElement: focusedElement)

return .browser(
if isBrowserEnabled {
let isFocusOnBrowserAddress = preferencesVM.isFocusOnBrowserAddress(
app: app,
info: (
focusedElement,
isFocusOnInputContainer,
url,
rule,
isFocusOnBrowserAddress
)
focusedElement: focusedElement
)
} else {
return .normal(
app: app,
info: (
focusedElement,
isFocusOnInputContainer
let browserURL = preferencesVM
.getBrowserURL(app.bundleIdentifier, application: application)?
.removeFragment()
let rule = browserURL.flatMap { preferencesVM.getBrowserRule(url: $0) }

if let browserInfo = makeBrowserInfo(
focusedElement: focusedElement,
isFocusOnInputContainer: isFocusOnInputContainer,
url: browserURL,
rule: rule,
isFocusedOnAddressBar: isFocusOnBrowserAddress
) {
return .browser(
app: app,
info: browserInfo
)
)
}
}

return .normal(
app: app,
info: (
focusedElement,
isFocusOnInputContainer
)
)
}
}
107 changes: 107 additions & 0 deletions Tests/AppKindComparisonTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import AppKit
import XCTest
@testable import Input_Source_Pro

@MainActor
final class AppKindComparisonTests: XCTestCase {
private let app = NSRunningApplication.current

func testFocusedAddressBarCreatesBrowserInfoWithoutURL() {
let browserInfo = AppKind.makeBrowserInfo(
focusedElement: nil,
isFocusOnInputContainer: true,
url: nil,
rule: nil,
isFocusedOnAddressBar: true
)

XCTAssertNotNil(browserInfo)
XCTAssertNil(browserInfo?.url)
XCTAssertEqual(browserInfo?.isFocusedOnAddressBar, true)
}

func testMissingURLOutsideAddressBarDoesNotCreateBrowserInfo() {
let browserInfo = AppKind.makeBrowserInfo(
focusedElement: nil,
isFocusOnInputContainer: true,
url: nil,
rule: nil,
isFocusedOnAddressBar: false
)

XCTAssertNil(browserInfo)
}

func testFocusedAddressBarPreservesObservedPageURL() {
let url = URL(string: "https://example.com/page")!
let browserInfo = AppKind.makeBrowserInfo(
focusedElement: nil,
isFocusOnInputContainer: true,
url: url,
rule: nil,
isFocusedOnAddressBar: true
)

XCTAssertEqual(browserInfo?.url, url)
}

func testAddressBarTextMutationDoesNotChangeContext() {
let previous = browser(url: URL(string: "https://example.com")!, addressBarFocused: true)
let current = browser(url: URL(string: "https://search.invalid/n")!, addressBarFocused: true)

XCTAssertTrue(current.isSameAppOrWebsite(with: previous, detectAddressBar: true))
}

func testEnteringAddressBarChangesContextWhenDetectionIsEnabled() {
let url = URL(string: "https://example.com")!
let previous = browser(url: url, addressBarFocused: false)
let current = browser(url: url, addressBarFocused: true)

XCTAssertFalse(current.isSameAppOrWebsite(with: previous, detectAddressBar: true))
}

func testLeavingAddressBarChangesContextWhenDetectionIsEnabled() {
let url = URL(string: "https://example.com")!
let previous = browser(url: url, addressBarFocused: true)
let current = browser(url: url, addressBarFocused: false)

XCTAssertFalse(current.isSameAppOrWebsite(with: previous, detectAddressBar: true))
}

func testNavigationChangesContextOutsideAddressBar() {
let previous = browser(url: URL(string: "https://example.com")!, addressBarFocused: false)
let current = browser(url: URL(string: "https://example.com/next")!, addressBarFocused: false)

XCTAssertFalse(current.isSameAppOrWebsite(with: previous, detectAddressBar: true))
}

func testNormalAndBrowserWithUnknownURLAreDifferentContexts() {
let normal = AppKind.normal(
app: app,
info: (focusedElement: nil, isFocusOnInputContainer: true)
)
let unknownBrowser = browser(url: nil, addressBarFocused: true)

XCTAssertFalse(unknownBrowser.isSameAppOrWebsite(with: normal, detectAddressBar: true))
XCTAssertFalse(normal.isSameAppOrWebsite(with: unknownBrowser, detectAddressBar: true))
}

func testNewTabURLDoesNotCreateWebsiteId() {
let newTab = browser(url: .newtab, addressBarFocused: false)

XCTAssertNil(newTab.getId())
}

private func browser(url: URL?, addressBarFocused: Bool) -> AppKind {
return .browser(
app: app,
info: (
focusedElement: nil,
isFocusOnInputContainer: true,
url: url,
rule: nil,
isFocusedOnAddressBar: addressBarFocused
)
)
}
}