diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Core.swift b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift index 1ac90a02..4834e277 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Core.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift @@ -27,9 +27,12 @@ class ctlInputMethod: IMKInputController { static var ctlCandidateCurrent: ctlCandidateProtocol = mgrPrefs.useIMKCandidateWindow ? ctlCandidateIMK.init(.horizontal) : ctlCandidateUniversal.init(.horizontal) - /// 工具提示視窗的副本,每次都重新初始化。 + /// 工具提示視窗的共用副本。 static var tooltipInstance = ctlTooltip() + /// 浮動組字窗的共用副本。 + static var popupCompositionBuffer = ctlPopupCompositionBuffer() + // MARK: - /// 當前這個 ctlInputMethod 副本是否處於英數輸入模式(滯後項)。 diff --git a/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift b/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift index 37f80e8d..12423441 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift @@ -67,7 +67,6 @@ extension ctlInputMethod { if !state.tooltip.isEmpty { show(tooltip: state.tooltip) } - } case .ofMarking: ctlInputMethod.ctlCandidateCurrent.visible = false setInlineDisplayWithCursor() @@ -82,6 +81,14 @@ extension ctlInputMethod { show(candidateWindowWith: state) default: break } + // 浮動組字窗的顯示判定 + if state.hasComposition, mgrPrefs.clientsIMKTextInputIncapable.contains(clientBundleIdentifier) { + ctlInputMethod.popupCompositionBuffer.show( + state: state, at: lineHeightRect(zeroCursor: true).origin + ) + } else { + ctlInputMethod.popupCompositionBuffer.hide() + } } /// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。 diff --git a/Source/Modules/UIModules/CandidateUI/ctlCandidateUniversal.swift b/Source/Modules/UIModules/CandidateUI/ctlCandidateUniversal.swift index 916d4798..caf269b7 100644 --- a/Source/Modules/UIModules/CandidateUI/ctlCandidateUniversal.swift +++ b/Source/Modules/UIModules/CandidateUI/ctlCandidateUniversal.swift @@ -346,7 +346,7 @@ public class ctlCandidateUniversal: ctlCandidate { let panel = NSPanel( contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false ) - panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 2) panel.hasShadow = true panel.isOpaque = false panel.backgroundColor = NSColor.clear diff --git a/Source/Modules/UIModules/PopupCompositionBufferUI/ctlPopupCompositionBuffer.swift b/Source/Modules/UIModules/PopupCompositionBufferUI/ctlPopupCompositionBuffer.swift new file mode 100644 index 00000000..c9ce1870 --- /dev/null +++ b/Source/Modules/UIModules/PopupCompositionBufferUI/ctlPopupCompositionBuffer.swift @@ -0,0 +1,127 @@ +// (c) 2021 and onwards The vChewing Project (MIT-NTL License). +// ==================== +// This code is released under the MIT license (SPDX-License-Identifier: MIT) +// ... with NTL restriction stating that: +// No trademark license is granted to use the trade names, trademarks, service +// marks, or product names of Contributor, except as required to fulfill notice +// requirements defined in MIT License. + +import Cocoa + +public class ctlPopupCompositionBuffer: NSWindowController { + private var messageTextField: NSTextField + private var textShown: NSAttributedString = .init(string: "") { + didSet { + messageTextField.attributedStringValue = textShown + adjustSize() + } + } + + public init() { + let transparentVisualEffect = NSVisualEffectView() + transparentVisualEffect.blendingMode = .behindWindow + transparentVisualEffect.state = .active + let contentRect = NSRect(x: 128.0, y: 128.0, width: 300.0, height: 20.0) + let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel] + let panel = NSPanel( + contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false + ) + panel.contentView = transparentVisualEffect + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) + panel.hasShadow = true + panel.backgroundColor = NSColor.clear + + messageTextField = NSTextField() + messageTextField.isEditable = false + messageTextField.isSelectable = false + messageTextField.isBezeled = false + messageTextField.textColor = NSColor.textColor + messageTextField.drawsBackground = true + messageTextField.backgroundColor = NSColor.clear + messageTextField.font = .systemFont(ofSize: 18) + panel.contentView?.addSubview(messageTextField) + super.init(window: panel) + } + + @available(*, unavailable) + public required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func show(state: IMEStateProtocol, at point: NSPoint) { + if !state.hasComposition { + hide() + return + } + // 在這個視窗內的下畫線繪製方法就得單獨設計了。 + let attrString: NSMutableAttributedString = .init(string: state.data.displayedTextConverted) + attrString.setAttributes( + [ + .backgroundColor: NSColor.alternateSelectedControlColor, + .foregroundColor: NSColor.alternateSelectedControlTextColor, + .markedClauseSegment: 0, + ], + range: NSRange( + location: state.data.u16MarkedRange.lowerBound, + length: state.data.u16MarkedRange.upperBound - state.data.u16MarkedRange.lowerBound + ) + ) + let attrCursor = NSMutableAttributedString(string: "_") + if #available(macOS 10.13, *) { + attrCursor.setAttributes( + [ + .kern: -18, + .baselineOffset: -2, + .markedClauseSegment: 1, + ], + range: NSRange(location: 0, length: attrCursor.string.utf16.count) + ) + } + attrString.insert(attrCursor, at: state.data.u16Cursor) + textShown = attrString + messageTextField.maximumNumberOfLines = 1 + if let editor = messageTextField.currentEditor() { + editor.selectedRange = NSRange(state.data.u16MarkedRange) + } + window?.orderFront(nil) + set(windowOrigin: point) + } + + public func hide() { + window?.orderOut(nil) + } + + private func set(windowOrigin: NSPoint) { + guard let window = window else { return } + let windowSize = window.frame.size + + var adjustedPoint = windowOrigin + var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.seniorTheBeast + for frame in NSScreen.screens.map(\.visibleFrame).filter({ !$0.contains(windowOrigin) }) { + screenFrame = frame + break + } + + adjustedPoint.y = min(max(adjustedPoint.y, screenFrame.minY + windowSize.height), screenFrame.maxY) + adjustedPoint.x = min(max(adjustedPoint.x, screenFrame.minX), screenFrame.maxX - windowSize.width) + + window.setFrameOrigin(adjustedPoint) + } + + private func adjustSize() { + let attrString = messageTextField.attributedStringValue + var rect = attrString.boundingRect( + with: NSSize(width: 1600.0, height: 1600.0), + options: [.usesLineFragmentOrigin, .usesFontLeading] + ) + rect.size.width = max(rect.size.width, 20 * CGFloat(attrString.string.count)) + 2 + rect.size.height = 22 + var bigRect = rect + bigRect.size.width += NSFont.systemFontSize + bigRect.size.height += NSFont.systemFontSize + rect.origin.x += NSFont.systemFontSize / 2 + rect.origin.y += NSFont.systemFontSize / 2 + messageTextField.frame = rect + window?.setFrame(bigRect, display: true) + } +} diff --git a/Source/Modules/UIModules/TooltipUI/ctlTooltip.swift b/Source/Modules/UIModules/TooltipUI/ctlTooltip.swift index 0ef603f5..2f41fc63 100644 --- a/Source/Modules/UIModules/TooltipUI/ctlTooltip.swift +++ b/Source/Modules/UIModules/TooltipUI/ctlTooltip.swift @@ -40,7 +40,7 @@ public class ctlTooltip: NSWindowController { let panel = NSPanel( contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false ) - panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 2) panel.hasShadow = true panel.backgroundColor = NSColor.controlBackgroundColor messageText = NSAttributedTextView() diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index fed689fb..744b2dca 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -39,6 +39,7 @@ 5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */; }; 5B62A34727AE7CD900A19448 /* ctlCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34027AE7CD900A19448 /* ctlCandidate.swift */; }; 5B62A34A27AE7CD900A19448 /* NotifierController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B62A34527AE7CD900A19448 /* NotifierController.swift */; }; + 5B630A3C28CC97020010D076 /* ctlPopupCompositionBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B630A3B28CC97020010D076 /* ctlPopupCompositionBuffer.swift */; }; 5B6C141228A9D4B30098ADF8 /* ctlInputMethod_Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B6C141128A9D4B30098ADF8 /* ctlInputMethod_Common.swift */; }; 5B73FB5E27B2BE1300E9BF49 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 5B73FB6027B2BE1300E9BF49 /* InfoPlist.strings */; }; 5B782EC4280C243C007276DE /* KeyHandler_HandleCandidate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */; }; @@ -253,6 +254,7 @@ 5B62A33C27AE7CC100A19448 /* ctlAboutWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlAboutWindow.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B62A34027AE7CD900A19448 /* ctlCandidate.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlCandidate.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B62A34527AE7CD900A19448 /* NotifierController.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = NotifierController.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; + 5B630A3B28CC97020010D076 /* ctlPopupCompositionBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlPopupCompositionBuffer.swift; sourceTree = ""; }; 5B65B919284D0185007C558B /* README-CHT.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "README-CHT.md"; sourceTree = ""; }; 5B6C141128A9D4B30098ADF8 /* ctlInputMethod_Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ctlInputMethod_Common.swift; sourceTree = ""; }; 5B73FB5427B2BD6900E9BF49 /* PhraseEditor-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "PhraseEditor-Info.plist"; path = "UserPhraseEditor/PhraseEditor-Info.plist"; sourceTree = SOURCE_ROOT; }; @@ -581,6 +583,7 @@ children = ( 5B62A33E27AE7CD900A19448 /* CandidateUI */, 5B62A34427AE7CD900A19448 /* NotifierUI */, + 5B630A3A28CC96D80010D076 /* PopupCompositionBufferUI */, 5BA9FD0927FED9F3002DE248 /* PrefUI */, 5B62A34227AE7CD900A19448 /* TooltipUI */, ); @@ -645,6 +648,14 @@ name = Data; sourceTree = ""; }; + 5B630A3A28CC96D80010D076 /* PopupCompositionBufferUI */ = { + isa = PBXGroup; + children = ( + 5B630A3B28CC97020010D076 /* ctlPopupCompositionBuffer.swift */, + ); + path = PopupCompositionBufferUI; + sourceTree = ""; + }; 5B84579B2871AD2200C93B01 /* HotenkaChineseConverter */ = { isa = PBXGroup; children = ( @@ -1268,6 +1279,7 @@ 5BA9FD1327FEDB6B002DE248 /* suiPrefPaneDictionary.swift in Sources */, 5B2170E8289FACAD00BE7304 /* 5_Vertex.swift in Sources */, 5BBBB77A27AEDC690023B93A /* clsSFX.swift in Sources */, + 5B630A3C28CC97020010D076 /* ctlPopupCompositionBuffer.swift in Sources */, 5BA9FD4727FEF3C9002DE248 /* PreferencesStyleController.swift in Sources */, 5B949BDB2816DDBC00D87B5D /* LMConsolidator.swift in Sources */, 5BFDF011289635C100417BBC /* ctlCandidateIMK.swift in Sources */,