style: align translation controls with launcher design
This commit is contained in:
@@ -81,9 +81,9 @@ Applications with identical display names show a muted containing-folder hint be
|
|||||||
|
|
||||||
## Conjure Translate
|
## Conjure Translate
|
||||||
|
|
||||||
Search for **Conjure Translate**, labeled **Command**, and open it with Return, double-click, or **Open Command**. The launcher fades into an English → Japanese translation tool; the existing search bar becomes the English input. Escape or **Back** returns to the previous search.
|
Search for **Conjure Translate**, labeled **Command**, and open it with Return, double-click, or **Open Command**. The launcher fades into an English → Japanese translation tool; the existing search bar becomes the English input and retains the launcher's placeholder styling. Escape returns to the previous search.
|
||||||
|
|
||||||
Choose **MyMemory (no key)** or **OpenAI · gpt-6-luna** from the provider dropdown. MyMemory is the initial default, and the selection is saved locally. Switching providers cancels pending work and clears the previous translation; there is no automatic fallback. Press Return from the English input or click **Translate** to send text to the selected provider.
|
Choose **MyMemory (no key)** or **OpenAI · gpt-6-luna** from the provider dropdown. MyMemory is the initial default, and the selection is saved locally. Switching providers cancels pending work and clears the previous translation; there is no automatic fallback. Press Return from the English input or click **Translate** in the bottom-right launcher-style action pill, beside its Return key badge, to send text to the selected provider.
|
||||||
|
|
||||||
[MyMemory](https://mymemory.translated.net/doc/spec.php) requires no account or key. Anonymous use allows 500 UTF-8 bytes per request and [5,000 characters per day](https://mymemory.translated.net/doc/usagelimits.php). Oversized input is rejected before sending; quota and service errors appear in the tool. Text is sent to a third-party translation service: avoid sensitive content.
|
[MyMemory](https://mymemory.translated.net/doc/spec.php) requires no account or key. Anonymous use allows 500 UTF-8 bytes per request and [5,000 characters per day](https://mymemory.translated.net/doc/usagelimits.php). Oversized input is rejected before sending; quota and service errors appear in the tool. Text is sent to a third-party translation service: avoid sensitive content.
|
||||||
|
|
||||||
|
|||||||
@@ -776,6 +776,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
translation = controller
|
translation = controller
|
||||||
controller.input = { [weak self] in self?.searchField.stringValue ?? "" }
|
controller.input = { [weak self] in self?.searchField.stringValue ?? "" }
|
||||||
let previousQuery = searchField.stringValue
|
let previousQuery = searchField.stringValue
|
||||||
|
let previousPlaceholder = searchField.placeholderAttributedString
|
||||||
controller.closed = { [weak self, weak controller] in
|
controller.closed = { [weak self, weak controller] in
|
||||||
guard let self, let controller else { return }
|
guard let self, let controller else { return }
|
||||||
controller.view.removeFromSuperview()
|
controller.view.removeFromSuperview()
|
||||||
@@ -785,7 +786,7 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
view.setAccessibilityHidden(false)
|
view.setAccessibilityHidden(false)
|
||||||
view.alphaValue = 1
|
view.alphaValue = 1
|
||||||
}
|
}
|
||||||
self.searchField.placeholderString = "Search apps, calculate, or convert…"
|
self.searchField.placeholderAttributedString = previousPlaceholder
|
||||||
self.searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies")
|
self.searchField.setAccessibilityLabel("Search applications, calculate, or convert units and currencies")
|
||||||
self.searchField.stringValue = previousQuery
|
self.searchField.stringValue = previousQuery
|
||||||
self.openButton.keyEquivalent = "\r"
|
self.openButton.keyEquivalent = "\r"
|
||||||
@@ -796,7 +797,10 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
|||||||
(window as? LauncherPanel)?.resetNumberHints()
|
(window as? LauncherPanel)?.resetNumberHints()
|
||||||
openButton.keyEquivalent = ""
|
openButton.keyEquivalent = ""
|
||||||
searchField.stringValue = ""
|
searchField.stringValue = ""
|
||||||
searchField.placeholderString = "Enter English text to translate…"
|
searchField.placeholderAttributedString = NSAttributedString(
|
||||||
|
string: "Enter English text to translate…",
|
||||||
|
attributes: previousPlaceholder?.attributes(at: 0, effectiveRange: nil) ?? [:]
|
||||||
|
)
|
||||||
searchField.setAccessibilityLabel("English text to translate into Japanese")
|
searchField.setAccessibilityLabel("English text to translate into Japanese")
|
||||||
for view in launcherViews.dropFirst() {
|
for view in launcherViews.dropFirst() {
|
||||||
view.isHidden = true
|
view.isHidden = true
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ final class TranslationController: NSViewController {
|
|||||||
private var usesMyMemory: Bool { provider.indexOfSelectedItem == 0 }
|
private var usesMyMemory: Bool { provider.indexOfSelectedItem == 0 }
|
||||||
private let output = NSTextView()
|
private let output = NSTextView()
|
||||||
private let status = NSTextField(wrappingLabelWithString: "")
|
private let status = NSTextField(wrappingLabelWithString: "")
|
||||||
private let translateButton = NSButton(title: "Translate ↵", target: nil, action: nil)
|
private let translateButton = NSButton(title: "Translate", target: nil, action: nil)
|
||||||
private var task: Task<Void, Never>?
|
private var task: Task<Void, Never>?
|
||||||
var input: (() -> String)?
|
var input: (() -> String)?
|
||||||
var closed: (() -> Void)?
|
var closed: (() -> Void)?
|
||||||
@@ -22,10 +22,9 @@ final class TranslationController: NSViewController {
|
|||||||
|
|
||||||
override func loadView() {
|
override func loadView() {
|
||||||
view = NSView()
|
view = NSView()
|
||||||
let back = NSButton(title: "‹ Back", target: self, action: #selector(goBack))
|
|
||||||
let title = NSTextField(labelWithString: "Conjure Translate · English → Japanese")
|
let title = NSTextField(labelWithString: "Conjure Translate · English → Japanese")
|
||||||
title.font = .systemFont(ofSize: 13, weight: .semibold)
|
title.font = .systemFont(ofSize: 13, weight: .semibold)
|
||||||
let heading = NSStackView(views: [back, title, NSView()])
|
let heading = NSStackView(views: [title, NSView()])
|
||||||
provider.addItems(withTitles: ["MyMemory (no key)", "OpenAI · gpt-6-luna"])
|
provider.addItems(withTitles: ["MyMemory (no key)", "OpenAI · gpt-6-luna"])
|
||||||
provider.selectItem(at: UserDefaults.standard.string(forKey: "translationProvider") == "openai" ? 1 : 0)
|
provider.selectItem(at: UserDefaults.standard.string(forKey: "translationProvider") == "openai" ? 1 : 0)
|
||||||
provider.setAccessibilityLabel("Translation provider")
|
provider.setAccessibilityLabel("Translation provider")
|
||||||
@@ -41,6 +40,33 @@ final class TranslationController: NSViewController {
|
|||||||
translateButton.target = self
|
translateButton.target = self
|
||||||
translateButton.action = #selector(translate)
|
translateButton.action = #selector(translate)
|
||||||
translateButton.keyEquivalent = "\r"
|
translateButton.keyEquivalent = "\r"
|
||||||
|
translateButton.isBordered = false
|
||||||
|
translateButton.font = .systemFont(ofSize: 12, weight: .medium)
|
||||||
|
translateButton.contentTintColor = SunsetPalette.text
|
||||||
|
translateButton.toolTip = "Translate the English input (Return). Escape returns to the launcher."
|
||||||
|
let returnKey = ShortcutIndicator("↵")
|
||||||
|
let actionPill = NSView()
|
||||||
|
actionPill.wantsLayer = true
|
||||||
|
actionPill.layer?.cornerRadius = 18
|
||||||
|
actionPill.layer?.borderWidth = 0.5
|
||||||
|
actionPill.layer?.borderColor = SunsetPalette.accent.withAlphaComponent(0.35).cgColor
|
||||||
|
actionPill.layer?.backgroundColor = SunsetPalette.background.withAlphaComponent(0.25).cgColor
|
||||||
|
for control in [translateButton, returnKey] {
|
||||||
|
control.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
actionPill.addSubview(control)
|
||||||
|
}
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
actionPill.heightAnchor.constraint(equalToConstant: 36),
|
||||||
|
translateButton.leadingAnchor.constraint(equalTo: actionPill.leadingAnchor, constant: 14),
|
||||||
|
translateButton.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
|
||||||
|
returnKey.leadingAnchor.constraint(equalTo: translateButton.trailingAnchor, constant: 8),
|
||||||
|
returnKey.centerYAnchor.constraint(equalTo: actionPill.centerYAnchor),
|
||||||
|
returnKey.trailingAnchor.constraint(equalTo: actionPill.trailingAnchor, constant: -12),
|
||||||
|
])
|
||||||
|
let footer = NSStackView(views: [NSView(), actionPill])
|
||||||
|
footer.orientation = .horizontal
|
||||||
|
footer.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(footer)
|
||||||
output.isEditable = false
|
output.isEditable = false
|
||||||
output.isSelectable = true
|
output.isSelectable = true
|
||||||
output.drawsBackground = false
|
output.drawsBackground = false
|
||||||
@@ -56,7 +82,7 @@ final class TranslationController: NSViewController {
|
|||||||
output.textContainer?.widthTracksTextView = true
|
output.textContainer?.widthTracksTextView = true
|
||||||
status.textColor = SunsetPalette.secondaryText
|
status.textColor = SunsetPalette.secondaryText
|
||||||
status.font = .systemFont(ofSize: 12)
|
status.font = .systemFont(ofSize: 12)
|
||||||
let stack = NSStackView(views: [heading, provider, credentials, scroll, status, translateButton])
|
let stack = NSStackView(views: [heading, provider, credentials, scroll, status])
|
||||||
stack.orientation = .vertical
|
stack.orientation = .vertical
|
||||||
stack.alignment = .leading
|
stack.alignment = .leading
|
||||||
stack.spacing = 12
|
stack.spacing = 12
|
||||||
@@ -66,7 +92,11 @@ final class TranslationController: NSViewController {
|
|||||||
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
||||||
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
|
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
|
||||||
stack.topAnchor.constraint(equalTo: view.topAnchor),
|
stack.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
stack.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -16),
|
stack.bottomAnchor.constraint(equalTo: footer.topAnchor, constant: -12),
|
||||||
|
footer.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
|
||||||
|
footer.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -8),
|
||||||
|
footer.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -8),
|
||||||
|
footer.heightAnchor.constraint(equalToConstant: 36),
|
||||||
credentials.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
credentials.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
scroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
scroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
|
||||||
scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 100),
|
scroll.heightAnchor.constraint(greaterThanOrEqualToConstant: 100),
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ import Testing
|
|||||||
#expect(search.accessibilityLabel() == "English text to translate into Japanese")
|
#expect(search.accessibilityLabel() == "English text to translate into Japanese")
|
||||||
search.stringValue = "Good morning"
|
search.stringValue = "Good morning"
|
||||||
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||||
#expect(descendants(content).contains { ($0 as? NSButton)?.title == "Translate ↵" })
|
|
||||||
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
|
_ = launcher.control(search, textView: NSTextView(), doCommandBy: #selector(NSResponder.cancelOperation(_:)))
|
||||||
#expect(search.stringValue == "translate")
|
#expect(search.stringValue == "translate")
|
||||||
#expect(!descendants(content).contains { ($0 as? NSButton)?.title == "Translate ↵" })
|
|
||||||
#expect(table.numberOfRows == 1)
|
#expect(table.numberOfRows == 1)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user