Compare commits
3 Commits
bc7e4fd013
...
0f49209d95
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f49209d95 | |||
| 8766584fe3 | |||
| a891d49bb1 |
@@ -44,7 +44,7 @@ Conjure searches the system and user Applications folders. Hover over a result t
|
|||||||
|
|
||||||
Conjure runs without a Dock icon. Dismissing the launcher leaves it running; reopen it from Finder or use your configured global shortcut.
|
Conjure runs without a Dock icon. Dismissing the launcher leaves it running; reopen it from Finder or use your configured global shortcut.
|
||||||
|
|
||||||
The launcher opens without a window animation; Settings is constructed only when needed, not before the launcher appears.
|
The launcher, emoji picker, and translation tool open immediately without window animations or content fades, including from global shortcuts. Shortcut destinations are prepared before the window appears. Settings is constructed only when needed, not before the launcher appears.
|
||||||
|
|
||||||
### Controls
|
### Controls
|
||||||
|
|
||||||
@@ -54,13 +54,15 @@ The launcher opens without a window animation; Settings is constructed only when
|
|||||||
| Open a result or copy a calculated answer | Return or double-click |
|
| Open a result or copy a calculated answer | Return or double-click |
|
||||||
| Open actions for the selected entry | Command-K |
|
| Open actions for the selected entry | Command-K |
|
||||||
| Open one of the first nine results | Hold Command until numbers appear, then press 1–9 |
|
| Open one of the first nine results | Hold Command until numbers appear, then press 1–9 |
|
||||||
| Go back or dismiss the launcher | Escape |
|
| Dismiss the launcher, including the current Command and Actions | Escape or Command-W |
|
||||||
| Dismiss the launcher | Command-W |
|
| Go back without dismissing | Back chevron |
|
||||||
| Open Settings | Command-comma |
|
| Open Settings | Command-comma |
|
||||||
| Quit | Command-Q |
|
| Quit | Command-Q |
|
||||||
|
|
||||||
Right-click the launcher background for Settings and Quit. Actions include favorites, resetting usage ranking, revealing applications in Finder, quitting them, and uninstalling them.
|
Right-click the launcher background for Settings and Quit. Actions include favorites, resetting usage ranking, revealing applications in Finder, quitting them, and uninstalling them.
|
||||||
|
|
||||||
|
The Actions surface crops the launcher’s full-size background at its own position, keeping the glows continuous without showing the results underneath.
|
||||||
|
|
||||||
### Global shortcuts and launch at login
|
### Global shortcuts and launch at login
|
||||||
|
|
||||||
In Settings, choose a **Global shortcut destination**: **Applications**, **Conjure Emoji**, or **Conjure Translate**. Choose **Record Shortcut…** and press a combination containing Command, Option, or Control. For example, select **Conjure Emoji** and record Command-Shift-Space to open the emoji picker directly. **Clear** disables only the selected destination's shortcut. No shortcuts are assigned by default; existing launcher bindings are preserved.
|
In Settings, choose a **Global shortcut destination**: **Applications**, **Conjure Emoji**, or **Conjure Translate**. Choose **Record Shortcut…** and press a combination containing Command, Option, or Control. For example, select **Conjure Emoji** and record Command-Shift-Space to open the emoji picker directly. **Clear** disables only the selected destination's shortcut. No shortcuts are assigned by default; existing launcher bindings are preserved.
|
||||||
@@ -104,19 +106,19 @@ Rates are shared across queries and refreshed on the next currency query after t
|
|||||||
|
|
||||||
Search for **Conjure Emoji**, identified by its centered 🤔 icon, and open it. Browse the grid or search by name, keyword, `:shortcode:`, or emoji. Use the category menu to filter results or show **Recently Used**. Your last 30 selections are saved locally. Favorites appear first, followed by recent selections when browsing All Emoji.
|
Search for **Conjure Emoji**, identified by its centered 🤔 icon, and open it. Browse the grid or search by name, keyword, `:shortcode:`, or emoji. Use the category menu to filter results or show **Recently Used**. Your last 30 selections are saved locally. Favorites appear first, followed by recent selections when browsing All Emoji.
|
||||||
|
|
||||||
Use the arrow keys or Control-H/J/K/L to navigate left/down/up/right; the grid scrolls to keep the selected emoji visible. Return, clicking an emoji, or **Copy and Paste** copies it, dismisses Conjure, and tries to paste into the app that was focused when the launcher opened. Automatic pasting requires **System Settings → Privacy & Security → Accessibility → Conjure**; macOS prompts if permission is missing. After granting it, select an emoji again. If permission is denied, the previous app is unavailable, or focus cannot be restored promptly, the emoji stays copied for manual Command-V. Escape or the Back chevron returns to the launcher and restores your search.
|
Use the arrow keys or Control-H/J/K/L to navigate left/down/up/right; the grid scrolls to keep the selected emoji visible. Return, clicking an emoji, or **Copy and Paste** copies it, dismisses Conjure, and tries to paste into the app that was focused when the launcher opened. Automatic pasting requires **System Settings → Privacy & Security → Accessibility → Conjure**; macOS prompts if permission is missing. After granting it, select an emoji again. If permission is denied, the previous app is unavailable, or focus cannot be restored promptly, the emoji stays copied for manual Command-V. The Back chevron returns to the launcher and restores your search.
|
||||||
|
|
||||||
If macOS keeps prompting despite an enabled Accessibility entry, quit Conjure, remove its stale entry with the minus button, add the exact app you launch (normally `/Applications/Conjure.app`), enable it, then reopen Conjure and retry. Avoid switching between installed and `.build` copies. For permission that survives rebuilds, configure certificate signing as described above. **Copy Emoji** followed by manual Command-V works without this permission.
|
If macOS keeps prompting despite an enabled Accessibility entry, quit Conjure, remove its stale entry with the minus button, add the exact app you launch (normally `/Applications/Conjure.app`), enable it, then reopen Conjure and retry. Avoid switching between installed and `.build` copies. For permission that survives rebuilds, configure certificate signing as described above. **Copy Emoji** followed by manual Command-V works without this permission.
|
||||||
|
|
||||||
Open **Actions** with Command-K for **Copy and Paste** (default), **Copy Emoji** (copy only, no Accessibility permission needed), **Favorite**, or **Remove Favorite**. Favorites are saved locally and grouped ahead of other emoji, with a small extra spacing break when browsing without a search. There are no star badges.
|
Open **Actions** with Command-K for **Copy and Paste** (default), **Copy Emoji** (copy only, no Accessibility permission needed), **Favorite**, or **Remove Favorite**. Favorites are saved locally and grouped ahead of other emoji, with a small extra spacing break when browsing without a search. There are no star badges.
|
||||||
|
|
||||||
With Actions open, Escape or the Back chevron closes Actions first; another Back returns to Applications and restores your search. Command-W dismisses the entire launcher, including the current Command and its Actions. Switching destinations also removes the previous Command's Actions completely. Copy and Copy and Paste dismiss the launcher without an intermediate Back step.
|
With Actions open, the Back chevron closes Actions first; another Back returns to Applications and restores your search. Escape or Command-W dismisses the entire launcher, including the current Command and its Actions. Switching destinations also removes the previous Command's Actions completely. Copy and Copy and Paste dismiss the launcher without an intermediate Back step.
|
||||||
|
|
||||||
The picker works offline with 1,870 emoji from [gemoji v4.1.0](https://github.com/github/gemoji/tree/v4.1.0). Its [MIT license](Sources/ConjureKit/Resources/LICENSE-gemoji.txt) is included in the application.
|
The picker works offline with 1,870 emoji from [gemoji v4.1.0](https://github.com/github/gemoji/tree/v4.1.0). Its [MIT license](Sources/ConjureKit/Resources/LICENSE-gemoji.txt) is included in the application.
|
||||||
|
|
||||||
## Translation
|
## Translation
|
||||||
|
|
||||||
Search for **Conjure Translate** and open it. Enter English in the search bar, choose a provider, then press Return or click **Translate**. Typing alone never sends text. Copy selected Japanese output with Command-C; the copy hint appears beside the Translate action at the bottom. The Back chevron beside the input or Escape returns to the launcher and restores your search.
|
Search for **Conjure Translate** and open it. Enter English in the search bar, choose a provider, then press Return or click **Translate**. Typing alone never sends text. Copy selected Japanese output with Command-C; the copy hint appears beside the Translate action at the bottom. The Back chevron beside the input returns to the launcher and restores your search; Escape dismisses Conjure.
|
||||||
|
|
||||||
Editing the input, switching providers, forgetting the key, or leaving Translate cancels the active request; cancelled results are not shown. After a request fails, submit again to retry.
|
Editing the input, switching providers, forgetting the key, or leaving Translate cancels the active request; cancelled results are not shown. After a request fails, submit again to retry.
|
||||||
|
|
||||||
@@ -127,7 +129,7 @@ Editing the input, switching providers, forgetting the key, or leaving Translate
|
|||||||
|
|
||||||
## Uninstalling applications
|
## Uninstalling applications
|
||||||
|
|
||||||
Select an application, open **Actions → Uninstall application**, and review the discovered files. Return toggles the highlighted file's inclusion; **Command-Return opens confirmation**. Back or Escape cancels review.
|
Select an application, open **Actions → Uninstall application**, and review the discovered files. Return toggles the highlighted file's inclusion; **Command-Return opens confirmation**. Back cancels review and returns to the launcher; Escape dismisses the entire launcher. Closing remains disabled while an uninstall is running, and confirmation sheets retain their own cancellation behavior.
|
||||||
|
|
||||||
Discovery looks for the application and associated files identified by its bundle IDs. Shared app-group data starts unchecked because other applications may use it. Review selections carefully: discovery is conservative, not a guarantee that every associated file is found. Protected applications and unsafe or inaccessible files cannot be removed. Running applications are asked to quit normally; Conjure does not force-quit them or request administrator privileges.
|
Discovery looks for the application and associated files identified by its bundle IDs. Shared app-group data starts unchecked because other applications may use it. Review selections carefully: discovery is conservative, not a guarantee that every associated file is found. Protected applications and unsafe or inaccessible files cannot be removed. Running applications are asked to quit normally; Conjure does not force-quit them or request administrator privileges.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import ConjureKit
|
|||||||
@MainActor
|
@MainActor
|
||||||
final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate {
|
final class ApplicationActionsController: NSViewController, NSSearchFieldDelegate {
|
||||||
private let search = ActionsSearchField()
|
private let search = ActionsSearchField()
|
||||||
|
private let background = LauncherBackground(frame: .zero)
|
||||||
private let entries = [
|
private let entries = [
|
||||||
ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text),
|
ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text),
|
||||||
ActionEntry(title: "Favorite", symbol: "star", color: SunsetPalette.text),
|
ActionEntry(title: "Favorite", symbol: "star", color: SunsetPalette.text),
|
||||||
@@ -49,11 +50,10 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
view.layer?.shadowOffset = NSSize(width: 0, height: -4)
|
view.layer?.shadowOffset = NSSize(width: 0, height: -4)
|
||||||
let content = NSVisualEffectView()
|
let content = NSVisualEffectView()
|
||||||
content.material = .hudWindow
|
content.material = .hudWindow
|
||||||
content.blendingMode = .withinWindow
|
content.blendingMode = .behindWindow
|
||||||
content.state = .active
|
content.state = .active
|
||||||
roundMaterialCorners(content, radius: 18)
|
roundMaterialCorners(content, radius: 18)
|
||||||
content.layer?.borderWidth = 0.5
|
content.layer?.borderWidth = 0.5
|
||||||
let background = LauncherBackground(frame: .zero)
|
|
||||||
background.setAccessibilityElement(false)
|
background.setAccessibilityElement(false)
|
||||||
content.addSubview(background)
|
content.addSubview(background)
|
||||||
applicationLabel.textColor = SunsetPalette.secondaryText
|
applicationLabel.textColor = SunsetPalette.secondaryText
|
||||||
@@ -108,10 +108,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
content.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
content.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
content.topAnchor.constraint(equalTo: view.topAnchor),
|
content.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
content.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
content.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
background.leadingAnchor.constraint(equalTo: content.leadingAnchor),
|
|
||||||
background.trailingAnchor.constraint(equalTo: content.trailingAnchor),
|
|
||||||
background.topAnchor.constraint(equalTo: content.topAnchor),
|
|
||||||
background.bottomAnchor.constraint(equalTo: content.bottomAnchor),
|
|
||||||
applicationLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
applicationLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
|
||||||
applicationLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
|
applicationLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -16),
|
||||||
applicationLabel.topAnchor.constraint(equalTo: content.topAnchor, constant: 14),
|
applicationLabel.topAnchor.constraint(equalTo: content.topAnchor, constant: 14),
|
||||||
@@ -164,6 +160,13 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
view.alphaValue = 0
|
view.alphaValue = 0
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
host.addSubview(view)
|
host.addSubview(view)
|
||||||
|
// Crop the window-sized background; filtering must not rescale its glows.
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
background.leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
||||||
|
background.trailingAnchor.constraint(equalTo: host.trailingAnchor),
|
||||||
|
background.topAnchor.constraint(equalTo: host.topAnchor),
|
||||||
|
background.bottomAnchor.constraint(equalTo: host.bottomAnchor)
|
||||||
|
])
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
view.trailingAnchor.constraint(equalTo: pill.trailingAnchor),
|
view.trailingAnchor.constraint(equalTo: pill.trailingAnchor),
|
||||||
view.bottomAnchor.constraint(equalTo: pill.bottomAnchor)
|
view.bottomAnchor.constraint(equalTo: pill.bottomAnchor)
|
||||||
@@ -267,7 +270,6 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
|||||||
switch selector {
|
switch selector {
|
||||||
case #selector(NSResponder.insertNewline(_:)):
|
case #selector(NSResponder.insertNewline(_:)):
|
||||||
if let selectedEntry { chooseAction(at: selectedEntry) }
|
if let selectedEntry { chooseAction(at: selectedEntry) }
|
||||||
case #selector(NSResponder.cancelOperation(_:)): close()
|
|
||||||
case #selector(NSResponder.moveUp(_:)): moveSelection(by: -1)
|
case #selector(NSResponder.moveUp(_:)): moveSelection(by: -1)
|
||||||
case #selector(NSResponder.moveDown(_:)): moveSelection(by: 1)
|
case #selector(NSResponder.moveDown(_:)): moveSelection(by: 1)
|
||||||
default: return false
|
default: return false
|
||||||
|
|||||||
@@ -116,7 +116,6 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
|||||||
actions.close(animated: false)
|
actions.close(animated: false)
|
||||||
closed?()
|
closed?()
|
||||||
}
|
}
|
||||||
override func cancelOperation(_ sender: Any?) { goBack() }
|
|
||||||
|
|
||||||
func inputChanged(preservingSelection: Bool = false) {
|
func inputChanged(preservingSelection: Bool = false) {
|
||||||
let previous = preservingSelection && results.indices.contains(selected) ? results[selected].value : nil
|
let previous = preservingSelection && results.indices.contains(selected) ? results[selected].value : nil
|
||||||
@@ -223,7 +222,6 @@ final class EmojiPickerController: NSViewController, NSCollectionViewDataSource,
|
|||||||
case #selector(NSResponder.moveUp(_:)): moveVertically(-1)
|
case #selector(NSResponder.moveUp(_:)): moveVertically(-1)
|
||||||
case #selector(NSResponder.moveDown(_:)): moveVertically(1)
|
case #selector(NSResponder.moveDown(_:)): moveVertically(1)
|
||||||
case #selector(NSResponder.insertNewline(_:)): copyAndPasteSelection()
|
case #selector(NSResponder.insertNewline(_:)): copyAndPasteSelection()
|
||||||
case #selector(NSResponder.cancelOperation(_:)): goBack()
|
|
||||||
default: return false
|
default: return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -132,6 +132,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
searchField.stringValue = ""
|
searchField.stringValue = ""
|
||||||
restorePosition()
|
restorePosition()
|
||||||
updateResults()
|
updateResults()
|
||||||
|
if let command {
|
||||||
|
showCommand(command)
|
||||||
|
history.recordLaunch(of: Application(command: command))
|
||||||
|
}
|
||||||
window?.deminiaturize(nil)
|
window?.deminiaturize(nil)
|
||||||
showWindow(nil)
|
showWindow(nil)
|
||||||
NSApp.activate()
|
NSApp.activate()
|
||||||
@@ -140,10 +144,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
(window as? LauncherPanel)?.sampleNumberHints()
|
(window as? LauncherPanel)?.sampleNumberHints()
|
||||||
(searchField.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
|
(searchField.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
|
||||||
refreshApplications()
|
refreshApplications()
|
||||||
if let command {
|
|
||||||
showCommand(command)
|
|
||||||
history.recordLaunch(of: Application(command: command))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggle(command: Application.Command? = nil) {
|
func toggle(command: Application.Command? = nil) {
|
||||||
@@ -158,9 +158,13 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
|
|
||||||
func dismiss(restoringFocus: Bool = true) {
|
func dismiss(restoringFocus: Bool = true) {
|
||||||
guard !restoringFocus || (!isLaunching && window?.attachedSheet == nil) else { return }
|
guard !restoringFocus || (!isLaunching && window?.attachedSheet == nil) else { return }
|
||||||
if let review = uninstallReview { review.goBack(); return }
|
if launcherFrameBeforeReview != nil {
|
||||||
|
guard uninstallReview?.canClose != false else { return }
|
||||||
|
pendingPresentation = { [weak self] in self?.dismiss(restoringFocus: restoringFocus) }
|
||||||
|
uninstallReview?.goBack()
|
||||||
|
return
|
||||||
|
}
|
||||||
closeCommand()
|
closeCommand()
|
||||||
guard launcherFrameBeforeReview == nil else { return }
|
|
||||||
actions.close(animated: false)
|
actions.close(animated: false)
|
||||||
alignmentGuides?.end()
|
alignmentGuides?.end()
|
||||||
let previous = previousApplication
|
let previous = previousApplication
|
||||||
@@ -257,7 +261,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
commandBack.isBordered = false
|
commandBack.isBordered = false
|
||||||
commandBack.contentTintColor = SunsetPalette.secondaryText
|
commandBack.contentTintColor = SunsetPalette.secondaryText
|
||||||
commandBack.setAccessibilityLabel("Back to applications")
|
commandBack.setAccessibilityLabel("Back to applications")
|
||||||
commandBack.toolTip = "Back to applications (Escape)"
|
commandBack.toolTip = "Back to applications"
|
||||||
commandBack.isHidden = true
|
commandBack.isHidden = true
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
commandBack.widthAnchor.constraint(equalToConstant: IconStyle.applicationSize(for: searchFont)),
|
commandBack.widthAnchor.constraint(equalToConstant: IconStyle.applicationSize(for: searchFont)),
|
||||||
@@ -478,10 +482,13 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
||||||
|
if commandSelector == #selector(NSResponder.cancelOperation(_:)) {
|
||||||
|
dismiss()
|
||||||
|
return true
|
||||||
|
}
|
||||||
if let emojiPicker { return emojiPicker.handle(commandSelector) }
|
if let emojiPicker { return emojiPicker.handle(commandSelector) }
|
||||||
if let translation {
|
if let translation {
|
||||||
if commandSelector == #selector(NSResponder.insertNewline(_:)) { translation.translate(); return true }
|
if commandSelector == #selector(NSResponder.insertNewline(_:)) { translation.translate(); return true }
|
||||||
if commandSelector == #selector(NSResponder.cancelOperation(_:)) { translation.goBack(); return true }
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
switch commandSelector {
|
switch commandSelector {
|
||||||
@@ -491,8 +498,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
moveSelection(by: -1)
|
moveSelection(by: -1)
|
||||||
case #selector(NSResponder.insertNewline(_:)):
|
case #selector(NSResponder.insertNewline(_:)):
|
||||||
openSelection()
|
openSelection()
|
||||||
case #selector(NSResponder.cancelOperation(_:)):
|
|
||||||
dismiss()
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -864,7 +869,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
view.setAccessibilityHidden(true)
|
view.setAccessibilityHidden(true)
|
||||||
}
|
}
|
||||||
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
controller.view.alphaValue = 0
|
|
||||||
content.addSubview(controller.view)
|
content.addSubview(controller.view)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
controller.view.leadingAnchor.constraint(equalTo: content.leadingAnchor),
|
controller.view.leadingAnchor.constraint(equalTo: content.leadingAnchor),
|
||||||
@@ -872,10 +876,6 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
controller.view.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 16),
|
controller.view.topAnchor.constraint(equalTo: searchField.bottomAnchor, constant: 16),
|
||||||
controller.view.bottomAnchor.constraint(equalTo: content.bottomAnchor),
|
controller.view.bottomAnchor.constraint(equalTo: content.bottomAnchor),
|
||||||
])
|
])
|
||||||
NSAnimationContext.runAnimationGroup { context in
|
|
||||||
context.duration = NSWorkspace.shared.accessibilityDisplayShouldReduceMotion ? 0 : 0.18
|
|
||||||
controller.view.animator().alphaValue = 1
|
|
||||||
}
|
|
||||||
window?.makeFirstResponder(searchField)
|
window?.makeFirstResponder(searchField)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1201,6 +1201,11 @@ final class LauncherPanel: NSPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func sendEvent(_ event: NSEvent) {
|
override func sendEvent(_ event: NSEvent) {
|
||||||
|
if event.type == .keyDown, event.keyCode == 53, attachedSheet == nil,
|
||||||
|
event.modifierFlags.intersection([.command, .control, .option, .shift]).isEmpty {
|
||||||
|
cancelOperation(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
let eligible = numberShortcutsEligible
|
let eligible = numberShortcutsEligible
|
||||||
if event.type == .flagsChanged || (event.type == .keyUp && event.keyCode == 49) {
|
if event.type == .flagsChanged || (event.type == .keyUp && event.keyCode == 49) {
|
||||||
reevaluateNumberHints(modifierFlags: event.modifierFlags, eligible: eligible, at: event.timestamp)
|
reevaluateNumberHints(modifierFlags: event.modifierFlags, eligible: eligible, at: event.timestamp)
|
||||||
@@ -1233,6 +1238,10 @@ final class LauncherPanel: NSPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func cancelOperation(_ sender: Any?) {
|
||||||
|
dismiss?()
|
||||||
|
}
|
||||||
|
|
||||||
override func performClose(_ sender: Any?) {
|
override func performClose(_ sender: Any?) {
|
||||||
dismiss?()
|
dismiss?()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import ConjureKit
|
|||||||
import Security
|
import Security
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class TranslationController: NSViewController, NSTextFieldDelegate, NSTextViewDelegate {
|
final class TranslationController: NSViewController, NSTextFieldDelegate {
|
||||||
private let keyField = NSSecureTextField()
|
private let keyField = NSSecureTextField()
|
||||||
private let provider = NSPopUpButton()
|
private let provider = NSPopUpButton()
|
||||||
private let credentials = NSStackView()
|
private let credentials = NSStackView()
|
||||||
@@ -59,7 +59,7 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText
|
|||||||
translateButton.isBordered = false
|
translateButton.isBordered = false
|
||||||
translateButton.font = .systemFont(ofSize: 12, weight: .medium)
|
translateButton.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
translateButton.contentTintColor = SunsetPalette.text
|
translateButton.contentTintColor = SunsetPalette.text
|
||||||
translateButton.toolTip = "Translate the English input (Return). Escape returns to the launcher."
|
translateButton.toolTip = "Translate the English input (Return). Escape dismisses Conjure."
|
||||||
let returnKey = ShortcutIndicator("↵")
|
let returnKey = ShortcutIndicator("↵")
|
||||||
let actionPill = NSView()
|
let actionPill = NSView()
|
||||||
actionPill.wantsLayer = true
|
actionPill.wantsLayer = true
|
||||||
@@ -91,7 +91,6 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText
|
|||||||
output.textColor = SunsetPalette.text
|
output.textColor = SunsetPalette.text
|
||||||
output.font = .systemFont(ofSize: 20)
|
output.font = .systemFont(ofSize: 20)
|
||||||
output.setAccessibilityLabel("Japanese translation")
|
output.setAccessibilityLabel("Japanese translation")
|
||||||
output.delegate = self
|
|
||||||
let scroll = NSScrollView()
|
let scroll = NSScrollView()
|
||||||
scroll.documentView = output
|
scroll.documentView = output
|
||||||
scroll.hasVerticalScroller = true
|
scroll.hasVerticalScroller = true
|
||||||
@@ -129,20 +128,6 @@ final class TranslationController: NSViewController, NSTextFieldDelegate, NSText
|
|||||||
updateProvider()
|
updateProvider()
|
||||||
}
|
}
|
||||||
|
|
||||||
override func cancelOperation(_ sender: Any?) { goBack() }
|
|
||||||
|
|
||||||
func control(_ control: NSControl, textView: NSTextView, doCommandBy selector: Selector) -> Bool {
|
|
||||||
guard selector == #selector(NSResponder.cancelOperation(_:)) else { return false }
|
|
||||||
goBack()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func textView(_ textView: NSTextView, doCommandBy selector: Selector) -> Bool {
|
|
||||||
guard selector == #selector(NSResponder.cancelOperation(_:)) else { return false }
|
|
||||||
goBack()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func controlTextDidChange(_ notification: Notification) { inputChanged() }
|
func controlTextDidChange(_ notification: Notification) { inputChanged() }
|
||||||
|
|
||||||
@objc func goBack() {
|
@objc func goBack() {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
|||||||
private var window: NSWindow? { view.window }
|
private var window: NSWindow? { view.window }
|
||||||
var closed: (() -> Void)?
|
var closed: (() -> Void)?
|
||||||
var changed: (() -> Void)?
|
var changed: (() -> Void)?
|
||||||
|
var canClose: Bool { !busy }
|
||||||
|
|
||||||
init(application: Application, uninstaller: ApplicationUninstaller) {
|
init(application: Application, uninstaller: ApplicationUninstaller) {
|
||||||
self.application = application
|
self.application = application
|
||||||
@@ -117,7 +118,6 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
|||||||
table.allowsEmptySelection = false
|
table.allowsEmptySelection = false
|
||||||
table.setAccessibilityLabel("Files to uninstall for \(application.name)")
|
table.setAccessibilityLabel("Files to uninstall for \(application.name)")
|
||||||
table.toggle = { [weak self] in self?.toggleHighlighted() }
|
table.toggle = { [weak self] in self?.toggleHighlighted() }
|
||||||
table.dismiss = { [weak self] in self?.goBack() }
|
|
||||||
let scroll = NSScrollView()
|
let scroll = NSScrollView()
|
||||||
scroll.documentView = table
|
scroll.documentView = table
|
||||||
scroll.hasVerticalScroller = true
|
scroll.hasVerticalScroller = true
|
||||||
@@ -229,7 +229,6 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
|||||||
case #selector(NSResponder.moveDown(_:)): moveSelection(1)
|
case #selector(NSResponder.moveDown(_:)): moveSelection(1)
|
||||||
case #selector(NSResponder.moveUp(_:)): moveSelection(-1)
|
case #selector(NSResponder.moveUp(_:)): moveSelection(-1)
|
||||||
case #selector(NSResponder.insertNewline(_:)): toggleHighlighted()
|
case #selector(NSResponder.insertNewline(_:)): toggleHighlighted()
|
||||||
case #selector(NSResponder.cancelOperation(_:)): goBack()
|
|
||||||
default: return false
|
default: return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -391,10 +390,8 @@ final class UninstallReviewController: NSViewController, NSSearchFieldDelegate,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func cancelOperation(_ sender: Any?) { goBack() }
|
|
||||||
|
|
||||||
@objc func goBack() {
|
@objc func goBack() {
|
||||||
guard !busy else { return }
|
guard canClose else { return }
|
||||||
scanTask?.cancel()
|
scanTask?.cancel()
|
||||||
(view as! UninstallReviewContent).acceptsInteraction = false
|
(view as! UninstallReviewContent).acceptsInteraction = false
|
||||||
view.setAccessibilityHidden(true)
|
view.setAccessibilityHidden(true)
|
||||||
@@ -467,10 +464,8 @@ private final class UninstallFileCell: NSTableCellView {
|
|||||||
@MainActor
|
@MainActor
|
||||||
private final class UninstallFilesTable: NSTableView {
|
private final class UninstallFilesTable: NSTableView {
|
||||||
var toggle: (() -> Void)?
|
var toggle: (() -> Void)?
|
||||||
var dismiss: (() -> Void)?
|
|
||||||
override func keyDown(with event: NSEvent) {
|
override func keyDown(with event: NSEvent) {
|
||||||
if [36, 76, 49].contains(event.keyCode), event.modifierFlags.intersection([.command, .option, .control]).isEmpty { toggle?() }
|
if [36, 76, 49].contains(event.keyCode), event.modifierFlags.intersection([.command, .option, .control]).isEmpty { toggle?() }
|
||||||
else if event.keyCode == 53 { dismiss?() }
|
|
||||||
else { super.keyDown(with: event) }
|
else { super.keyDown(with: event) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
- [x] bugfix: the emoji picker currently doesn't scroll with the selection (arrows or ctrl+hjkl), only with mouse scroll.
|
- [x] bugfix: the emoji picker currently doesn't scroll with the selection (arrows or ctrl+hjkl), only with mouse scroll.
|
||||||
- the window should scroll with the current selection
|
- the window should scroll with the current selection
|
||||||
- [ ] make escape close the window that was opened, be it the application launcher, the emoji picker, or something else.
|
- [x] make escape close the window that was opened, be it the application launcher, the emoji picker, or something else.
|
||||||
- clicking the "back" chevron should still take you back to the application launcher view though (as it does today).
|
- clicking the "back" chevron should still take you back to the application launcher view though (as it does today).
|
||||||
|
- [x] remove window open/fade in/transition animation when opening conjure from keybindings
|
||||||
|
- this is already the case for the application launcher window.
|
||||||
|
- evidently not the case for the emoji picker (though it should be ubiquitous)
|
||||||
- [ ] add a notes app inspired by raycast notes
|
- [ ] add a notes app inspired by raycast notes
|
||||||
|
- support multiple different notes
|
||||||
|
- support switching between notes
|
||||||
|
- highlight markdown syntax
|
||||||
|
- allow images
|
||||||
- [ ] improve on the conjure translation command
|
- [ ] improve on the conjure translation command
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import AppKit
|
||||||
|
import ConjureKit
|
||||||
|
import Testing
|
||||||
|
@testable import Conjure
|
||||||
|
|
||||||
|
@Test @MainActor func actionsBackgroundKeepsWindowCoordinates() throws {
|
||||||
|
_ = NSApplication.shared
|
||||||
|
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 480),
|
||||||
|
styleMask: [.borderless], backing: .buffered, defer: false)
|
||||||
|
let host = try #require(window.contentView)
|
||||||
|
let anchor = NSView(frame: NSRect(x: 700, y: 20, width: 30, height: 30))
|
||||||
|
host.addSubview(anchor)
|
||||||
|
let actions = ApplicationActionsController()
|
||||||
|
defer { actions.close(animated: false) }
|
||||||
|
func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) }
|
||||||
|
for width in [750.0, 900.0] {
|
||||||
|
window.setContentSize(NSSize(width: width, height: 480))
|
||||||
|
actions.showEmoji(named: "Smile", relativeTo: anchor)
|
||||||
|
let background = try #require(descendants(host).compactMap { $0 as? LauncherBackground }.first)
|
||||||
|
let search = try #require(descendants(actions.view).compactMap { $0 as? NSSearchField }.first)
|
||||||
|
for query in ["", "Copy Emoji", "no matching action"] {
|
||||||
|
search.stringValue = query
|
||||||
|
actions.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||||
|
host.layoutSubtreeIfNeeded()
|
||||||
|
let rect = background.convert(background.bounds, to: host)
|
||||||
|
#expect(rect == host.bounds)
|
||||||
|
#expect(background.bounds.size != actions.view.bounds.size)
|
||||||
|
}
|
||||||
|
actions.close(animated: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -202,7 +202,7 @@ import Testing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test @MainActor func emojiCommandRestoresLauncherSearchOnEscape() throws {
|
@Test @MainActor func emojiCommandBackNavigationRestoresLauncherSearch() throws {
|
||||||
_ = NSApplication.shared
|
_ = NSApplication.shared
|
||||||
let suite = "ConjureEmojiCommand.\(UUID().uuidString)"
|
let suite = "ConjureEmojiCommand.\(UUID().uuidString)"
|
||||||
let defaults = UserDefaults(suiteName: suite)!
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
@@ -215,6 +215,9 @@ import Testing
|
|||||||
search.stringValue = "emoji"
|
search.stringValue = "emoji"
|
||||||
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))
|
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))
|
||||||
|
let back = try #require(descendants(content).compactMap { $0 as? NSButton }.first {
|
||||||
|
!$0.isHidden && $0.accessibilityLabel() == "Back to applications"
|
||||||
|
})
|
||||||
let grid = try #require(descendants(content).compactMap { $0 as? NSCollectionView }.first)
|
let grid = try #require(descendants(content).compactMap { $0 as? NSCollectionView }.first)
|
||||||
search.stringValue = ":woman_technologist:"
|
search.stringValue = ":woman_technologist:"
|
||||||
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||||
@@ -224,12 +227,12 @@ import Testing
|
|||||||
$0.title == "Actions" && $0.target is EmojiPickerController
|
$0.title == "Actions" && $0.target is EmojiPickerController
|
||||||
})
|
})
|
||||||
(actionsButton.target as? EmojiPickerController)?.showActions()
|
(actionsButton.target as? EmojiPickerController)?.showActions()
|
||||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
|
back.performClick(nil)
|
||||||
#expect(descendants(content).contains { $0 === grid })
|
#expect(descendants(content).contains { $0 === grid })
|
||||||
#expect(search.stringValue == ":woman_technologist:")
|
#expect(search.stringValue == ":woman_technologist:")
|
||||||
let copyButton = try #require(descendants(content).compactMap { $0 as? NSButton }.first { $0.title == "Copy and Paste" })
|
let copyButton = try #require(descendants(content).compactMap { $0 as? NSButton }.first { $0.title == "Copy and Paste" })
|
||||||
#expect(copyButton.keyEquivalent == "\r")
|
#expect(copyButton.keyEquivalent == "\r")
|
||||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
|
back.performClick(nil)
|
||||||
#expect(!descendants(content).contains { $0 === grid })
|
#expect(!descendants(content).contains { $0 === grid })
|
||||||
#expect(search.stringValue == "emoji")
|
#expect(search.stringValue == "emoji")
|
||||||
let table = try #require(descendants(content).compactMap { $0 as? NSTableView }.first)
|
let table = try #require(descendants(content).compactMap { $0 as? NSTableView }.first)
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import AppKit
|
||||||
|
import ConjureKit
|
||||||
|
import Testing
|
||||||
|
@testable import Conjure
|
||||||
|
|
||||||
|
@Test(arguments: ["launcher", "launcher-actions", "emoji", "emoji-actions", "translate-input", "translate-output", "translate-key"])
|
||||||
|
@MainActor func escapeDismissesEntireLauncher(_ destination: String) throws {
|
||||||
|
_ = NSApplication.shared
|
||||||
|
let suite = "ConjureEscape.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
|
defer { defaults.removePersistentDomain(forName: suite) }
|
||||||
|
let launcher = LauncherWindowController(
|
||||||
|
history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }),
|
||||||
|
defaults: defaults)
|
||||||
|
let window = try #require(launcher.window)
|
||||||
|
defer { window.orderOut(nil) }
|
||||||
|
let content = try #require(window.contentView)
|
||||||
|
func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) }
|
||||||
|
let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first)
|
||||||
|
if !destination.hasPrefix("launcher") {
|
||||||
|
search.stringValue = destination.hasPrefix("emoji") ? "emoji" : "translate"
|
||||||
|
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||||
|
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))
|
||||||
|
}
|
||||||
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
window.makeFirstResponder(search)
|
||||||
|
if destination == "launcher-actions" {
|
||||||
|
search.stringValue = "emoji"
|
||||||
|
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||||
|
let button = try #require(descendants(content).compactMap { $0 as? NSButton }.first {
|
||||||
|
$0.title == "Actions" && $0.target === launcher
|
||||||
|
})
|
||||||
|
button.performClick(nil)
|
||||||
|
}
|
||||||
|
if destination == "emoji-actions" {
|
||||||
|
let button = try #require(descendants(content).compactMap { $0 as? NSButton }.first {
|
||||||
|
$0.title == "Actions" && $0.target is EmojiPickerController
|
||||||
|
})
|
||||||
|
(button.target as? EmojiPickerController)?.showActions()
|
||||||
|
} else if destination == "translate-output" {
|
||||||
|
let output = try #require(descendants(content).compactMap { $0 as? NSTextView }.first {
|
||||||
|
$0.accessibilityLabel() == "Japanese translation"
|
||||||
|
})
|
||||||
|
window.makeFirstResponder(output)
|
||||||
|
} else if destination == "translate-key" {
|
||||||
|
let provider = try #require(descendants(content).compactMap { $0 as? NSPopUpButton }.first)
|
||||||
|
provider.selectItem(at: 1)
|
||||||
|
_ = provider.target?.perform(provider.action, with: provider)
|
||||||
|
let key = try #require(descendants(content).compactMap { $0 as? NSSecureTextField }.first)
|
||||||
|
window.makeFirstResponder(key)
|
||||||
|
}
|
||||||
|
#expect(window.isVisible)
|
||||||
|
let escape = try #require(NSEvent.keyEvent(
|
||||||
|
with: .keyDown, location: .zero, modifierFlags: [], timestamp: 1,
|
||||||
|
windowNumber: window.windowNumber, context: nil, characters: "\u{1b}",
|
||||||
|
charactersIgnoringModifiers: "\u{1b}", isARepeat: false, keyCode: 53))
|
||||||
|
window.sendEvent(escape)
|
||||||
|
#expect(!window.isVisible)
|
||||||
|
#expect(!descendants(content).contains { $0 is NSCollectionView })
|
||||||
|
}
|
||||||
@@ -32,3 +32,38 @@ import Testing
|
|||||||
#expect(!descendants(content).contains { $0 is NSCollectionView })
|
#expect(!descendants(content).contains { $0 is NSCollectionView })
|
||||||
#expect(search.accessibilityLabel() == "Search applications, calculate, or convert units and currencies")
|
#expect(search.accessibilityLabel() == "Search applications, calculate, or convert units and currencies")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(arguments: Application.Command.allCases, [true, false])
|
||||||
|
@MainActor func commandsAreFullyVisibleImmediately(_ command: Application.Command, _ fromShortcut: Bool) throws {
|
||||||
|
_ = NSApplication.shared
|
||||||
|
let suite = "ConjurePresentation.\(UUID().uuidString)"
|
||||||
|
let defaults = UserDefaults(suiteName: suite)!
|
||||||
|
defer { defaults.removePersistentDomain(forName: suite) }
|
||||||
|
let launcher = LauncherWindowController(
|
||||||
|
history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }),
|
||||||
|
defaults: defaults)
|
||||||
|
let window = try #require(launcher.window)
|
||||||
|
defer { window.orderOut(nil) }
|
||||||
|
let content = try #require(window.contentView)
|
||||||
|
func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) }
|
||||||
|
if fromShortcut {
|
||||||
|
launcher.toggle(command: command)
|
||||||
|
} else {
|
||||||
|
launcher.present()
|
||||||
|
let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first)
|
||||||
|
search.stringValue = command.rawValue
|
||||||
|
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||||
|
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))
|
||||||
|
}
|
||||||
|
let tool = try #require(content.subviews.first { view in
|
||||||
|
descendants(view).contains {
|
||||||
|
switch command {
|
||||||
|
case .emoji: $0 is NSCollectionView
|
||||||
|
case .translate: $0.accessibilityLabel() == "Japanese translation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
#expect(window.isVisible)
|
||||||
|
#expect(!tool.isHidden)
|
||||||
|
#expect(tool.alphaValue == 1)
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import Testing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test @MainActor func translationCommandOpensToolAndEscapeRestoresSearch() throws {
|
@Test @MainActor func translationCommandBackNavigationRestoresSearch() throws {
|
||||||
_ = NSApplication.shared
|
_ = NSApplication.shared
|
||||||
let launcher = LauncherWindowController()
|
let launcher = LauncherWindowController()
|
||||||
let content = try #require(launcher.window?.contentView)
|
let content = try #require(launcher.window?.contentView)
|
||||||
@@ -42,12 +42,6 @@ import Testing
|
|||||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))
|
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))
|
||||||
#expect(search.stringValue.isEmpty)
|
#expect(search.stringValue.isEmpty)
|
||||||
#expect(search.accessibilityLabel() == "English text to translate into Japanese")
|
#expect(search.accessibilityLabel() == "English text to translate into Japanese")
|
||||||
search.stringValue = "Good morning"
|
|
||||||
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
|
||||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
|
|
||||||
#expect(search.stringValue == "translate")
|
|
||||||
#expect(table.numberOfRows == 1)
|
|
||||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.insertNewline(_:)))
|
|
||||||
let back = try #require(descendants(content).compactMap { $0 as? NSButton }.first {
|
let back = try #require(descendants(content).compactMap { $0 as? NSButton }.first {
|
||||||
!$0.isHidden && $0.accessibilityLabel() == "Back to applications"
|
!$0.isHidden && $0.accessibilityLabel() == "Back to applications"
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user