Starts to convert candidate UI to Swift.

This commit is contained in:
zonble 2022-01-10 19:56:55 +08:00
parent ba6889fa63
commit 5aafe64751
3 changed files with 495 additions and 0 deletions

View File

@ -49,6 +49,8 @@
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; }; D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; };
D47F7DD5278C25A0002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; }; D47F7DD5278C25A0002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; };
D47F7DD6278C3075002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; }; D47F7DD6278C3075002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; };
D47F7DDA278C32CD002F9DD7 /* CandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD9278C32CD002F9DD7 /* CandidateController.swift */; };
D47F7DDC278C39EC002F9DD7 /* HorizontalCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DDB278C39EC002F9DD7 /* HorizontalCandidateController.swift */; };
D48550A325EBE689006A204C /* OpenCC in Frameworks */ = {isa = PBXBuildFile; productRef = D48550A225EBE689006A204C /* OpenCC */; }; D48550A325EBE689006A204C /* OpenCC in Frameworks */ = {isa = PBXBuildFile; productRef = D48550A225EBE689006A204C /* OpenCC */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -181,6 +183,8 @@
D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = "<group>"; }; D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = "<group>"; };
D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserOverrideModel.cpp; sourceTree = "<group>"; }; D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserOverrideModel.cpp; sourceTree = "<group>"; };
D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSourceHelper.swift; sourceTree = "<group>"; }; D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSourceHelper.swift; sourceTree = "<group>"; };
D47F7DD9278C32CD002F9DD7 /* CandidateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandidateController.swift; sourceTree = "<group>"; };
D47F7DDB278C39EC002F9DD7 /* HorizontalCandidateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCandidateController.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -260,6 +264,8 @@
6A0D4ED815FC0DA600ABF4B3 /* CandidateUI */ = { 6A0D4ED815FC0DA600ABF4B3 /* CandidateUI */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
D47F7DD9278C32CD002F9DD7 /* CandidateController.swift */,
D47F7DDB278C39EC002F9DD7 /* HorizontalCandidateController.swift */,
6A0D4ED915FC0DA600ABF4B3 /* VTCandidateController.h */, 6A0D4ED915FC0DA600ABF4B3 /* VTCandidateController.h */,
6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */, 6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */,
6A0D4EDB15FC0DA600ABF4B3 /* VTHorizontalCandidateController.h */, 6A0D4EDB15FC0DA600ABF4B3 /* VTHorizontalCandidateController.h */,
@ -575,11 +581,13 @@
6A0D4EFE15FC0DA600ABF4B3 /* VTCandidateController.m in Sources */, 6A0D4EFE15FC0DA600ABF4B3 /* VTCandidateController.m in Sources */,
6A0D4EFF15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m in Sources */, 6A0D4EFF15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m in Sources */,
D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */, D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */,
D47F7DDC278C39EC002F9DD7 /* HorizontalCandidateController.swift in Sources */,
6A0D4F0015FC0DA600ABF4B3 /* VTHorizontalCandidateView.m in Sources */, 6A0D4F0015FC0DA600ABF4B3 /* VTHorizontalCandidateView.m in Sources */,
6A0D4F0115FC0DA600ABF4B3 /* VTVerticalCandidateController.m in Sources */, 6A0D4F0115FC0DA600ABF4B3 /* VTVerticalCandidateController.m in Sources */,
6A0D4F0215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m in Sources */, 6A0D4F0215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m in Sources */,
6A0D4F0315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m in Sources */, 6A0D4F0315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m in Sources */,
D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */, D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */,
D47F7DDA278C32CD002F9DD7 /* CandidateController.swift in Sources */,
D427A9C125ED28CC005D43E0 /* OpenCCBridge.swift in Sources */, D427A9C125ED28CC005D43E0 /* OpenCCBridge.swift in Sources */,
D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */, D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */,
6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */, 6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */,

View File

@ -0,0 +1,105 @@
import Cocoa
@objc(CandidateControllerDelegate)
public protocol CandidateControllerDelegate: AnyObject {
func candidateCountForController(_ controller: CandidateController) -> UInt
func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String
func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt)
}
@objc(CandidateController)
public class CandidateController: NSWindowController {
@objc public weak var delegate: CandidateControllerDelegate?
@objc public var selectedCandidateIndex: UInt = UInt.max
@objc public var visible: Bool = false {
didSet {
if visible {
window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0)
} else {
window?.perform(#selector(NSWindow.orderOut(_:)), with: self, afterDelay: 0.0)
}
}
}
@objc public var windowTopLeftPoint: NSPoint {
get {
guard let frameRect = window?.frame else {
return NSPoint.zero
}
return NSPoint(x: frameRect.minX, y: frameRect.maxY)
}
set {
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
self.set(windowTopLeftPoint: newValue, bottomOutOfScreenAdjustmentHeight: 0)
}
}
}
@objc public var keyLabels: [String] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
@objc public var keyLabelFont: NSFont = NSFont.systemFont(ofSize: 14)
@objc public var candidateFont: NSFont = NSFont.systemFont(ofSize: 18)
@objc public func reloadData() {
}
@objc public func showNextPage() -> Bool {
return false
}
@objc public func showPreviousPage() -> Bool {
return false
}
@objc public func highlightNextCandidate() -> Bool {
return false
}
@objc public func highlightPreviousCandidate() -> Bool {
return false
}
func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height:CGFloat) {
var adjustedPoint = windowTopLeftPoint
var adjustedHeight = height
var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero
for screen in NSScreen.screens {
let frame = screen.visibleFrame
if windowTopLeftPoint.x >= frame.minX &&
windowTopLeftPoint.x <= frame.maxX &&
windowTopLeftPoint.y >= frame.minY &&
windowTopLeftPoint.y <= frame.maxY {
screenFrame = frame
break
}
}
if adjustedHeight > screenFrame.size.height / 2.0 {
adjustedHeight = 0.0
}
let windowSize = window?.frame.size ?? NSSize.zero
// bottom beneath the screen?
if adjustedPoint.y - windowSize.height < screenFrame.maxY {
adjustedPoint.y = windowTopLeftPoint.y + adjustedHeight + windowSize.height
}
// top over the screen?
if adjustedPoint.y >= screenFrame.maxY {
adjustedPoint.y = screenFrame.maxY - 1.0
}
// right
if adjustedPoint.x + windowSize.width >= screenFrame.maxX {
adjustedPoint.x = NSMaxX(screenFrame) - windowSize.width
}
// left
if adjustedPoint.x < screenFrame.minX {
adjustedPoint.x = screenFrame.minX
}
window?.setFrameTopLeftPoint(adjustedPoint)
}
}

