InputHandler // Implement stroke composition support.
This commit is contained in:
parent
23e02b9132
commit
c0ef70fe0d
|
@ -29,7 +29,7 @@ extension vChewingLM {
|
|||
/// 是否已有資料載入。
|
||||
public var isLoaded: Bool { !charDefMap.isEmpty }
|
||||
/// 返回「允許使用的敲字鍵」的
|
||||
public var allowedKeys: [String] { Array(keyNameMap.keys) }
|
||||
public var allowedKeys: [String] { Array(keyNameMap.keys + [" "]).deduplicated }
|
||||
/// 將給定的按鍵字母轉換成要顯示的形態。
|
||||
public func convertKeyToDisplay(char: String) -> String {
|
||||
keyNameMap[char] ?? char
|
||||
|
|
|
@ -57,6 +57,7 @@ public class InputHandler: InputHandlerProtocol {
|
|||
/// 半衰模組的衰減指數
|
||||
let kEpsilon: Double = 0.000_001
|
||||
|
||||
public var calligrapher = "" // 磁帶專用組筆區
|
||||
public var composer: Tekkon.Composer = .init() // 注拼槽
|
||||
public var compositor: Megrez.Compositor // 組字器
|
||||
public var currentUOM: vChewingLM.LMUserOverride
|
||||
|
@ -83,6 +84,7 @@ public class InputHandler: InputHandlerProtocol {
|
|||
public func clear() {
|
||||
composer.clear()
|
||||
compositor.clear()
|
||||
calligrapher.removeAll()
|
||||
}
|
||||
|
||||
// MARK: - Functions dealing with Megrez.
|
||||
|
@ -336,6 +338,8 @@ public class InputHandler: InputHandlerProtocol {
|
|||
|
||||
// MARK: - Extracted methods and functions (Tekkon).
|
||||
|
||||
var isComposerOrCalligrapherEmpty: Bool { prefs.cassetteEnabled ? calligrapher.isEmpty : composer.isEmpty }
|
||||
|
||||
/// 獲取與當前注音排列或拼音輸入種類有關的標點索引鍵,以英數下畫線「_」結尾。
|
||||
var currentKeyboardParser: String { currentKeyboardParserType.name + "_" }
|
||||
var currentKeyboardParserType: KeyboardParser { .init(rawValue: prefs.keyboardParser) ?? .ofStandard }
|
||||
|
@ -363,6 +367,22 @@ public class InputHandler: InputHandlerProtocol {
|
|||
composer.phonabetCombinationCorrectionEnabled = prefs.autoCorrectReadingCombination
|
||||
}
|
||||
|
||||
func clearComposerAndCalligrapher() {
|
||||
_ = prefs.cassetteEnabled ? calligrapher.removeAll() : composer.clear()
|
||||
}
|
||||
|
||||
func letComposerAndCalligrapherDoBackSpace() {
|
||||
_ = prefs.cassetteEnabled ? calligrapher = String(calligrapher.dropLast(1)) : composer.doBackSpace()
|
||||
}
|
||||
|
||||
/// 返回前一個游標位置的可解析的漢字筆畫。
|
||||
/// 返回的內容分別是:「完整讀音」「去掉聲調的讀音」「是否有聲調」。
|
||||
var previousParsableCalligraph: String? {
|
||||
if compositor.cursor == 0 { return nil }
|
||||
let cursorPrevious = max(compositor.cursor - 1, 0)
|
||||
return compositor.keys[cursorPrevious]
|
||||
}
|
||||
|
||||
/// 返回前一個游標位置的可解析的漢字讀音。
|
||||
/// 返回的內容分別是:「完整讀音」「去掉聲調的讀音」「是否有聲調」。
|
||||
var previousParsableReading: (String, String, Bool)? {
|
||||
|
@ -390,6 +410,18 @@ public class InputHandler: InputHandlerProtocol {
|
|||
return theComposer.hasIntonation(withNothingElse: true)
|
||||
}
|
||||
|
||||
var readingForDisplay: String {
|
||||
if !prefs.cassetteEnabled {
|
||||
return composer.getInlineCompositionForDisplay(isHanyuPinyin: prefs.showHanyuPinyinInCompositionBuffer)
|
||||
}
|
||||
if !prefs.showTranslatedStrokesInCompositionBuffer { return calligrapher }
|
||||
var result = calligrapher.charComponents
|
||||
for idx in 0..<result.count {
|
||||
result[idx] = currentLM.currentCassette.convertKeyToDisplay(char: result[idx])
|
||||
}
|
||||
return result.joined()
|
||||
}
|
||||
|
||||
// MARK: - Extracted methods and functions (Megrez).
|
||||
|
||||
/// 生成標點符號索引鍵。
|
||||
|
|
|
@ -168,8 +168,12 @@ extension InputHandler {
|
|||
]
|
||||
let punctuation: String = arrPunctuations.joined()
|
||||
|
||||
let isInputValid: Bool =
|
||||
prefs.cassetteEnabled
|
||||
? currentLM.currentCassette.allowedKeys.contains(input.text) : composer.inputValidityCheck(key: input.charCode)
|
||||
|
||||
var shouldAutoSelectCandidate: Bool =
|
||||
composer.inputValidityCheck(key: input.charCode) || currentLM.hasUnigramsFor(key: customPunctuation)
|
||||
isInputValid || currentLM.hasUnigramsFor(key: customPunctuation)
|
||||
|| currentLM.hasUnigramsFor(key: punctuation)
|
||||
|
||||
if !shouldAutoSelectCandidate, input.isUpperCaseASCIILetterKey {
|
||||
|
|
|
@ -17,9 +17,11 @@ extension InputHandler {
|
|||
/// - input: 輸入訊號。
|
||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||
func handleComposition(input: InputSignalProtocol) -> Bool? {
|
||||
handlePhonabetComposition(input: input)
|
||||
prefs.cassetteEnabled ? handleCassetteComposition(input: input) : handlePhonabetComposition(input: input)
|
||||
}
|
||||
|
||||
// MARK: 注音按鍵輸入處理 (Handle BPMF Keys)
|
||||
|
||||
/// 用來處理 InputHandler.HandleInput() 當中的與注音输入有關的組字行為。
|
||||
/// - Parameters:
|
||||
/// - input: 輸入訊號。
|
||||
|
@ -27,8 +29,6 @@ extension InputHandler {
|
|||
private func handlePhonabetComposition(input: InputSignalProtocol) -> Bool? {
|
||||
guard let delegate = delegate else { return nil }
|
||||
|
||||
// MARK: 注音按鍵輸入處理 (Handle BPMF Keys)
|
||||
|
||||
var keyConsumedByReading = false
|
||||
let skipPhoneticHandling =
|
||||
input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey
|
||||
|
@ -76,7 +76,7 @@ extension InputHandler {
|
|||
|
||||
var composeReading = composer.hasIntonation() && composer.inputValidityCheck(key: input.charCode) // 這裡不需要做排他性判斷。
|
||||
|
||||
// 如果當前的按鍵是 Enter 或 Space 的話,這時就可以取出 _composer 內的注音來做檢查了。
|
||||
// 如果當前的按鍵是 Enter 或 Space 的話,這時就可以取出 composer 內的注音來做檢查了。
|
||||
// 來看看詞庫內到底有沒有對應的讀音索引。這裡用了類似「|=」的判斷處理方式。
|
||||
composeReading = composeReading || (!composer.isEmpty && (input.isSpace || input.isEnter))
|
||||
if composeReading {
|
||||
|
@ -160,3 +160,106 @@ extension InputHandler {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 磁帶模式的組字支援。
|
||||
|
||||
extension InputHandler {
|
||||
/// 用來處理 InputHandler.HandleInput() 當中的與磁帶模組有關的組字行為。
|
||||
/// - Parameters:
|
||||
/// - input: 輸入訊號。
|
||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||
private func handleCassetteComposition(input: InputSignalProtocol) -> Bool? {
|
||||
guard let delegate = delegate else { return nil }
|
||||
|
||||
var keyConsumedByStrokes = false
|
||||
let skipStrokeHandling =
|
||||
input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey
|
||||
|| input.isControlHold || input.isOptionHold || input.isShiftHold || input.isCommandHold
|
||||
|
||||
var isStrokesFull: Bool { calligrapher.count >= currentLM.currentCassette.maxKeyLength }
|
||||
|
||||
if !skipStrokeHandling && currentLM.currentCassette.allowedKeys.contains(input.text) {
|
||||
if isStrokesFull {
|
||||
calligrapher = String(calligrapher.dropLast(1))
|
||||
}
|
||||
calligrapher.append(input.text)
|
||||
keyConsumedByStrokes = true
|
||||
|
||||
if !isStrokesFull {
|
||||
delegate.switchState(generateStateOfInputting())
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var compoundStrokes = isStrokesFull // 這裡不需要做排他性判斷。
|
||||
|
||||
// 如果當前的按鍵是 Enter 或 Space 的話,這時就可以取出 calligrapher 內的筆畫來做檢查了。
|
||||
// 來看看詞庫內到底有沒有對應的讀音索引。這裡用了類似「|=」的判斷處理方式。
|
||||
compoundStrokes = compoundStrokes || (!calligrapher.isEmpty && (input.isSpace || input.isEnter))
|
||||
if compoundStrokes {
|
||||
// 向語言模型詢問是否有對應的記錄。
|
||||
if !currentLM.hasUnigramsFor(key: calligrapher) {
|
||||
delegate.callError("B49C0979_Cassette:語彙庫內無「\(calligrapher)」的匹配記錄。")
|
||||
|
||||
calligrapher.removeAll()
|
||||
// 根據「組字器是否為空」來判定回呼哪一種狀態。
|
||||
switch compositor.isEmpty {
|
||||
case false: delegate.switchState(generateStateOfInputting())
|
||||
case true: delegate.switchState(IMEState.ofAbortion())
|
||||
}
|
||||
return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了。
|
||||
}
|
||||
|
||||
// 將該讀音插入至組字器內的軌格當中。
|
||||
compositor.insertKey(calligrapher)
|
||||
|
||||
// 讓組字器反爬軌格。
|
||||
walk()
|
||||
|
||||
// 一邊吃一邊屙(僅對位列黑名單的 App 用這招限制組字區長度)。
|
||||
let textToCommit = commitOverflownComposition
|
||||
|
||||
// 看看半衰記憶模組是否會對目前的狀態給出自動選字建議。
|
||||
retrieveUOMSuggestions(apply: true)
|
||||
|
||||
// 之後就是更新組字區了。先清空注拼槽的內容。
|
||||
calligrapher.removeAll()
|
||||
|
||||
// 再以回呼組字狀態的方式來執行 setInlineDisplayWithCursor()。
|
||||
var inputting = generateStateOfInputting()
|
||||
inputting.textToCommit = textToCommit
|
||||
delegate.switchState(inputting)
|
||||
|
||||
/// 逐字選字模式的處理,與注音輸入的部分完全雷同。
|
||||
if prefs.useSCPCTypingMode {
|
||||
let candidateState: IMEStateProtocol = generateStateOfCandidates()
|
||||
switch candidateState.candidates.count {
|
||||
case 2...: delegate.switchState(candidateState)
|
||||
case 1:
|
||||
let firstCandidate = candidateState.candidates.first! // 一定會有,所以強制拆包也無妨。
|
||||
let reading: String = firstCandidate.0
|
||||
let text: String = firstCandidate.1
|
||||
delegate.switchState(IMEState.ofCommitting(textToCommit: text))
|
||||
|
||||
if !prefs.associatedPhrasesEnabled {
|
||||
delegate.switchState(IMEState.ofEmpty())
|
||||
} else {
|
||||
let associatedPhrases = generateStateOfAssociates(withPair: .init(key: reading, value: text))
|
||||
delegate.switchState(associatedPhrases.candidates.isEmpty ? IMEState.ofEmpty() : associatedPhrases)
|
||||
}
|
||||
default: break
|
||||
}
|
||||
}
|
||||
// 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 SessionCtl 回報給 IMK。
|
||||
return true
|
||||
}
|
||||
|
||||
/// 是說此時注拼槽並非為空、卻還沒組音。這種情況下只可能是「注拼槽內只有聲調」。
|
||||
if keyConsumedByStrokes {
|
||||
// 以回呼組字狀態的方式來執行 setInlineDisplayWithCursor()。
|
||||
delegate.switchState(generateStateOfInputting())
|
||||
return true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,7 +112,7 @@ extension InputHandler {
|
|||
|
||||
// MARK: 用上下左右鍵呼叫選字窗 (Calling candidate window using Up / Down or PageUp / PageDn.)
|
||||
|
||||
if state.hasComposition, composer.isEmpty, !input.isOptionHold,
|
||||
if state.hasComposition, isComposerOrCalligrapherEmpty, !input.isOptionHold,
|
||||
input.isCursorClockLeft || input.isCursorClockRight || input.isSpace
|
||||
|| input.isPageDown || input.isPageUp || (input.isTab && prefs.specifyShiftTabKeyBehavior)
|
||||
{
|
||||
|
@ -181,7 +181,7 @@ extension InputHandler {
|
|||
if input.isSymbolMenuPhysicalKey, !input.isShiftHold, !input.isControlHold, state.type != .ofDeactivated {
|
||||
if input.isOptionHold {
|
||||
if currentLM.hasUnigramsFor(key: "_punctuation_list") {
|
||||
if composer.isEmpty {
|
||||
if isComposerOrCalligrapherEmpty {
|
||||
compositor.insertKey("_punctuation_list")
|
||||
walk()
|
||||
// 一邊吃一邊屙(僅對位列黑名單的 App 用這招限制組字區長度)。
|
||||
|
@ -268,7 +268,7 @@ extension InputHandler {
|
|||
/// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。
|
||||
/// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。
|
||||
/// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。
|
||||
if state.hasComposition || !composer.isEmpty {
|
||||
if state.hasComposition || !isComposerOrCalligrapherEmpty {
|
||||
delegate.callError("Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)")
|
||||
delegate.callError("A9BFF20E")
|
||||
delegate.switchState(state)
|
||||
|
|
|
@ -23,7 +23,7 @@ extension InputHandler {
|
|||
/// 換成由此處重新生成的原始資料在 IMEStateData 當中生成的 NSAttributeString。
|
||||
var displayTextSegments: [String] = compositor.walkedNodes.values
|
||||
var cursor = convertCursorForDisplay(compositor.cursor)
|
||||
let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: prefs.showHanyuPinyinInCompositionBuffer)
|
||||
let reading: String = readingForDisplay // 先提出來,減輕運算負擔。
|
||||
if !reading.isEmpty {
|
||||
var newDisplayTextSegments = [String]()
|
||||
var temporaryNode = ""
|
||||
|
@ -67,10 +67,11 @@ extension InputHandler {
|
|||
continue
|
||||
}
|
||||
if !theNode.isReadingMismatched {
|
||||
for _ in 0..<strNodeValue.count {
|
||||
guard readingCursorIndex < rawCursor else { continue }
|
||||
composedStringCursorIndex += 1
|
||||
readingCursorIndex += 1
|
||||
strNodeValue.forEach { _ in
|
||||
if readingCursorIndex < rawCursor {
|
||||
composedStringCursorIndex += 1
|
||||
readingCursorIndex += 1
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
@ -227,7 +228,7 @@ extension InputHandler {
|
|||
return false
|
||||
}
|
||||
|
||||
guard composer.isEmpty else {
|
||||
guard isComposerOrCalligrapherEmpty else {
|
||||
// 注音沒敲完的情況下,無視標點輸入。
|
||||
delegate.callError("A9B69908D")
|
||||
delegate.switchState(state)
|
||||
|
@ -243,7 +244,7 @@ extension InputHandler {
|
|||
delegate.switchState(inputting)
|
||||
|
||||
// 從這一行之後開始,就是針對逐字選字模式的單獨處理。
|
||||
guard prefs.useSCPCTypingMode, composer.isEmpty else { return true }
|
||||
guard prefs.useSCPCTypingMode, isComposerOrCalligrapherEmpty else { return true }
|
||||
|
||||
let candidateState = generateStateOfCandidates()
|
||||
switch candidateState.candidates.count {
|
||||
|
@ -339,14 +340,22 @@ extension InputHandler {
|
|||
guard state.type == .ofInputting else { return false }
|
||||
|
||||
// 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。
|
||||
switch prefs.specifyShiftBackSpaceKeyBehavior {
|
||||
shiftBksp: switch prefs.specifyShiftBackSpaceKeyBehavior {
|
||||
case 0:
|
||||
guard input.isShiftHold, composer.isEmpty else { break }
|
||||
guard let prevReading = previousParsableReading else { break }
|
||||
// prevReading 的內容分別是:「完整讀音」「去掉聲調的讀音」「是否有聲調」。
|
||||
compositor.dropKey(direction: .rear)
|
||||
walk() // 這裡必須 Walk 一次、來更新目前被 walk 的內容。
|
||||
prevReading.1.charComponents.forEach { composer.receiveKey(fromPhonabet: $0) }
|
||||
if prefs.cassetteEnabled {
|
||||
guard input.isShiftHold, calligrapher.isEmpty else { break shiftBksp }
|
||||
guard let prevReading = previousParsableCalligraph else { break shiftBksp }
|
||||
compositor.dropKey(direction: .rear)
|
||||
walk() // 這裡必須 Walk 一次、來更新目前被 walk 的內容。
|
||||
calligrapher = prevReading
|
||||
} else {
|
||||
guard input.isShiftHold, isComposerOrCalligrapherEmpty else { break shiftBksp }
|
||||
guard let prevReading = previousParsableReading else { break shiftBksp }
|
||||
// prevReading 的內容分別是:「完整讀音」「去掉聲調的讀音」「是否有聲調」。
|
||||
compositor.dropKey(direction: .rear)
|
||||
walk() // 這裡必須 Walk 一次、來更新目前被 walk 的內容。
|
||||
prevReading.1.charComponents.forEach { composer.receiveKey(fromPhonabet: $0) }
|
||||
}
|
||||
delegate.switchState(generateStateOfInputting())
|
||||
return true
|
||||
case 1:
|
||||
|
@ -360,9 +369,11 @@ extension InputHandler {
|
|||
return true
|
||||
}
|
||||
|
||||
if composer.hasIntonation(withNothingElse: true) {
|
||||
composer.clear()
|
||||
} else if composer.isEmpty {
|
||||
let isConfirm: Bool = prefs.cassetteEnabled ? input.isSpace : composer.hasIntonation(withNothingElse: true)
|
||||
|
||||
if isConfirm {
|
||||
clearComposerAndCalligrapher()
|
||||
} else if isComposerOrCalligrapherEmpty {
|
||||
if compositor.cursor > 0 {
|
||||
compositor.dropKey(direction: .rear)
|
||||
walk()
|
||||
|
@ -372,10 +383,10 @@ extension InputHandler {
|
|||
return true
|
||||
}
|
||||
} else {
|
||||
composer.doBackSpace()
|
||||
letComposerAndCalligrapherDoBackSpace()
|
||||
}
|
||||
|
||||
switch composer.isEmpty && compositor.isEmpty {
|
||||
switch isComposerOrCalligrapherEmpty && compositor.isEmpty {
|
||||
case false: delegate.switchState(generateStateOfInputting())
|
||||
case true: delegate.switchState(IMEState.ofAbortion())
|
||||
}
|
||||
|
@ -398,17 +409,17 @@ extension InputHandler {
|
|||
return true
|
||||
}
|
||||
|
||||
if compositor.cursor == compositor.length, composer.isEmpty {
|
||||
if compositor.cursor == compositor.length, isComposerOrCalligrapherEmpty {
|
||||
delegate.callError("9B69938D")
|
||||
delegate.switchState(state)
|
||||
return true
|
||||
}
|
||||
|
||||
if composer.isEmpty {
|
||||
if isComposerOrCalligrapherEmpty {
|
||||
compositor.dropKey(direction: .front)
|
||||
walk()
|
||||
} else {
|
||||
composer.clear()
|
||||
clearComposerAndCalligrapher()
|
||||
}
|
||||
|
||||
let inputting = generateStateOfInputting()
|
||||
|
@ -428,7 +439,7 @@ extension InputHandler {
|
|||
guard let delegate = delegate else { return false }
|
||||
let state = delegate.state
|
||||
guard state.type == .ofInputting else { return false }
|
||||
if !composer.isEmpty { delegate.callError("9B6F908D") }
|
||||
if !isComposerOrCalligrapherEmpty { delegate.callError("9B6F908D") }
|
||||
delegate.switchState(state)
|
||||
return true
|
||||
}
|
||||
|
@ -442,7 +453,7 @@ extension InputHandler {
|
|||
let state = delegate.state
|
||||
guard state.type == .ofInputting else { return false }
|
||||
|
||||
if !composer.isEmpty {
|
||||
if !isComposerOrCalligrapherEmpty {
|
||||
delegate.callError("ABC44080")
|
||||
delegate.switchState(state)
|
||||
return true
|
||||
|
@ -468,7 +479,7 @@ extension InputHandler {
|
|||
let state = delegate.state
|
||||
guard state.type == .ofInputting else { return false }
|
||||
|
||||
if !composer.isEmpty {
|
||||
if !isComposerOrCalligrapherEmpty {
|
||||
delegate.callError("9B69908D")
|
||||
delegate.switchState(state)
|
||||
return true
|
||||
|
@ -499,9 +510,9 @@ extension InputHandler {
|
|||
/// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。
|
||||
delegate.switchState(IMEState.ofAbortion())
|
||||
} else {
|
||||
if composer.isEmpty { return true }
|
||||
/// 如果注拼槽不是空的話,則清空之。
|
||||
composer.clear()
|
||||
if isComposerOrCalligrapherEmpty { return true }
|
||||
/// 如果注拼槽或組筆區不是空的話,則清空之。
|
||||
clearComposerAndCalligrapher()
|
||||
switch compositor.isEmpty {
|
||||
case false: delegate.switchState(generateStateOfInputting())
|
||||
case true: delegate.switchState(IMEState.ofAbortion())
|
||||
|
@ -521,7 +532,7 @@ extension InputHandler {
|
|||
let state = delegate.state
|
||||
guard state.type == .ofInputting else { return false }
|
||||
|
||||
if !composer.isEmpty {
|
||||
if !isComposerOrCalligrapherEmpty {
|
||||
delegate.callError("B3BA5257")
|
||||
delegate.switchState(state)
|
||||
return true
|
||||
|
@ -584,7 +595,7 @@ extension InputHandler {
|
|||
let state = delegate.state
|
||||
guard state.type == .ofInputting else { return false }
|
||||
|
||||
if !composer.isEmpty {
|
||||
if !isComposerOrCalligrapherEmpty {
|
||||
delegate.callError("6ED95318")
|
||||
delegate.switchState(state)
|
||||
return true
|
||||
|
@ -643,7 +654,7 @@ extension InputHandler {
|
|||
func rotateCandidate(reverseOrder: Bool) -> Bool {
|
||||
guard let delegate = delegate else { return false }
|
||||
let state = delegate.state
|
||||
if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
|
||||
if isComposerOrCalligrapherEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
|
||||
guard state.type == .ofInputting else {
|
||||
guard state.type == .ofEmpty else {
|
||||
delegate.callError("6044F081")
|
||||
|
@ -653,7 +664,7 @@ extension InputHandler {
|
|||
return false
|
||||
}
|
||||
|
||||
guard composer.isEmpty else {
|
||||
guard isComposerOrCalligrapherEmpty else {
|
||||
delegate.callError("A2DAF7BC")
|
||||
return true
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue