checkpoint before checking out main
This commit is contained in:
@@ -68,6 +68,11 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
/// One random ID per keyboard presentation. The repository splits this ID
|
||||
/// into independent UTC-day fragments when a presentation crosses midnight.
|
||||
private var keyboardUsageSessionID = UUID()
|
||||
/// UIKit can reuse this controller across host apps. Keep document-scoped
|
||||
/// candidate state separate from the heavy typing-engine lifetime.
|
||||
private var isKeyboardPresentationActive = false
|
||||
private var isTypingSurfaceActive = false
|
||||
private var typingDocumentPresentationID: UInt64?
|
||||
|
||||
private var memoryTelemetryContext: String {
|
||||
let language = typingSessionStorage?.language.rawValue ?? "-"
|
||||
@@ -208,6 +213,9 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
isKeyboardPresentationActive = false
|
||||
typingDocumentPresentationID = nil
|
||||
typingSessionStorage?.endDocumentPresentation()
|
||||
AnalyticsExtensionService.shared.keyboardWillDisappear()
|
||||
assistantFieldActionRefreshTask?.cancel()
|
||||
assistantFieldActionRefreshTask = nil
|
||||
@@ -243,8 +251,8 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
if !preserve {
|
||||
prepareSurfaceForNextPresentation()
|
||||
}
|
||||
if state.surface == .typing {
|
||||
typingSession.leaveTypingMode()
|
||||
if state.surface != .typing {
|
||||
deactivateTypingSurface()
|
||||
}
|
||||
if preserve {
|
||||
return
|
||||
@@ -255,6 +263,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
isKeyboardPresentationActive = true
|
||||
OSGDiag.log(
|
||||
"KVC.viewWillAppear begin surface=\(state.surface.rawValue) "
|
||||
+ "fullAccess=\(hasFullAccess) \(OSGDiag.memoryTag())",
|
||||
@@ -263,6 +272,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
recordMemory("KVC.viewWillAppear.begin")
|
||||
setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
|
||||
configureDictationBehavior()
|
||||
state.hasFullAccess = hasFullAccess
|
||||
KeyboardSetupBridge.markExtensionAppearance(hasFullAccess: hasFullAccess)
|
||||
// Refresh only Flow/config state; edit targets come from verified OSG insertions.
|
||||
flowCoordinator.refreshSessionState()
|
||||
@@ -278,8 +288,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
recordMemory("KVC.viewWillAppear.afterHaptics")
|
||||
if state.surface == .typing {
|
||||
OSGDiag.log("KVC.viewWillAppear enterTypingMode", category: "boot")
|
||||
typingSession.enterTypingMode()
|
||||
refreshEnglishSupplementaryLexicon()
|
||||
activateTypingPresentationIfNeeded()
|
||||
recordMemory("KVC.viewWillAppear.afterTypingEnter")
|
||||
}
|
||||
clipboardCapture.keyboardDidAppear()
|
||||
@@ -322,6 +331,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
heightPhase = .presented
|
||||
lockPresentedKeyboardHeight()
|
||||
refreshReturnKeyRole()
|
||||
scheduleTypingDocumentSynchronization()
|
||||
// Presentation math is locked before arming the PiP handoff.
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self, self.heightPhase == .presented else { return }
|
||||
@@ -357,7 +367,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
public override func textDidChange(_ textInput: UITextInput?) {
|
||||
super.textDidChange(textInput)
|
||||
refreshReturnKeyRole()
|
||||
typingSession.synchronizeEnglishDocumentContext()
|
||||
synchronizeTypingDocumentContext()
|
||||
textInserter?.refreshEditingAvailability()
|
||||
lastInputEditCoordinator?.refreshContext()
|
||||
}
|
||||
@@ -365,7 +375,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
public override func selectionDidChange(_ textInput: UITextInput?) {
|
||||
super.selectionDidChange(textInput)
|
||||
refreshReturnKeyRole()
|
||||
typingSession.synchronizeEnglishDocumentContext(caretMoved: true)
|
||||
synchronizeTypingDocumentContext(caretMoved: true)
|
||||
textInserter?.refreshEditingAvailability()
|
||||
lastInputEditCoordinator?.refreshContext()
|
||||
}
|
||||
@@ -395,10 +405,9 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
)
|
||||
flowCoordinator.cancelPipelineUnlessAwaitingResult()
|
||||
if state.surface == .typing {
|
||||
typingSession.leaveTypingMode()
|
||||
applySurface(.voice)
|
||||
} else {
|
||||
typingSession.leaveTypingMode()
|
||||
deactivateTypingSurface()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,9 +432,15 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
self?.insertTextIntoDocument(text, source: source)
|
||||
},
|
||||
deleteBackward: { [weak self] in self?.deleteBackwardFromDocument() },
|
||||
contextBeforeInput: { [weak self] in self?.textDocumentProxy.documentContextBeforeInput },
|
||||
contextBeforeInput: { [weak self] in
|
||||
guard let self, self.textDocumentProxy.isSecureTextEntry == false else { return nil }
|
||||
return self.textDocumentProxy.documentContextBeforeInput
|
||||
},
|
||||
fieldContextProvider: { [weak self] in self?.captureFieldContext() },
|
||||
selectedText: { [weak self] in self?.textDocumentProxy.selectedText },
|
||||
selectedText: { [weak self] in
|
||||
guard let self, self.textDocumentProxy.isSecureTextEntry == false else { return nil }
|
||||
return self.textDocumentProxy.selectedText
|
||||
},
|
||||
scheduleAutoClearError: { [weak self] in self?.scheduleAutoClearError() },
|
||||
editHintScheduler: editHintScheduler
|
||||
)
|
||||
@@ -439,6 +454,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
onConfigChanged: { [weak self] in
|
||||
// Only an already-live typing session can be showing the setup
|
||||
// error; never force-create one just to retry.
|
||||
self?.typingSessionStorage?.reloadPersonalDictionaryTerms()
|
||||
self?.typingSessionStorage?.retryPrepareAfterResourceDeployment()
|
||||
}
|
||||
)
|
||||
@@ -502,7 +518,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
clipboardCapture = ClipboardCaptureCoordinator(state: state)
|
||||
clipboardCapture.configure(
|
||||
isSecure: { [weak self] in
|
||||
self?.textDocumentProxy.isSecureTextEntry ?? false
|
||||
self?.textDocumentProxy.isSecureTextEntry ?? true
|
||||
},
|
||||
hasFullAccess: { [weak self] in
|
||||
self?.hasFullAccess ?? false
|
||||
@@ -666,8 +682,14 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
|
||||
state.$surface
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] _ in
|
||||
self?.refreshKeyboardHeight()
|
||||
.sink { [weak self] surface in
|
||||
guard let self else { return }
|
||||
if surface == .typing {
|
||||
self.activateTypingPresentationIfNeeded()
|
||||
} else {
|
||||
self.deactivateTypingSurface()
|
||||
}
|
||||
self.refreshKeyboardHeight()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
@@ -685,6 +707,9 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
return
|
||||
}
|
||||
guard state.surface != surface else {
|
||||
if surface == .typing {
|
||||
activateTypingPresentationIfNeeded()
|
||||
}
|
||||
refreshKeyboardHeight()
|
||||
return
|
||||
}
|
||||
@@ -695,14 +720,68 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
state.surface = surface
|
||||
recordMemory("KVC.applySurface", details: "requested=\(surface.rawValue)")
|
||||
if surface == .typing {
|
||||
typingSession.enterTypingMode()
|
||||
refreshEnglishSupplementaryLexicon()
|
||||
activateTypingPresentationIfNeeded()
|
||||
} else {
|
||||
typingSession.leaveTypingMode()
|
||||
deactivateTypingSurface()
|
||||
}
|
||||
refreshKeyboardHeight()
|
||||
}
|
||||
|
||||
private func activateTypingPresentationIfNeeded() {
|
||||
guard isKeyboardPresentationActive,
|
||||
state.surface == .typing,
|
||||
typingDocumentPresentationID == nil else {
|
||||
return
|
||||
}
|
||||
if !isTypingSurfaceActive {
|
||||
typingSession.enterTypingMode()
|
||||
isTypingSurfaceActive = true
|
||||
}
|
||||
typingDocumentPresentationID = typingSession.beginDocumentPresentation()
|
||||
refreshEnglishSupplementaryLexicon()
|
||||
if heightPhase == .presented {
|
||||
refreshReturnKeyRole()
|
||||
scheduleTypingDocumentSynchronization()
|
||||
}
|
||||
}
|
||||
|
||||
private func deactivateTypingSurface() {
|
||||
guard isTypingSurfaceActive || typingDocumentPresentationID != nil else { return }
|
||||
typingDocumentPresentationID = nil
|
||||
typingSessionStorage?.leaveTypingMode()
|
||||
isTypingSurfaceActive = false
|
||||
}
|
||||
|
||||
private func scheduleTypingDocumentSynchronization() {
|
||||
guard let presentationID = typingDocumentPresentationID else { return }
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self,
|
||||
self.isKeyboardPresentationActive,
|
||||
self.state.surface == .typing,
|
||||
self.typingDocumentPresentationID == presentationID else {
|
||||
return
|
||||
}
|
||||
self.synchronizeTypingDocumentContext(
|
||||
caretMoved: true,
|
||||
presentationID: presentationID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func synchronizeTypingDocumentContext(
|
||||
caretMoved: Bool = false,
|
||||
presentationID: UInt64? = nil
|
||||
) {
|
||||
guard let activeID = presentationID ?? typingDocumentPresentationID,
|
||||
typingDocumentPresentationID == activeID else {
|
||||
return
|
||||
}
|
||||
typingSessionStorage?.synchronizeEnglishDocumentContext(
|
||||
caretMoved: caretMoved,
|
||||
presentationID: activeID
|
||||
)
|
||||
}
|
||||
|
||||
private func applyPreferredSurfaceOnOpen() {
|
||||
let preference = TypingInputConfiguration.preferredOpenPreference()
|
||||
let resolved = KeyboardOpenSurfacePolicy.resolve(
|
||||
@@ -716,10 +795,10 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
+ "locksTyping=\(state.locksTypingSurface ? 1 : 0)",
|
||||
category: "boot"
|
||||
)
|
||||
applySurface(resolved)
|
||||
if resolved == .typing, let language = preference.typingLanguage {
|
||||
_ = typingSession.setLanguage(language)
|
||||
}
|
||||
applySurface(resolved)
|
||||
}
|
||||
|
||||
/// When not remembering, snap to the static open preference while hidden
|
||||
@@ -842,24 +921,31 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
assistantFieldActionRefreshTask?.cancel()
|
||||
assistantFieldActionRefreshTask = nil
|
||||
refreshAssistantFieldAction()
|
||||
let isSecure = textDocumentProxy.isSecureTextEntry ?? false
|
||||
// An unavailable trait is not proof that the new host field is safe.
|
||||
// Fail closed until UIKit supplies an explicit non-secure value.
|
||||
let isSecure = textDocumentProxy.isSecureTextEntry ?? true
|
||||
state.setSecureTextEntry(isSecure)
|
||||
clipboardCapture?.secureEntryDidChange(isSecure: isSecure)
|
||||
// Secure fields must not run English autocomplete / autocorrect / learning.
|
||||
typingSession.suggestionsEnabled = !isSecure
|
||||
typingSession.syncAutocapitalization()
|
||||
typingSessionStorage?.suggestionsEnabled = !isSecure
|
||||
if !isSecure {
|
||||
typingSessionStorage?.syncAutocapitalization()
|
||||
}
|
||||
}
|
||||
|
||||
private func installTypingContextProviders() {
|
||||
typingSession.precedingTextProvider = { [weak self] in
|
||||
self?.textDocumentProxy.documentContextBeforeInput
|
||||
guard let self, self.textDocumentProxy.isSecureTextEntry == false else { return nil }
|
||||
return self.textDocumentProxy.documentContextBeforeInput
|
||||
}
|
||||
typingSession.followingTextProvider = { [weak self] in
|
||||
self?.textDocumentProxy.documentContextAfterInput
|
||||
guard let self, self.textDocumentProxy.isSecureTextEntry == false else { return nil }
|
||||
return self.textDocumentProxy.documentContextAfterInput
|
||||
}
|
||||
typingSession.autocapitalizationModeProvider = { [weak self] in
|
||||
Self.typingAutocapitalizationMode(
|
||||
for: self?.textDocumentProxy.autocapitalizationType ?? .sentences
|
||||
guard let self, self.textDocumentProxy.isSecureTextEntry == false else { return .none }
|
||||
return Self.typingAutocapitalizationMode(
|
||||
for: self.textDocumentProxy.autocapitalizationType ?? .sentences
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1037,6 +1123,7 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
// MARK: - App context
|
||||
|
||||
private func detectAndStoreAppContext() {
|
||||
guard textDocumentProxy.isSecureTextEntry == false else { return }
|
||||
let preceding = textDocumentProxy.documentContextBeforeInput
|
||||
let store = AppGroupStore()
|
||||
let detector = AppContextDetector()
|
||||
@@ -1048,9 +1135,9 @@ public final class KeyboardViewController: UIInputViewController {
|
||||
}
|
||||
|
||||
private func captureFieldContext() -> FlowFieldContext {
|
||||
let isSecure = textDocumentProxy.isSecureTextEntry ?? false
|
||||
let preceding = textDocumentProxy.documentContextBeforeInput
|
||||
let following = textDocumentProxy.documentContextAfterInput
|
||||
let isSecure = textDocumentProxy.isSecureTextEntry ?? true
|
||||
let preceding = isSecure ? nil : textDocumentProxy.documentContextBeforeInput
|
||||
let following = isSecure ? nil : textDocumentProxy.documentContextAfterInput
|
||||
let isAvailable = preceding != nil || following != nil
|
||||
let isEmpty = isAvailable && (preceding ?? "").isEmpty && (following ?? "").isEmpty
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ public struct AppGroupPersistor {
|
||||
guard AppGroup.isAvailable else { return }
|
||||
let store = AppGroupStore()
|
||||
state.engineMode = store.engineMode
|
||||
state.localeId = store.localeId
|
||||
let shouldProtectTranslation = KeyboardTranslationConfigProtection.shouldProtect(
|
||||
until: protectTranslationUntil
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ struct CandidateExpandGridView: UIViewRepresentable {
|
||||
var candidates: [TypingCandidate]
|
||||
var textColor: UIColor
|
||||
var dividerColor: UIColor
|
||||
var onSelect: (Int) -> Void
|
||||
var onSelect: (TypingCandidate) -> Void
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(onSelect: onSelect)
|
||||
@@ -73,10 +73,10 @@ struct CandidateExpandGridView: UIViewRepresentable {
|
||||
var candidates: [TypingCandidate] = []
|
||||
var textColor: UIColor = .label
|
||||
var dividerColor: UIColor = .separator
|
||||
var onSelect: (Int) -> Void
|
||||
var onSelect: (TypingCandidate) -> Void
|
||||
weak var collectionView: UICollectionView?
|
||||
|
||||
init(onSelect: @escaping (Int) -> Void) {
|
||||
init(onSelect: @escaping (TypingCandidate) -> Void) {
|
||||
self.onSelect = onSelect
|
||||
}
|
||||
|
||||
@@ -105,7 +105,8 @@ struct CandidateExpandGridView: UIViewRepresentable {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
collectionView.deselectItem(at: indexPath, animated: false)
|
||||
onSelect(indexPath.item)
|
||||
guard candidates.indices.contains(indexPath.item) else { return }
|
||||
onSelect(candidates[indexPath.item])
|
||||
}
|
||||
|
||||
private func isLastRow(index: Int) -> Bool {
|
||||
|
||||
@@ -82,11 +82,6 @@ struct KeyboardSurfaceRoot: View {
|
||||
// lingers (a slow Universal Clipboard read, a system alert).
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
||||
.animation(.easeInOut(duration: 0.15), value: state.surface)
|
||||
.onChange(of: state.surface) { _, newSurface in
|
||||
if newSurface != .typing {
|
||||
typing.leaveTypingMode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func wrappedInsert(_ text: String) {
|
||||
@@ -124,7 +119,9 @@ struct KeyboardSurfaceRoot: View {
|
||||
onClear: state.clearClipboardHistory,
|
||||
onInsert: { state.insertClipboardText($0) },
|
||||
onDelete: { state.deleteClipboardHistoryEntry($0) },
|
||||
pastePermissionHint: nil
|
||||
pastePermissionHint: state.needsClipboardFullAccessHint
|
||||
? ExtL10n.string("keyboard.clipboard.panel.fullAccessHint")
|
||||
: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ struct TypingRootView: View {
|
||||
typingKeySurface
|
||||
.padding(.top, TypingLayoutMetrics.verticalKeySpacing)
|
||||
.opacity(expanded ? 0 : 1)
|
||||
.allowsHitTesting(!expanded)
|
||||
.accessibilityHidden(expanded)
|
||||
.allowsHitTesting(!expanded && canAcceptTypingKeys)
|
||||
.accessibilityHidden(expanded || !canAcceptTypingKeys)
|
||||
|
||||
if expanded || candidatePanelMounted {
|
||||
expandedCandidatePanel
|
||||
@@ -141,6 +141,14 @@ struct TypingRootView: View {
|
||||
!typing.composition.preedit.isEmpty || !typing.composition.candidates.isEmpty
|
||||
}
|
||||
|
||||
private var isChineseEnginePreparing: Bool {
|
||||
typing.language == .chinese && typing.isPreparingEngine
|
||||
}
|
||||
|
||||
private var canAcceptTypingKeys: Bool {
|
||||
typing.language == .english || typing.engineReady
|
||||
}
|
||||
|
||||
private var idleTopBar: some View {
|
||||
ZStack {
|
||||
KeyboardTopControls(
|
||||
@@ -157,6 +165,10 @@ struct TypingRootView: View {
|
||||
|
||||
if let err = typing.lastError {
|
||||
typingErrorLabel(err)
|
||||
} else if isChineseEnginePreparing {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
.accessibilityLabel(ExtL10n.text("keyboard.typing.preparingA11y"))
|
||||
}
|
||||
|
||||
// iOS-style editing cluster (undo / redo / copy / cut) — iPad only,
|
||||
@@ -238,16 +250,17 @@ struct TypingRootView: View {
|
||||
.fill(palette.dividerStrong)
|
||||
.frame(width: 1, height: 18)
|
||||
}
|
||||
englishQuickTypeSlot(candidate, index: index)
|
||||
englishQuickTypeSlot(candidate)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.leading, KeyboardTopBarMetrics.nestedHorizontalInset)
|
||||
}
|
||||
|
||||
private func englishQuickTypeSlot(_ candidate: TypingCandidate, index: Int) -> some View {
|
||||
private func englishQuickTypeSlot(_ candidate: TypingCandidate) -> some View {
|
||||
let label = candidate.isQuoted ? "\"\(candidate.text)\"" : candidate.text
|
||||
let weight: Font.Weight = candidate.role == .correction ? .semibold : .regular
|
||||
let candidateRevision = typing.candidateRevision
|
||||
return Text(label)
|
||||
.font(.system(size: 17, weight: weight))
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
@@ -256,20 +269,32 @@ struct TypingRootView: View {
|
||||
.frame(maxWidth: .infinity, minHeight: 40)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
apply(typing.selectCandidate(at: index))
|
||||
apply(
|
||||
typing.selectCandidate(
|
||||
id: candidate.id,
|
||||
candidateRevision: candidateRevision
|
||||
)
|
||||
)
|
||||
}
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.accessibilityLabel(candidate.text)
|
||||
}
|
||||
|
||||
private var chineseCandidateStrip: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
let candidateRevision = typing.candidateRevision
|
||||
return ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: Spacing.xs) {
|
||||
if typing.composition.candidates.isEmpty {
|
||||
selectedCandidateLabel(text: typing.composition.preedit)
|
||||
} else if typing.isCandidatePanelExpanded {
|
||||
candidateChip(text: typing.composition.candidates[0].text) {
|
||||
apply(typing.selectCandidate(at: 0))
|
||||
} else if typing.isCandidatePanelExpanded,
|
||||
let candidate = typing.composition.candidates.first {
|
||||
candidateChip(text: candidate.text) {
|
||||
apply(
|
||||
typing.selectCandidate(
|
||||
id: candidate.id,
|
||||
candidateRevision: candidateRevision
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ForEach(
|
||||
@@ -282,7 +307,12 @@ struct TypingRootView: View {
|
||||
) { index, candidate in
|
||||
if index == 0 {
|
||||
candidateChip(text: candidate.text) {
|
||||
apply(typing.selectCandidate(at: index))
|
||||
apply(
|
||||
typing.selectCandidate(
|
||||
id: candidate.id,
|
||||
candidateRevision: candidateRevision
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(candidate.text)
|
||||
@@ -292,7 +322,12 @@ struct TypingRootView: View {
|
||||
.frame(height: 40)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
apply(typing.selectCandidate(at: index))
|
||||
apply(
|
||||
typing.selectCandidate(
|
||||
id: candidate.id,
|
||||
candidateRevision: candidateRevision
|
||||
)
|
||||
)
|
||||
}
|
||||
.accessibilityAddTraits(.isButton)
|
||||
.accessibilityLabel(candidate.text)
|
||||
@@ -393,15 +428,21 @@ struct TypingRootView: View {
|
||||
|
||||
/// UIKit-recycled labels — no SwiftUI Button per candidate.
|
||||
private var expandedCandidatePanel: some View {
|
||||
CandidateExpandGridView(
|
||||
let candidateRevision = typing.candidateRevision
|
||||
return CandidateExpandGridView(
|
||||
candidates: typing.composition.candidates,
|
||||
textColor: UIColor(keyTextColor),
|
||||
dividerColor: UIColor(palette.dividerStrong),
|
||||
onSelect: { index in
|
||||
onSelect: { candidate in
|
||||
var transaction = Transaction()
|
||||
transaction.disablesAnimations = true
|
||||
withTransaction(transaction) {
|
||||
apply(typing.selectCandidate(at: index))
|
||||
apply(
|
||||
typing.selectCandidate(
|
||||
id: candidate.id,
|
||||
candidateRevision: candidateRevision
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -646,6 +687,7 @@ struct TypingRootView: View {
|
||||
}
|
||||
|
||||
private func commitTypingKey(_ key: TypingKeyHitTarget) {
|
||||
guard canAcceptTypingKeys else { return }
|
||||
switch key.id {
|
||||
case TypingKeyLayoutBuilder.BottomKeyID.pageSwitch.rawValue:
|
||||
typing.setPage(typing.page == .letters ? .numbers : .letters)
|
||||
|
||||
@@ -581,10 +581,12 @@ struct AIKeyboardView: View {
|
||||
if let expectedSkillID = oobeExpectedSkillID {
|
||||
return AIClipboardSkillCatalog.catalog.filter { $0.id == expectedSkillID }
|
||||
}
|
||||
guard let newest = clipboardHistory.newestEntry,
|
||||
let snapshot = semanticRanking.snapshot,
|
||||
guard let newest = clipboardHistory.newestEntry else { return [] }
|
||||
guard let snapshot = semanticRanking.snapshot,
|
||||
snapshot.entryID == newest.id else {
|
||||
return []
|
||||
return state.clipboardSkillCatalog.filter {
|
||||
$0.id == AIClipboardSkillCatalog.replyID
|
||||
}
|
||||
}
|
||||
return ClipboardSkillSemanticRanker.recommended(
|
||||
skills: state.clipboardSkillCatalog,
|
||||
|
||||
@@ -152,11 +152,10 @@
|
||||
"keyboard.redoA11y" = "Redo";
|
||||
"keyboard.copyA11y" = "Copy";
|
||||
"keyboard.cutA11y" = "Cut";
|
||||
"keyboard.cursorDrag.hint" = "Hold and drag to move the cursor";
|
||||
"keyboard.cursorDrag.centerHint" = "Drag to move the cursor";
|
||||
"keyboard.nextKeyboardA11y" = "Next keyboard";
|
||||
"keyboard.nextKeyboardA11yHint" = "Tap to switch to the next keyboard. Touch and hold to see all keyboards.";
|
||||
"keyboard.typing.setupA11yHint" = "Open OSGKeyboard to finish input method setup.";
|
||||
"keyboard.typing.preparingA11y" = "Preparing Chinese input";
|
||||
|
||||
/* Flow session (keyboard) */
|
||||
"keyboard.flow.sessionInactive" = "Voice session off";
|
||||
@@ -211,6 +210,7 @@
|
||||
"keyboard.clipboard.guide.cta" = "Open Settings";
|
||||
"keyboard.clipboard.panel.title" = "Clipboard";
|
||||
"keyboard.clipboard.panel.empty" = "No history yet";
|
||||
"keyboard.clipboard.panel.fullAccessHint" = "New copies are paused because Allow Full Access is off. Existing history remains available.";
|
||||
"keyboard.clipboard.panel.delete" = "Delete";
|
||||
"keyboard.clipboard.panel.close" = "Close clipboard";
|
||||
"keyboard.clipboard.panel.closeHint" = "Return to the keyboard.";
|
||||
@@ -319,9 +319,9 @@
|
||||
"keyboard.ai.error.requestTimeout" = "AI response timed out. Try again";
|
||||
"keyboard.ai.error.requestFailed" = "AI response failed. Try again";
|
||||
"keyboard.ai.error.clipboardUnavailable" = "This clipboard suggestion expired. Copy the text again";
|
||||
"keyboard.ai.skill.reply" = "Same-language reply";
|
||||
"keyboard.ai.skill.reply" = "Reply";
|
||||
"keyboard.ai.skill.playfulReply" = "Playful";
|
||||
"keyboard.ai.skill.replyInSourceLanguage" = "Same language";
|
||||
"keyboard.ai.skill.replyInSourceLanguage" = "Reply";
|
||||
"keyboard.ai.skill.summarize" = "Key points";
|
||||
"keyboard.ai.skill.openLink" = "Open";
|
||||
"keyboard.ai.skill.summarizeWebPage" = "Web summary";
|
||||
|
||||
@@ -152,11 +152,10 @@
|
||||
"keyboard.redoA11y" = "重做";
|
||||
"keyboard.copyA11y" = "拷贝";
|
||||
"keyboard.cutA11y" = "剪切";
|
||||
"keyboard.cursorDrag.hint" = "按住并拖动以移动光标";
|
||||
"keyboard.cursorDrag.centerHint" = "拖动移动光标";
|
||||
"keyboard.nextKeyboardA11y" = "切换键盘";
|
||||
"keyboard.nextKeyboardA11yHint" = "轻点切换到下一个键盘;长按查看全部键盘。";
|
||||
"keyboard.typing.setupA11yHint" = "打开 OSGKeyboard 完成输入法初始化。";
|
||||
"keyboard.typing.preparingA11y" = "正在准备中文输入";
|
||||
|
||||
/* Flow session (keyboard) */
|
||||
"keyboard.flow.sessionInactive" = "语音会话未启动";
|
||||
@@ -211,6 +210,7 @@
|
||||
"keyboard.clipboard.guide.cta" = "去设置开启";
|
||||
"keyboard.clipboard.panel.title" = "剪贴板";
|
||||
"keyboard.clipboard.panel.empty" = "暂无历史记录";
|
||||
"keyboard.clipboard.panel.fullAccessHint" = "「允许完全访问」已关闭,新复制内容将暂停采集,已有历史仍可使用。";
|
||||
"keyboard.clipboard.panel.delete" = "删除";
|
||||
"keyboard.clipboard.panel.close" = "关闭剪贴板";
|
||||
"keyboard.clipboard.panel.closeHint" = "返回键盘输入界面。";
|
||||
@@ -319,9 +319,9 @@
|
||||
"keyboard.ai.error.requestTimeout" = "AI 回答超时,请重试";
|
||||
"keyboard.ai.error.requestFailed" = "AI 回答失败,请重试";
|
||||
"keyboard.ai.error.clipboardUnavailable" = "剪贴板建议已过期,请重新复制文本";
|
||||
"keyboard.ai.skill.reply" = "原语言回复";
|
||||
"keyboard.ai.skill.reply" = "回复";
|
||||
"keyboard.ai.skill.playfulReply" = "俏皮回复";
|
||||
"keyboard.ai.skill.replyInSourceLanguage" = "原语言回复";
|
||||
"keyboard.ai.skill.replyInSourceLanguage" = "回复";
|
||||
"keyboard.ai.skill.summarize" = "总结要点";
|
||||
"keyboard.ai.skill.openLink" = "打开";
|
||||
"keyboard.ai.skill.summarizeWebPage" = "总结网页";
|
||||
|
||||
Reference in New Issue
Block a user