From 3b20d05ea90d411919febdd222ecb6f02b80daf5 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Wed, 20 Sep 2023 22:38:43 +0800 Subject: [PATCH] TooltipUI // Maintenance. --- .../MainAssembly/SessionCtl_Core.swift | 7 +- .../SessionCtl_HandleDisplay.swift | 4 +- .../Sources/TooltipUI/TooltipUIProtocol.swift | 21 ++ .../TooltipUI/TooltipUI_EarlyCocoa.swift | 218 ++++++++++++++++++ ...ltipUI.swift => TooltipUI_LateCocoa.swift} | 8 +- 5 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUIProtocol.swift create mode 100644 Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI_EarlyCocoa.swift rename Packages/vChewing_TooltipUI/Sources/TooltipUI/{TooltipUI.swift => TooltipUI_LateCocoa.swift} (94%) diff --git a/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_Core.swift b/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_Core.swift index f8b55b0f..8bb9764a 100644 --- a/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_Core.swift +++ b/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_Core.swift @@ -37,7 +37,7 @@ public class SessionCtl: IMKInputController { public var candidateUI: CtlCandidateProtocol? /// 工具提示視窗的副本。 - public var tooltipInstance = TooltipUI() + public var tooltipInstance: any TooltipUIProtocol = SessionCtl.makeTooltipUI() /// 浮動組字窗的副本。 public var popupCompositionBuffer = PopupCompositionBuffer() @@ -268,6 +268,11 @@ public extension SessionCtl { // 有相關需求者,請在切換掉輸入法或者切換至新的客體應用之前敲一下 Shift+Delete。 switchState(IMEState.ofCommitting(textToCommit: textToCommit)) } + + static func makeTooltipUI() -> TooltipUIProtocol { + if #unavailable(macOS 10.14) { return TooltipUI_EarlyCocoa() } + return TooltipUI_LateCocoa() + } } // MARK: - IMKStateSetting 協定規定的方法 diff --git a/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_HandleDisplay.swift b/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_HandleDisplay.swift index a67b33a3..910a256d 100644 --- a/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_HandleDisplay.swift +++ b/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_HandleDisplay.swift @@ -59,14 +59,14 @@ public extension SessionCtl { x: lineHeightRect.origin.x + lineHeightRect.size.width + 5, y: lineHeightRect.origin.y ) } - let tooltipContentDirection: NSAttributedTextView.writingDirection = { + let tooltipContentDirection: NSUserInterfaceLayoutOrientation = { if PrefMgr.shared.alwaysShowTooltipTextsHorizontally { return .horizontal } return isVerticalTyping ? .vertical : .horizontal }() // 強制重新初期化,因為 NSAttributedTextView 有顯示滯後性。 do { tooltipInstance.hide() - tooltipInstance = .init() + tooltipInstance = Self.makeTooltipUI() tooltipInstance.setColor(state: state.data.tooltipColorState) } // 再設定其文字顯示內容並顯示。 diff --git a/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUIProtocol.swift b/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUIProtocol.swift new file mode 100644 index 00000000..1a8cc88a --- /dev/null +++ b/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUIProtocol.swift @@ -0,0 +1,21 @@ +// (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 AppKit +import Shared + +public protocol TooltipUIProtocol { + func show( + tooltip: String, at point: NSPoint, + bottomOutOfScreenAdjustmentHeight heightDelta: Double, + direction: NSUserInterfaceLayoutOrientation, duration: Double + ) + + func hide() + func setColor(state: TooltipColorState) +} diff --git a/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI_EarlyCocoa.swift b/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI_EarlyCocoa.swift new file mode 100644 index 00000000..6e94c6a7 --- /dev/null +++ b/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI_EarlyCocoa.swift @@ -0,0 +1,218 @@ +// (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 AppKit +import Shared + +public class TooltipUI_EarlyCocoa: NSWindowController, TooltipUIProtocol { + private var messageText: NSTextField + private var tooltip: String = "" { + didSet { + var text = tooltip + if direction == .vertical { + text = text.replacingOccurrences(of: "˙", with: "・") + text = text.replacingOccurrences(of: "\u{A0}", with: " ") + text = text.replacingOccurrences(of: "+", with: "") + text = text.replacingOccurrences(of: "Shift", with: "⇧") + text = text.replacingOccurrences(of: "Control", with: "⌃") + text = text.replacingOccurrences(of: "Enter", with: "⏎") + text = text.replacingOccurrences(of: "Command", with: "⌘") + text = text.replacingOccurrences(of: "Delete", with: "⌦") + text = text.replacingOccurrences(of: "BackSpace", with: "⌫") + text = text.replacingOccurrences(of: "Space", with: "␣") + text = text.replacingOccurrences(of: "SHIFT", with: "⇧") + text = text.replacingOccurrences(of: "CONTROL", with: "⌃") + text = text.replacingOccurrences(of: "ENTER", with: "⏎") + text = text.replacingOccurrences(of: "COMMAND", with: "⌘") + text = text.replacingOccurrences(of: "DELETE", with: "⌦") + text = text.replacingOccurrences(of: "BACKSPACE", with: "⌫") + text = text.replacingOccurrences(of: "SPACE", with: "␣") + } + + let attrString: NSMutableAttributedString = .init(string: text) + let verticalAttributes: [NSAttributedString.Key: Any] = [ + .kern: 0, + .verticalGlyphForm: true, + .paragraphStyle: { + let newStyle = NSMutableParagraphStyle() + let fontSize = messageText.font?.pointSize ?? NSFont.systemFontSize + newStyle.lineSpacing = 1 + newStyle.maximumLineHeight = fontSize + newStyle.minimumLineHeight = fontSize + return newStyle + }(), + ] + + attrString.setAttributes( + [.kern: 0], range: NSRange(location: 0, length: attrString.length) + ) + + if direction == .vertical { + attrString.setAttributes( + verticalAttributes, range: NSRange(location: 0, length: attrString.length) + ) + } + + messageText.attributedStringValue = attrString + adjustSize() + } + } + + private static var currentWindow: NSWindow? { + willSet { + currentWindow?.orderOut(nil) + } + } + + public var direction: NSUserInterfaceLayoutOrientation = .horizontal { + didSet { + if #unavailable(macOS 10.14) { + direction = .horizontal + } + } + } + + 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)) + 2) + panel.hasShadow = true + panel.backgroundColor = NSColor.controlBackgroundColor + panel.isMovable = false + messageText = NSTextField() + messageText.isEditable = false + messageText.isSelectable = false + messageText.isBezeled = false + messageText.textColor = NSColor.textColor + messageText.drawsBackground = true + messageText.backgroundColor = NSColor.clear + messageText.textColor = NSColor.textColor + messageText.font = NSFont.systemFont(ofSize: NSFont.systemFontSize) + messageText.needsDisplay = true + panel.contentView?.addSubview(messageText) + Self.currentWindow = panel + super.init(window: panel) + } + + @available(*, unavailable) + public required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func show( + tooltip: String, at point: NSPoint, + bottomOutOfScreenAdjustmentHeight heightDelta: Double, + direction: NSUserInterfaceLayoutOrientation = .horizontal, duration: Double + ) { + self.direction = direction + self.tooltip = tooltip + window?.setIsVisible(false) + window?.orderFront(nil) + set(windowTopLeftPoint: point, bottomOutOfScreenAdjustmentHeight: heightDelta, useGCD: false) + window?.setIsVisible(true) + if duration > 0 { + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + self.window?.orderOut(nil) + } + } + } + + public func setColor(state: TooltipColorState) { + var backgroundColor = NSColor( + red: 0.12, green: 0.12, blue: 0.12, alpha: 1.00 + ) + var textColor = NSColor( + red: 0.9, green: 0.9, blue: 0.9, alpha: 1.00 + ) + switch state { + case .normal: + backgroundColor = NSColor( + red: 0.12, green: 0.12, blue: 0.12, alpha: 1.00 + ) + textColor = NSColor( + red: 0.9, green: 0.9, blue: 0.9, alpha: 1.00 + ) + case .information: + backgroundColor = NSColor( + red: 0.09, green: 0.14, blue: 0.16, alpha: 1.00 + ) + textColor = NSColor( + red: 0.91, green: 0.92, blue: 0.95, alpha: 1.00 + ) + case .redAlert: + backgroundColor = NSColor( + red: 0.55, green: 0.00, blue: 0.00, alpha: 1.00 + ) + textColor = NSColor.white + case .warning: + backgroundColor = NSColor.purple + textColor = NSColor.white + case .succeeded: + backgroundColor = NSColor( + red: 0.21, green: 0.15, blue: 0.02, alpha: 1.00 + ) + textColor = NSColor.white + case .denialOverflow: + backgroundColor = NSColor( + red: 0.13, green: 0.08, blue: 0.00, alpha: 1.00 + ) + textColor = NSColor( + red: 1.00, green: 0.60, blue: 0.00, alpha: 1.00 + ) + case .denialInsufficiency: + backgroundColor = NSColor( + red: 0.15, green: 0.15, blue: 0.15, alpha: 1.00 + ) + textColor = NSColor( + red: 0.88, green: 0.88, blue: 0.88, alpha: 1.00 + ) + case .prompt: + backgroundColor = NSColor( + red: 0.09, green: 0.16, blue: 0.14, alpha: 1.00 + ) + textColor = NSColor( + red: 0.91, green: 0.95, blue: 0.92, alpha: 1.00 + ) + } + window?.backgroundColor = backgroundColor + messageText.backgroundColor = backgroundColor + messageText.textColor = textColor + } + + public func resetColor() { + setColor(state: .normal) + } + + public func hide() { + setColor(state: .normal) + window?.orderOut(nil) + } + + private func adjustSize() { + messageText.sizeToFit() + var rect = messageText.frame + if direction == .vertical { + rect = .init(x: rect.minX, y: rect.minY, width: rect.height * 1.5, 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 direction == .vertical { + messageText.boundsRotation = 90 + } else { + messageText.boundsRotation = 0 + } + messageText.frame = rect + window?.setFrame(bigRect, display: true) + } +} diff --git a/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI.swift b/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI_LateCocoa.swift similarity index 94% rename from Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI.swift rename to Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI_LateCocoa.swift index 748536ca..4aef3645 100644 --- a/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI.swift +++ b/Packages/vChewing_TooltipUI/Sources/TooltipUI/TooltipUI_LateCocoa.swift @@ -11,7 +11,7 @@ import CocoaExtension import NSAttributedTextView import Shared -public class TooltipUI: NSWindowController { +public class TooltipUI_LateCocoa: NSWindowController, TooltipUIProtocol { private var messageText: NSAttributedTooltipTextView private var tooltip: String = "" { didSet { @@ -63,11 +63,11 @@ public class TooltipUI: NSWindowController { } public func show( - tooltip: String = "", at point: NSPoint, + tooltip: String, at point: NSPoint, bottomOutOfScreenAdjustmentHeight heightDelta: Double, - direction: NSAttributedTooltipTextView.writingDirection = .horizontal, duration: Double = 0 + direction: NSUserInterfaceLayoutOrientation = .horizontal, duration: Double ) { - self.direction = direction + self.direction = direction == .horizontal ? .horizontal : .vertical self.tooltip = tooltip window?.setIsVisible(false) window?.orderFront(nil)