vChewing-macOS/Packages/vChewing_PopupCompositionBu.../Sources/PopupCompositionBuffer/PopupCompositionBuffer.swift

194 lines
6.5 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (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
import Shared
public class PopupCompositionBuffer: NSWindowController {
public var isTypingDirectionVertical = false {
didSet {
if #unavailable(macOS 10.14) {
isTypingDirectionVertical = false
}
}
}
private var messageTextField: NSTextField
private var textShown: NSAttributedString = .init(string: "") {
didSet {
messageTextField.attributedStringValue = textShown
adjustSize()
}
}
public init() {
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.level = NSWindow.Level(Int(max(CGShieldingWindowLevel(), kCGPopUpMenuWindowLevel)) + 1)
panel.hasShadow = true
panel.backgroundColor = NSColor.controlBackgroundColor
panel.styleMask = .fullSizeContentView
panel.isMovable = false
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)
panel.contentView?.wantsLayer = true
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.displayedTextConverted)
let attrPCBHeader: NSMutableAttributedString = .init(string: " ")
let verticalAttributes: [NSAttributedString.Key: Any] = [
.verticalGlyphForm: true,
.paragraphStyle: {
let newStyle = NSMutableParagraphStyle()
if #available(macOS 10.13, *) {
let fontSize = messageTextField.font?.pointSize ?? 18
newStyle.lineSpacing = fontSize / -3
newStyle.maximumLineHeight = fontSize
newStyle.minimumLineHeight = fontSize
}
return newStyle
}(),
]
if isTypingDirectionVertical {
attrPCBHeader.setAttributes(
verticalAttributes, range: NSRange(location: 0, length: attrPCBHeader.length)
)
attrString.setAttributes(
verticalAttributes, range: NSRange(location: 0, length: attrString.length)
)
}
let markerAttributes: [NSAttributedString.Key: Any] = {
var result: [NSAttributedString.Key: Any] = [
.backgroundColor: NSApplication.isDarkMode ? NSColor.systemRed : NSColor.systemYellow,
.markedClauseSegment: 0,
]
if isTypingDirectionVertical {
result[.paragraphStyle] = verticalAttributes[.paragraphStyle]
result[.verticalGlyphForm] = true
}
return result
}()
//
attrString.setAttributes(
markerAttributes,
range: NSRange(
location: state.u16MarkedRange.lowerBound,
length: state.u16MarkedRange.upperBound - state.u16MarkedRange.lowerBound
)
)
let cursorAttributes: [NSAttributedString.Key: Any] = {
var result: [NSAttributedString.Key: Any] = [
.kern: -18,
.foregroundColor: NSColor.textColor,
]
if isTypingDirectionVertical {
result[.paragraphStyle] = verticalAttributes[.paragraphStyle]
result[.verticalGlyphForm] = true
result[.baselineOffset] = 3
} else {
result[.baselineOffset] = -2
}
if #unavailable(macOS 10.13) {
result[.kern] = 0
result[.baselineOffset] = 0
}
return result
}()
let attrCursor: NSAttributedString =
isTypingDirectionVertical
? NSMutableAttributedString(string: "", attributes: cursorAttributes)
: NSMutableAttributedString(string: "_", attributes: cursorAttributes)
attrString.insert(attrCursor, at: state.u16Cursor)
attrString.insert(attrPCBHeader, at: 0)
textShown = attrString
messageTextField.maximumNumberOfLines = 1
if let editor = messageTextField.currentEditor() {
editor.selectedRange = NSRange(state.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)
if isTypingDirectionVertical {
window.setFrameTopLeftPoint(adjustedPoint)
} else {
window.setFrameOrigin(adjustedPoint)
}
}
private func adjustSize() {
let attrString = messageTextField.attributedStringValue
var rect = NSRect(origin: .zero, size: attrString.boundingDimension)
rect.size.height *= 1.2
rect.size.height = max(22, rect.size.height)
rect.size.width = max(rect.size.width, 20 * Double(attrString.string.count)) + 2
if isTypingDirectionVertical {
rect = .init(x: rect.minX, y: rect.minY, width: rect.height, height: rect.width)
}
var bigRect = rect
bigRect.size.width += NSFont.systemFontSize
bigRect.size.height += NSFont.systemFontSize
rect.origin.x += ceil(NSFont.systemFontSize / 2)
rect.origin.y += ceil(NSFont.systemFontSize / 2)
if isTypingDirectionVertical {
messageTextField.boundsRotation = 90
} else {
messageTextField.boundsRotation = 0
}
messageTextField.frame = rect
window?.setFrame(bigRect, display: true)
}
}