View File

@ -0,0 +1,382 @@
import Cocoa
class HorizontalCandidateView: NSView {
var highlightedIndex: UInt = 0
var action: Selector?
weak var target: AnyObject?
private var keyLabels: [String] = []
private var displayedCandidates: [String] = []
private var keyLabelHeight: CGFloat = 0
private var candidateTextHeight: CGFloat = 0
private var cellPadding: CGFloat = 0
private var keyLabelAttrDict: [NSAttributedString.Key:AnyObject] = [:]
private var candidateAttrDict: [NSAttributedString.Key:AnyObject] = [:]
private var elementWidths: [CGFloat] = []
private var trackingHighlightedIndex: UInt = UInt.max
override var isFlipped: Bool {
true
}
var sizeForView: NSSize {
var result = NSSize.zero
if !elementWidths.isEmpty {
result.width = elementWidths.reduce(0, +)
result.width += CGFloat(elementWidths.count)
result.height = keyLabelHeight + candidateTextHeight + 1.0
}
return result
}
@objc(setKeyLabels:displayedCandidates:)
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
let count = min(labels.count, candidates.count)
keyLabels = Array(labels[0..<count])
displayedCandidates = Array(candidates[0..<count])
var newWidths = [CGFloat]()
let baseSize = NSSize(width: 10240.0, height: 10240.0)
for index in 0..<count {
let labelRect = (keyLabels[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: keyLabelAttrDict)
let candidateRect = (displayedCandidates[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: keyLabelAttrDict)
let cellWidth = max(labelRect.size.width, candidateRect.size.width)
newWidths.append(cellWidth)
}
elementWidths = newWidths
}
@objc(setKeyLabelFont:candidateFont:)
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .center
keyLabelAttrDict = [.font: labelFont,
.paragraphStyle: paraStyle,
.foregroundColor: NSColor.black]
candidateAttrDict = [.font: candidateFont,
.paragraphStyle: paraStyle,
.foregroundColor: NSColor.textColor]
let labelFontSize = labelFont.pointSize
let candidateFontSize = candidateFont.pointSize
let biggestSize = max(labelFontSize, candidateFontSize)
keyLabelHeight = ceil(labelFontSize * 1.20)
candidateTextHeight = ceil(candidateFontSize * 1.20)
cellPadding = ceil(biggestSize / 2.0)
}
override func draw(_ dirtyRect: NSRect) {
let backgroundColor = NSColor.controlBackgroundColor
let darkGray = NSColor(deviceWhite: 0.7, alpha: 1.0)
let lightGray = NSColor(deviceWhite: 0.8, alpha: 1.0)
let bounds = self.bounds
backgroundColor.setFill()
NSBezierPath.fill(bounds)
if #available(macOS 10.14, *) {
NSColor.separatorColor.setStroke()
} else {
NSColor.darkGray.setStroke()
}
NSBezierPath.strokeLine(from: NSPoint(x: bounds.size.width, y: 0.0), to: NSPoint(x: bounds.size.width, y: bounds.size.height))
var accuWidth: CGFloat = 0
for index in 0..<elementWidths.count {
let currentWidth = elementWidths[index]
let labelRect = NSRect(x: accuWidth, y: 0.0, width: currentWidth, height: keyLabelHeight);
let candidateRect = NSRect(x: accuWidth, y: keyLabelHeight + 1.0, width: currentWidth, height: candidateTextHeight);
(index == highlightedIndex ? darkGray : lightGray).setFill()
NSBezierPath.fill(labelRect)
(keyLabels[index] as NSString).draw(in: labelRect, withAttributes: keyLabelAttrDict)
var activeCandidateAttr = candidateAttrDict
if index == highlightedIndex {
NSColor.selectedTextBackgroundColor.setFill()
activeCandidateAttr = candidateAttrDict
activeCandidateAttr[.foregroundColor] = NSColor.selectedTextColor
} else {
backgroundColor.setFill()
}
NSBezierPath.fill(candidateRect)
(displayedCandidates[index] as NSString).draw(in: candidateRect, withAttributes: activeCandidateAttr)
accuWidth += currentWidth + 1.0
}
}
private func findHitIndex(event: NSEvent) -> UInt? {
let location = convert(event.locationInWindow, to: nil)
if !NSPointInRect(location, self.bounds) {
return nil
}
var accuWidth: CGFloat = 0.0
for index in 0..<elementWidths.count {
let currentWidth = elementWidths[index]
if location.x >= accuWidth && location.x <= accuWidth + currentWidth {
return UInt(index)
}
accuWidth += currentWidth + 1.0
}
return nil
}
override func mouseUp(with event: NSEvent) {
trackingHighlightedIndex = highlightedIndex
guard let newIndex = findHitIndex(event: event) else {
return
}
highlightedIndex = newIndex
self.setNeedsDisplay(self.bounds)
}
override func mouseDown(with event: NSEvent) {
guard let newIndex = findHitIndex(event: event) else {
return
}
var triggerAction = false
if newIndex == highlightedIndex {
triggerAction = true
} else {
highlightedIndex = trackingHighlightedIndex;
}
trackingHighlightedIndex = 0
self.setNeedsDisplay(self.bounds)
if triggerAction {
if let target = target as? NSObject, let action = action {
target.perform(action, with: self)
}
}
}
}
public class HorizontalCandidateController : CandidateController {
private var candidateView: HorizontalCandidateView
private var prevPageButton: NSButton
private var nextPageButton: NSButton
private var currentPage: UInt = 0
public init() {
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel]
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
contentRect.origin = NSPoint.zero
candidateView = HorizontalCandidateView(frame: contentRect)
panel.contentView?.addSubview(candidateView)
contentRect.size = NSSize(width: 36.0, height: 20.0)
nextPageButton = NSButton(frame: contentRect)
prevPageButton = NSButton(frame: contentRect)
panel.contentView?.addSubview(nextPageButton)
panel.contentView?.addSubview(prevPageButton)
super.init(window: panel)
candidateView.target = self
candidateView.action = #selector(candidateViewMouseDidClick(_:))
nextPageButton.setButtonType(.momentaryLight)
nextPageButton.bezelStyle = .smallSquare
nextPageButton.title = "»"
nextPageButton.target = self
nextPageButton.action = #selector(pageButtonAction(_:))
prevPageButton.setButtonType(.momentaryLight)
prevPageButton.bezelStyle = .smallSquare
prevPageButton.title = "«"
prevPageButton.target = self
prevPageButton.action = #selector(pageButtonAction(_:))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func reloadData() {
candidateView.highlightedIndex = 0
currentPage = 0
layoutCandidateView()
}
public override func showNextPage() -> Bool {
guard delegate != nil else {
return false
}
if currentPage + 1 >= self.pageCount {
return false
}
currentPage += 1
candidateView.highlightedIndex = 0
layoutCandidateView();
return true;
}
public override func showPreviousPage() -> Bool {
guard delegate != nil else {
return false
}
if currentPage == 0 {
return false
}
currentPage -= 1
candidateView.highlightedIndex = 0
layoutCandidateView();
return true;
}
public override func highlightNextCandidate() -> Bool {
guard let delegate = delegate else {
return false
}
let currentIndex = selectedCandidateIndex
if currentIndex + 1 >= delegate.candidateCountForController(self) {
return false;
}
selectedCandidateIndex = currentIndex + 1
return true;
}
public override func highlightPreviousCandidate() -> Bool {
guard delegate != nil else {
return false
}
let currentIndex = self.selectedCandidateIndex;
if currentIndex == 0 {
return false
}
selectedCandidateIndex = currentIndex - 1
return true
}
@objc public func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt {
guard let delegate = delegate else {
return UInt.max
}
let result = currentPage * UInt(keyLabels.count) + index;
return result < delegate.candidateCountForController(self) ? result : UInt.max;
}
@objc public override var selectedCandidateIndex: UInt {
get {
return currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex
}
set {
guard let delegate = delegate else {
return
}
let keyLabelCount = UInt(keyLabels.count)
if newValue < delegate.candidateCountForController(self) {
currentPage = newValue / keyLabelCount;
candidateView.highlightedIndex = newValue % keyLabelCount;
layoutCandidateView()
}
}
}
}
extension HorizontalCandidateController {
var pageCount: UInt {
guard let delegate = delegate else {
return 0
}
let totalCount = delegate.candidateCountForController(self)
let keyLabelCount = UInt(keyLabels.count)
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0)
}
func layoutCandidateView() {
guard let delegate = delegate else {
return
}
candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont)
var candidates = [String]()
let count = delegate.candidateCountForController(self)
let keyLabelCount = UInt(keyLabels.count)
let begin = currentPage * keyLabelCount
for index in begin..<min(begin + keyLabelCount, count) {
let candidate = delegate.candidateController(self, candidateAtIndex: index)
candidates.append(candidate)
}
candidateView.set(keyLabels: keyLabels, displayedCandidates: candidates)
var newSize = candidateView.sizeForView
var frameRect = candidateView.frame
frameRect.size = newSize
candidateView.frame = frameRect
if self.pageCount > 1 {
var buttonRect = nextPageButton.frame
var spacing = 0.0
if newSize.height < 40.0 {
buttonRect.size.height = floor(newSize.height / 2)
} else {
buttonRect.size.height = 20.0
}
if newSize.height >= 60.0 {
spacing = ceil(newSize.height * 0.1)
}
let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) / 2.0
buttonRect.origin = NSPoint(x: newSize.width + 8.0, y: buttonOriginY)
nextPageButton.frame = buttonRect
buttonRect.origin = NSPoint(x: newSize.width + 8.0, y: buttonOriginY + buttonRect.size.height + spacing)
prevPageButton.frame = buttonRect
newSize.width += 52.0
nextPageButton.isHidden = false
prevPageButton.isHidden = false
} else {
nextPageButton.isHidden = true
prevPageButton.isHidden = true
}
frameRect = window?.frame ?? NSRect.zero
let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height)
frameRect.size = newSize
frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height)
self.window?.setFrame(frameRect, display: false)
self.candidateView.setNeedsDisplay(candidateView.bounds)
}
@objc func pageButtonAction(_ sender: Any) {
guard let sender = sender as? NSButton else {
return
}
if sender == nextPageButton {
_ = showNextPage()
} else if sender == prevPageButton {
_ = showPreviousPage()
}
}
@objc func candidateViewMouseDidClick(_ sender: Any) {
delegate?.candidateController(self, didSelectCandidateAtIndex: self.selectedCandidateIndex)
}
}