Reset // Beautifying Candidate Window.
This commit is contained in:
parent
2e0965aa5d
commit
02e8a63e2a
|
@ -1,3 +1,10 @@
|
||||||
|
/*
|
||||||
|
* HorizontalCandidateController.swift
|
||||||
|
*
|
||||||
|
* Copyright 2021-2022 vChewing Project (3-Clause BSD License).
|
||||||
|
* Derived from 2011-2022 OpenVanilla Project (MIT License).
|
||||||
|
* Some rights reserved. See "LICENSE.TXT" for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
|
@ -8,31 +15,17 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
|
|
||||||
private var keyLabels: [String] = []
|
private var keyLabels: [String] = []
|
||||||
private var displayedCandidates: [String] = []
|
private var displayedCandidates: [String] = []
|
||||||
|
private var dispCandidatesWithLabels: [String] = []
|
||||||
private var keyLabelHeight: CGFloat = 0
|
private var keyLabelHeight: CGFloat = 0
|
||||||
|
private var keyLabelWidth: CGFloat = 0
|
||||||
private var candidateTextHeight: CGFloat = 0
|
private var candidateTextHeight: CGFloat = 0
|
||||||
private var cellPadding: CGFloat = 0
|
private var cellPadding: CGFloat = 0
|
||||||
private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:]
|
private var keyLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:]
|
||||||
private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:]
|
private var candidateAttrDict: [NSAttributedString.Key: AnyObject] = [:]
|
||||||
|
private var candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:]
|
||||||
private var elementWidths: [CGFloat] = []
|
private var elementWidths: [CGFloat] = []
|
||||||
private var trackingHighlightedIndex: UInt = UInt.max
|
private var trackingHighlightedIndex: UInt = UInt.max
|
||||||
|
|
||||||
private let tooltipPadding: CGFloat = 2.0
|
|
||||||
private var tooltipSize: NSSize = NSSize.zero
|
|
||||||
|
|
||||||
override var toolTip: String? {
|
|
||||||
didSet {
|
|
||||||
if let toolTip = toolTip, !toolTip.isEmpty {
|
|
||||||
let baseSize = NSSize(width: 10240.0, height: 10240.0)
|
|
||||||
var tooltipRect = (toolTip as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: keyLabelAttrDict)
|
|
||||||
tooltipRect.size.height += tooltipPadding * 2
|
|
||||||
tooltipRect.size.width += tooltipPadding * 2
|
|
||||||
self.tooltipSize = tooltipRect.size
|
|
||||||
} else {
|
|
||||||
self.tooltipSize = NSSize.zero
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var isFlipped: Bool {
|
override var isFlipped: Bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
@ -43,59 +36,63 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
if !elementWidths.isEmpty {
|
if !elementWidths.isEmpty {
|
||||||
result.width = elementWidths.reduce(0, +)
|
result.width = elementWidths.reduce(0, +)
|
||||||
result.width += CGFloat(elementWidths.count)
|
result.width += CGFloat(elementWidths.count)
|
||||||
result.height = keyLabelHeight + candidateTextHeight + 1.0
|
result.height = candidateTextHeight + cellPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
result.height += tooltipSize.height
|
|
||||||
result.width = max(tooltipSize.width, result.width)
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc(setKeyLabels:displayedCandidates:)
|
||||||
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
|
func set(keyLabels labels: [String], displayedCandidates candidates: [String]) {
|
||||||
let count = min(labels.count, candidates.count)
|
let count = min(labels.count, candidates.count)
|
||||||
keyLabels = Array(labels[0..<count])
|
keyLabels = Array(labels[0..<count])
|
||||||
displayedCandidates = Array(candidates[0..<count])
|
displayedCandidates = Array(candidates[0..<count])
|
||||||
|
dispCandidatesWithLabels = zip(keyLabels,displayedCandidates).map() {$0 + $1}
|
||||||
|
|
||||||
var newWidths = [CGFloat]()
|
var newWidths = [CGFloat]()
|
||||||
let baseSize = NSSize(width: 10240.0, height: 10240.0)
|
let baseSize = NSSize(width: 10240.0, height: 10240.0)
|
||||||
for index in 0..<count {
|
for index in 0..<count {
|
||||||
let labelRect = (keyLabels[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: keyLabelAttrDict)
|
let rctCandidate = (dispCandidatesWithLabels[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: candidateWithLabelAttrDict)
|
||||||
let candidateRect = (displayedCandidates[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: candidateAttrDict)
|
let cellWidth = rctCandidate.size.width + cellPadding
|
||||||
let cellWidth = max(candidateTextHeight,
|
|
||||||
max(labelRect.size.width, candidateRect.size.width)) + cellPadding;
|
|
||||||
newWidths.append(cellWidth)
|
newWidths.append(cellWidth)
|
||||||
}
|
}
|
||||||
elementWidths = newWidths
|
elementWidths = newWidths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc(setKeyLabelFont:candidateFont:)
|
||||||
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
|
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
|
||||||
let paraStyle = NSMutableParagraphStyle()
|
let paraStyle = NSMutableParagraphStyle()
|
||||||
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
||||||
paraStyle.alignment = .center
|
paraStyle.alignment = .center
|
||||||
|
|
||||||
|
candidateWithLabelAttrDict = [.font: candidateFont,
|
||||||
|
.paragraphStyle: paraStyle,
|
||||||
|
.foregroundColor: NSColor.labelColor] // We still need this dummy section to make sure the space occupations of the candidates are correct.
|
||||||
|
|
||||||
keyLabelAttrDict = [.font: labelFont,
|
keyLabelAttrDict = [.font: labelFont,
|
||||||
.paragraphStyle: paraStyle,
|
.paragraphStyle: paraStyle,
|
||||||
.foregroundColor: NSColor.black]
|
.foregroundColor: NSColor.secondaryLabelColor] // Candidate phrase text color
|
||||||
candidateAttrDict = [.font: candidateFont,
|
candidateAttrDict = [.font: candidateFont,
|
||||||
.paragraphStyle: paraStyle,
|
.paragraphStyle: paraStyle,
|
||||||
.foregroundColor: NSColor.textColor]
|
.foregroundColor: NSColor.labelColor] // Candidate index text color
|
||||||
|
|
||||||
let labelFontSize = labelFont.pointSize
|
let labelFontSize = labelFont.pointSize
|
||||||
let candidateFontSize = candidateFont.pointSize
|
let candidateFontSize = candidateFont.pointSize
|
||||||
let biggestSize = max(labelFontSize, candidateFontSize)
|
let biggestSize = max(labelFontSize, candidateFontSize)
|
||||||
|
keyLabelWidth = ceil(labelFontSize)
|
||||||
keyLabelHeight = ceil(labelFontSize * 1.20)
|
keyLabelHeight = ceil(labelFontSize * 1.20)
|
||||||
candidateTextHeight = ceil(candidateFontSize * 1.20)
|
candidateTextHeight = ceil(candidateFontSize * 1.20)
|
||||||
cellPadding = ceil(biggestSize / 2.0)
|
cellPadding = ceil(biggestSize / 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func draw(_ dirtyRect: NSRect) {
|
override func draw(_ dirtyRect: NSRect) {
|
||||||
let backgroundColor = NSColor.controlBackgroundColor
|
|
||||||
let darkGray = NSColor(deviceWhite: 0.7, alpha: 1.0)
|
// Give a standalone layer to the candidate list panel
|
||||||
let lightGray = NSColor(deviceWhite: 0.8, alpha: 1.0)
|
self.wantsLayer = true
|
||||||
|
self.layer?.borderColor = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.30).cgColor
|
||||||
|
self.layer?.borderWidth = 1.0
|
||||||
|
self.layer?.cornerRadius = 6.0
|
||||||
|
|
||||||
let bounds = self.bounds
|
let bounds = self.bounds
|
||||||
backgroundColor.setFill()
|
NSColor.controlBackgroundColor.setFill() // Candidate list panel base background
|
||||||
NSBezierPath.fill(bounds)
|
NSBezierPath.fill(bounds)
|
||||||
|
|
||||||
if #available(macOS 10.14, *) {
|
if #available(macOS 10.14, *) {
|
||||||
|
@ -104,36 +101,27 @@ fileprivate class HorizontalCandidateView: NSView {
|
||||||
NSColor.darkGray.setStroke()
|
NSColor.darkGray.setStroke()
|
||||||
}
|
}
|
||||||
|
|
||||||
if let toolTip = toolTip {
|
NSBezierPath.strokeLine(from: NSPoint(x: bounds.size.width, y: 0.0), to: NSPoint(x: bounds.size.width, y: bounds.size.height))
|
||||||
lightGray.setFill()
|
|
||||||
NSBezierPath.fill(NSMakeRect(0, 0, bounds.width, tooltipSize.height))
|
|
||||||
let tooltipFrame = NSRect(x: 0, y: 0, width: tooltipSize.width, height: tooltipSize.height)
|
|
||||||
(toolTip as NSString).draw(in: tooltipFrame, withAttributes: keyLabelAttrDict)
|
|
||||||
NSBezierPath.strokeLine(from: NSPoint(x: 0, y: tooltipSize.height - 2), to: NSPoint(x: bounds.width, y: tooltipSize.height - 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
NSBezierPath.strokeLine(from: NSPoint(x: bounds.width, y: 0), to: NSPoint(x: bounds.width, y: bounds.height))
|
|
||||||
|
|
||||||
var accuWidth: CGFloat = 0
|
var accuWidth: CGFloat = 0
|
||||||
for index in 0..<elementWidths.count {
|
for index in 0..<elementWidths.count {
|
||||||
let currentWidth = elementWidths[index]
|
let currentWidth = elementWidths[index]
|
||||||
let labelRect = NSRect(x: accuWidth, y: tooltipSize.height, width: currentWidth, height: keyLabelHeight)
|
let rctCandidateArea = NSRect(x: accuWidth, y: 0.0, width: currentWidth + 1.0, height: candidateTextHeight + cellPadding)
|
||||||
let candidateRect = NSRect(x: accuWidth, y: tooltipSize.height + keyLabelHeight + 1.0, width: currentWidth, height: candidateTextHeight)
|
let rctLabel = NSRect(x: accuWidth + cellPadding / 2 - 1, y: cellPadding / 2 , width: keyLabelWidth, height: candidateTextHeight)
|
||||||
(index == highlightedIndex ? darkGray : lightGray).setFill()
|
let rctCandidatePhrase = NSRect(x: accuWidth + keyLabelWidth - 1, y: cellPadding / 2 , width: currentWidth - keyLabelWidth, height: candidateTextHeight)
|
||||||
NSBezierPath.fill(labelRect)
|
|
||||||
(keyLabels[index] as NSString).draw(in: labelRect, withAttributes: keyLabelAttrDict)
|
|
||||||
|
|
||||||
|
var activeCandidateIndexAttr = keyLabelAttrDict
|
||||||
var activeCandidateAttr = candidateAttrDict
|
var activeCandidateAttr = candidateAttrDict
|
||||||
if index == highlightedIndex {
|
if index == highlightedIndex {
|
||||||
NSColor.selectedTextBackgroundColor.setFill()
|
NSColor.alternateSelectedControlColor.setFill() // The background color of the highlightened candidate
|
||||||
activeCandidateAttr = candidateAttrDict
|
activeCandidateIndexAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.84) // The index text color of the highlightened candidate
|
||||||
activeCandidateAttr[.foregroundColor] = NSColor.selectedTextColor
|
activeCandidateAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor // The phrase text color of the highlightened candidate
|
||||||
} else {
|
} else {
|
||||||
backgroundColor.setFill()
|
NSColor.controlBackgroundColor.setFill()
|
||||||
}
|
}
|
||||||
|
NSBezierPath.fill(rctCandidateArea)
|
||||||
NSBezierPath.fill(candidateRect)
|
(keyLabels[index] as NSString).draw(in: rctLabel, withAttributes: activeCandidateIndexAttr)
|
||||||
(displayedCandidates[index] as NSString).draw(in: candidateRect, withAttributes: activeCandidateAttr)
|
(displayedCandidates[index] as NSString).draw(in: rctCandidatePhrase, withAttributes: activeCandidateAttr)
|
||||||
accuWidth += currentWidth + 1.0
|
accuWidth += currentWidth + 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,22 +187,32 @@ public class HorizontalCandidateController: CandidateController {
|
||||||
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
|
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
|
||||||
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
|
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
|
||||||
panel.hasShadow = true
|
panel.hasShadow = true
|
||||||
|
panel.isOpaque = false
|
||||||
|
panel.backgroundColor = NSColor.clear // Transparentify everything outside of the candidate list panel
|
||||||
|
|
||||||
contentRect.origin = NSPoint.zero
|
contentRect.origin = NSPoint.zero
|
||||||
candidateView = HorizontalCandidateView(frame: contentRect)
|
candidateView = HorizontalCandidateView(frame: contentRect)
|
||||||
panel.contentView?.addSubview(candidateView)
|
panel.contentView?.addSubview(candidateView)
|
||||||
|
|
||||||
contentRect.size = NSSize(width: 36.0, height: 20.0)
|
contentRect.size = NSSize(width: 20.0, height: 15.0) // Reduce the button width
|
||||||
nextPageButton = NSButton(frame: contentRect)
|
nextPageButton = NSButton(frame: contentRect)
|
||||||
nextPageButton.setButtonType(.momentaryLight)
|
nextPageButton.setButtonType(.momentaryLight)
|
||||||
nextPageButton.bezelStyle = .smallSquare
|
nextPageButton.bezelStyle = .shadowlessSquare
|
||||||
nextPageButton.title = "»"
|
nextPageButton.wantsLayer = true
|
||||||
|
nextPageButton.layer?.masksToBounds = true
|
||||||
|
nextPageButton.layer?.borderColor = NSColor.clear.cgColor // Attempt to remove the system default layer border color - step 1
|
||||||
|
nextPageButton.layer?.borderWidth = 0.0 // Attempt to remove the system default layer border color - step 2
|
||||||
|
nextPageButton.layer?.backgroundColor = NSColor.black.cgColor // Button Background Color. Otherwise the button will be half-transparent in macOS Monterey Dark Mode.
|
||||||
|
nextPageButton.attributedTitle = NSMutableAttributedString(string: "⬇︎") // Next Page Arrow
|
||||||
prevPageButton = NSButton(frame: contentRect)
|
prevPageButton = NSButton(frame: contentRect)
|
||||||
prevPageButton.setButtonType(.momentaryLight)
|
prevPageButton.setButtonType(.momentaryLight)
|
||||||
prevPageButton.bezelStyle = .smallSquare
|
prevPageButton.bezelStyle = .shadowlessSquare
|
||||||
prevPageButton.title = "«"
|
prevPageButton.wantsLayer = true
|
||||||
|
prevPageButton.layer?.masksToBounds = true
|
||||||
|
prevPageButton.layer?.borderColor = NSColor.clear.cgColor // Attempt to remove the system default layer border color - step 1
|
||||||
|
prevPageButton.layer?.borderWidth = 0.0 // Attempt to remove the system default layer border color - step 2
|
||||||
|
prevPageButton.layer?.backgroundColor = NSColor.black.cgColor // Button Background Color. Otherwise the button will be half-transparent in macOS Monterey Dark Mode.
|
||||||
|
prevPageButton.attributedTitle = NSMutableAttributedString(string: "⬆︎") // Previous Page Arrow
|
||||||
panel.contentView?.addSubview(nextPageButton)
|
panel.contentView?.addSubview(nextPageButton)
|
||||||
panel.contentView?.addSubview(prevPageButton)
|
panel.contentView?.addSubview(prevPageButton)
|
||||||
|
|
||||||
|
@ -351,7 +349,6 @@ extension HorizontalCandidateController {
|
||||||
candidates.append(candidate)
|
candidates.append(candidate)
|
||||||
}
|
}
|
||||||
candidateView.set(keyLabels: keyLabels.map { $0.displayedText}, displayedCandidates: candidates)
|
candidateView.set(keyLabels: keyLabels.map { $0.displayedText}, displayedCandidates: candidates)
|
||||||
candidateView.toolTip = tooltip
|
|
||||||
var newSize = candidateView.sizeForView
|
var newSize = candidateView.sizeForView
|
||||||
var frameRect = candidateView.frame
|
var frameRect = candidateView.frame
|
||||||
frameRect.size = newSize
|
frameRect.size = newSize
|
||||||
|
@ -359,17 +356,9 @@ extension HorizontalCandidateController {
|
||||||
|
|
||||||
if pageCount > 1 {
|
if pageCount > 1 {
|
||||||
var buttonRect = nextPageButton.frame
|
var buttonRect = nextPageButton.frame
|
||||||
var spacing: CGFloat = 0.0
|
let spacing:CGFloat = 0.0
|
||||||
|
|
||||||
if newSize.height < 40.0 {
|
|
||||||
buttonRect.size.height = floor(newSize.height / 2)
|
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
|
let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) / 2.0
|
||||||
buttonRect.origin = NSPoint(x: newSize.width + 8.0, y: buttonOriginY)
|
buttonRect.origin = NSPoint(x: newSize.width + 8.0, y: buttonOriginY)
|
||||||
|
|
|
@ -1,86 +1,195 @@
|
||||||
|
/*
|
||||||
|
* VerticalCandidateController.swift
|
||||||
|
*
|
||||||
|
* Copyright 2021-2022 vChewing Project (3-Clause BSD License).
|
||||||
|
* Derived from 2011-2022 OpenVanilla Project (MIT License).
|
||||||
|
* Some rights reserved. See "LICENSE.TXT" for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
fileprivate class VerticalKeyLabelStripView: NSView {
|
fileprivate class VerticalCandidateView: NSView {
|
||||||
var keyLabelFont: NSFont = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
|
var highlightedIndex: UInt = 0
|
||||||
var labelOffsetY: CGFloat = 0
|
var action: Selector?
|
||||||
var keyLabels: [String] = []
|
weak var target: AnyObject?
|
||||||
var highlightedIndex: UInt = UInt.max
|
|
||||||
|
private var keyLabels: [String] = []
|
||||||
|
private var displayedCandidates: [String] = []
|
||||||
|
private var dispCandidatesWithLabels: [String] = []
|
||||||
|
private var keyLabelHeight: CGFloat = 0
|
||||||
|
private var keyLabelWidth: 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 candidateWithLabelAttrDict: [NSAttributedString.Key: AnyObject] = [:]
|
||||||
|
private var windowWidth: CGFloat = 0
|
||||||
|
private var elementWidths: [CGFloat] = []
|
||||||
|
private var elementHeights: [CGFloat] = []
|
||||||
|
private var trackingHighlightedIndex: UInt = UInt.max
|
||||||
|
|
||||||
override var isFlipped: Bool {
|
override var isFlipped: Bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
override func draw(_ dirtyRect: NSRect) {
|
var sizeForView: NSSize {
|
||||||
let bounds = self.bounds
|
var result = NSSize.zero
|
||||||
NSColor.white.setFill()
|
|
||||||
NSBezierPath.fill(bounds)
|
|
||||||
|
|
||||||
let count = UInt(keyLabels.count)
|
if !elementWidths.isEmpty {
|
||||||
if count == 0 {
|
result.width = windowWidth
|
||||||
return
|
result.height = elementHeights.reduce(0, +)
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
let cellHeight: CGFloat = bounds.size.height / CGFloat(count)
|
|
||||||
let black = NSColor.black
|
|
||||||
let darkGray = NSColor(deviceWhite: 0.7, alpha: 1.0)
|
|
||||||
let lightGray = NSColor(deviceWhite: 0.8, alpha: 1.0)
|
|
||||||
|
|
||||||
|
@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])
|
||||||
|
dispCandidatesWithLabels = zip(keyLabels,displayedCandidates).map() {$0 + $1}
|
||||||
|
|
||||||
|
var newWidths = [CGFloat]()
|
||||||
|
var calculatedWindowWidth = CGFloat()
|
||||||
|
var newHeights = [CGFloat]()
|
||||||
|
let baseSize = NSSize(width: 10240.0, height: 10240.0)
|
||||||
|
for index in 0..<count {
|
||||||
|
let rctCandidate = (dispCandidatesWithLabels[index] as NSString).boundingRect(with: baseSize, options: .usesLineFragmentOrigin, attributes: candidateWithLabelAttrDict)
|
||||||
|
let cellWidth = rctCandidate.size.width + cellPadding
|
||||||
|
let cellHeight = rctCandidate.size.height + cellPadding
|
||||||
|
if (calculatedWindowWidth < rctCandidate.size.width) {
|
||||||
|
calculatedWindowWidth = rctCandidate.size.width
|
||||||
|
}
|
||||||
|
newWidths.append(cellWidth)
|
||||||
|
newHeights.append(cellHeight)
|
||||||
|
}
|
||||||
|
elementWidths = newWidths
|
||||||
|
elementHeights = newHeights
|
||||||
|
windowWidth = calculatedWindowWidth + cellPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc(setKeyLabelFont:candidateFont:)
|
||||||
|
func set(keyLabelFont labelFont: NSFont, candidateFont: NSFont) {
|
||||||
let paraStyle = NSMutableParagraphStyle()
|
let paraStyle = NSMutableParagraphStyle()
|
||||||
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
||||||
paraStyle.alignment = .center
|
paraStyle.alignment = .left
|
||||||
|
|
||||||
let textAttr: [NSAttributedString.Key: AnyObject] = [
|
candidateWithLabelAttrDict = [.font: candidateFont,
|
||||||
.font: keyLabelFont,
|
.paragraphStyle: paraStyle,
|
||||||
.foregroundColor: black,
|
.foregroundColor: NSColor.labelColor] // We still need this dummy section to make sure the space occupations of the candidates are correct.
|
||||||
.paragraphStyle: paraStyle]
|
|
||||||
for index in 0..<count {
|
|
||||||
let textRect = NSRect(x: 0.0, y: CGFloat(index) * cellHeight + labelOffsetY, width: bounds.size.width, height: cellHeight - labelOffsetY)
|
|
||||||
var cellRect = NSRect(x: 0.0, y: CGFloat(index) * cellHeight, width: bounds.size.width, height: cellHeight - 1)
|
|
||||||
|
|
||||||
if index + 1 >= count {
|
keyLabelAttrDict = [.font: labelFont,
|
||||||
cellRect.size.height += 1.0
|
.paragraphStyle: paraStyle,
|
||||||
|
.foregroundColor: NSColor.secondaryLabelColor] // Candidate phrase text color
|
||||||
|
candidateAttrDict = [.font: candidateFont,
|
||||||
|
.paragraphStyle: paraStyle,
|
||||||
|
.foregroundColor: NSColor.labelColor] // Candidate index text color
|
||||||
|
let labelFontSize = labelFont.pointSize
|
||||||
|
let candidateFontSize = candidateFont.pointSize
|
||||||
|
let biggestSize = max(labelFontSize, candidateFontSize)
|
||||||
|
keyLabelWidth = ceil(labelFontSize)
|
||||||
|
keyLabelHeight = ceil(labelFontSize * 1.20)
|
||||||
|
candidateTextHeight = ceil(candidateFontSize * 1.20)
|
||||||
|
cellPadding = ceil(biggestSize / 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
(index == highlightedIndex ? darkGray : lightGray).setFill()
|
|
||||||
NSBezierPath.fill(cellRect)
|
|
||||||
let text = keyLabels[Int(index)]
|
|
||||||
(text as NSString).draw(in: textRect, withAttributes: textAttr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate class VerticalCandidateTableView: NSTableView {
|
|
||||||
override func adjustScroll(_ newVisible: NSRect) -> NSRect {
|
|
||||||
var scrollRect = newVisible
|
|
||||||
let rowHeightPlusSpacing = rowHeight + intercellSpacing.height
|
|
||||||
scrollRect.origin.y = (scrollRect.origin.y / rowHeightPlusSpacing) * rowHeightPlusSpacing
|
|
||||||
return scrollRect
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let kCandidateTextPadding: CGFloat = 24.0
|
|
||||||
private let kCandidateTextLeftMargin: CGFloat = 8.0
|
|
||||||
private let kCandidateTextPaddingWithMandatedTableViewPadding: CGFloat = 18.0
|
|
||||||
private let kCandidateTextLeftMarginWithMandatedTableViewPadding: CGFloat = 0.0
|
|
||||||
|
|
||||||
private class BackgroundView: NSView {
|
|
||||||
override func draw(_ dirtyRect: NSRect) {
|
override func draw(_ dirtyRect: NSRect) {
|
||||||
NSColor.windowBackgroundColor.setFill()
|
|
||||||
NSBezierPath.fill(self.bounds)
|
// Give a standalone layer to the candidate list panel
|
||||||
|
self.wantsLayer = true
|
||||||
|
self.layer?.borderColor = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.30).cgColor
|
||||||
|
self.layer?.borderWidth = 1.0
|
||||||
|
self.layer?.cornerRadius = 6.0
|
||||||
|
|
||||||
|
let bounds = self.bounds
|
||||||
|
NSColor.controlBackgroundColor.setFill() // Candidate list panel base background
|
||||||
|
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 accuHeight: CGFloat = 0
|
||||||
|
for index in 0..<elementHeights.count {
|
||||||
|
let currentHeight = elementHeights[index]
|
||||||
|
let rctCandidateArea = NSRect(x: 0.0, y: accuHeight, width: windowWidth, height: candidateTextHeight + cellPadding)
|
||||||
|
let rctLabel = NSRect(x: cellPadding / 2 - 1, y: accuHeight + cellPadding / 2, width: keyLabelWidth, height: candidateTextHeight)
|
||||||
|
let rctCandidatePhrase = NSRect(x: cellPadding / 2 - 1 + keyLabelWidth, y: accuHeight + cellPadding / 2 - 1, width: windowWidth - keyLabelWidth, height: candidateTextHeight)
|
||||||
|
|
||||||
|
var activeCandidateIndexAttr = keyLabelAttrDict
|
||||||
|
var activeCandidateAttr = candidateAttrDict
|
||||||
|
if index == highlightedIndex {
|
||||||
|
NSColor.alternateSelectedControlColor.setFill() // The background color of the highlightened candidate
|
||||||
|
activeCandidateIndexAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor.withAlphaComponent(0.84) // The index text color of the highlightened candidate
|
||||||
|
activeCandidateAttr[.foregroundColor] = NSColor.selectedMenuItemTextColor // The phrase text color of the highlightened candidate
|
||||||
|
} else {
|
||||||
|
NSColor.controlBackgroundColor.setFill()
|
||||||
|
}
|
||||||
|
NSBezierPath.fill(rctCandidateArea)
|
||||||
|
(keyLabels[index] as NSString).draw(in: rctLabel, withAttributes: activeCandidateIndexAttr)
|
||||||
|
(displayedCandidates[index] as NSString).draw(in: rctCandidatePhrase, withAttributes: activeCandidateAttr)
|
||||||
|
accuHeight += currentHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findHitIndex(event: NSEvent) -> UInt? {
|
||||||
|
let location = convert(event.locationInWindow, to: nil)
|
||||||
|
if !NSPointInRect(location, self.bounds) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var accuHeight: CGFloat = 0.0
|
||||||
|
for index in 0..<elementHeights.count {
|
||||||
|
let currentHeight = elementHeights[index]
|
||||||
|
|
||||||
|
if location.y >= accuHeight && location.y <= accuHeight + currentHeight {
|
||||||
|
return UInt(index)
|
||||||
|
}
|
||||||
|
accuHeight += currentHeight
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc(VTVerticalCandidateController)
|
@objc(VTVerticalCandidateController)
|
||||||
public class VerticalCandidateController: CandidateController {
|
public class VerticalCandidateController: CandidateController {
|
||||||
private var keyLabelStripView: VerticalKeyLabelStripView
|
private var candidateView: VerticalCandidateView
|
||||||
private var scrollView: NSScrollView
|
private var prevPageButton: NSButton
|
||||||
private var tableView: NSTableView
|
private var nextPageButton: NSButton
|
||||||
private var candidateTextParagraphStyle: NSMutableParagraphStyle
|
private var currentPage: UInt = 0
|
||||||
private var candidateTextPadding: CGFloat = kCandidateTextPadding
|
|
||||||
private var candidateTextLeftMargin: CGFloat = kCandidateTextLeftMargin
|
|
||||||
private var maxCandidateAttrStringWidth: CGFloat = 0
|
|
||||||
private let tooltipPadding: CGFloat = 2.0
|
|
||||||
private var tooltipView: NSTextField
|
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
|
var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0)
|
||||||
|
@ -88,62 +197,45 @@ public class VerticalCandidateController: CandidateController {
|
||||||
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
|
let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false)
|
||||||
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
|
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
|
||||||
panel.hasShadow = true
|
panel.hasShadow = true
|
||||||
panel.contentView = BackgroundView()
|
panel.isOpaque = false
|
||||||
|
panel.backgroundColor = NSColor.clear // Transparentify everything outside of the candidate list panel
|
||||||
tooltipView = NSTextField(frame: NSRect.zero)
|
|
||||||
tooltipView.isEditable = false
|
|
||||||
tooltipView.isSelectable = false
|
|
||||||
tooltipView.isBezeled = false
|
|
||||||
tooltipView.drawsBackground = true
|
|
||||||
tooltipView.lineBreakMode = .byTruncatingTail
|
|
||||||
|
|
||||||
contentRect.origin = NSPoint.zero
|
contentRect.origin = NSPoint.zero
|
||||||
var stripRect = contentRect
|
candidateView = VerticalCandidateView(frame: contentRect)
|
||||||
stripRect.size.width = 10.0
|
panel.contentView?.addSubview(candidateView)
|
||||||
keyLabelStripView = VerticalKeyLabelStripView(frame: stripRect)
|
|
||||||
panel.contentView?.addSubview(keyLabelStripView)
|
|
||||||
|
|
||||||
var scrollViewRect = contentRect
|
contentRect.size = NSSize(width: 20.0, height: 15.0) // Reduce the button width
|
||||||
scrollViewRect.origin.x = stripRect.size.width
|
nextPageButton = NSButton(frame: contentRect)
|
||||||
scrollViewRect.size.width -= stripRect.size.width
|
nextPageButton.setButtonType(.momentaryLight)
|
||||||
scrollView = NSScrollView(frame: scrollViewRect)
|
nextPageButton.bezelStyle = .shadowlessSquare
|
||||||
scrollView.verticalScrollElasticity = .none
|
nextPageButton.wantsLayer = true
|
||||||
|
nextPageButton.layer?.masksToBounds = true
|
||||||
tableView = NSTableView(frame: contentRect)
|
nextPageButton.layer?.borderColor = NSColor.clear.cgColor // Attempt to remove the system default layer border color - step 1
|
||||||
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "candidate"))
|
nextPageButton.layer?.borderWidth = 0.0 // Attempt to remove the system default layer border color - step 2
|
||||||
column.dataCell = NSTextFieldCell()
|
nextPageButton.layer?.backgroundColor = NSColor.black.cgColor // Button Background Color. Otherwise the button will be half-transparent in macOS Monterey Dark Mode.
|
||||||
column.isEditable = false
|
nextPageButton.attributedTitle = NSMutableAttributedString(string: "➡︎") // Next Page Arrow
|
||||||
|
prevPageButton = NSButton(frame: contentRect)
|
||||||
candidateTextPadding = kCandidateTextPadding
|
prevPageButton.setButtonType(.momentaryLight)
|
||||||
candidateTextLeftMargin = kCandidateTextLeftMargin
|
prevPageButton.bezelStyle = .shadowlessSquare
|
||||||
|
prevPageButton.wantsLayer = true
|
||||||
tableView.addTableColumn(column)
|
prevPageButton.layer?.masksToBounds = true
|
||||||
tableView.intercellSpacing = NSSize(width: 0.0, height: 1.0)
|
prevPageButton.layer?.borderColor = NSColor.clear.cgColor // Attempt to remove the system default layer border color - step 1
|
||||||
tableView.headerView = nil
|
prevPageButton.layer?.borderWidth = 0.0 // Attempt to remove the system default layer border color - step 2
|
||||||
tableView.allowsMultipleSelection = false
|
prevPageButton.layer?.backgroundColor = NSColor.black.cgColor // Button Background Color. Otherwise the button will be half-transparent in macOS Monterey Dark Mode.
|
||||||
tableView.allowsEmptySelection = false
|
prevPageButton.attributedTitle = NSMutableAttributedString(string: "⬅︎") // Previous Page Arrow
|
||||||
|
panel.contentView?.addSubview(nextPageButton)
|
||||||
if #available(macOS 10.16, *) {
|
panel.contentView?.addSubview(prevPageButton)
|
||||||
tableView.style = .fullWidth
|
|
||||||
candidateTextPadding = kCandidateTextPaddingWithMandatedTableViewPadding
|
|
||||||
candidateTextLeftMargin = kCandidateTextLeftMarginWithMandatedTableViewPadding
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollView.documentView = tableView
|
|
||||||
panel.contentView?.addSubview(scrollView)
|
|
||||||
|
|
||||||
let paraStyle = NSMutableParagraphStyle()
|
|
||||||
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
|
||||||
paraStyle.firstLineHeadIndent = candidateTextLeftMargin
|
|
||||||
paraStyle.lineBreakMode = .byClipping
|
|
||||||
|
|
||||||
candidateTextParagraphStyle = paraStyle
|
|
||||||
|
|
||||||
super.init(window: panel)
|
super.init(window: panel)
|
||||||
tableView.dataSource = self
|
|
||||||
tableView.delegate = self
|
candidateView.target = self
|
||||||
tableView.doubleAction = #selector(rowDoubleClicked(_:))
|
candidateView.action = #selector(candidateViewMouseDidClick(_:))
|
||||||
tableView.target = self
|
|
||||||
|
nextPageButton.target = self
|
||||||
|
nextPageButton.action = #selector(pageButtonAction(_:))
|
||||||
|
|
||||||
|
prevPageButton.target = self
|
||||||
|
prevPageButton.action = #selector(pageButtonAction(_:))
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
|
@ -151,28 +243,66 @@ public class VerticalCandidateController: CandidateController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func reloadData() {
|
public override func reloadData() {
|
||||||
maxCandidateAttrStringWidth = ceil(candidateFont.pointSize * 2.0 + candidateTextPadding)
|
candidateView.highlightedIndex = 0
|
||||||
tableView.reloadData()
|
currentPage = 0
|
||||||
layoutCandidateView()
|
layoutCandidateView()
|
||||||
if delegate?.candidateCountForController(self) ?? 0 > 0 {
|
|
||||||
selectedCandidateIndex = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func showNextPage() -> Bool {
|
public override func showNextPage() -> Bool {
|
||||||
scrollPageByOne(true)
|
guard delegate != nil else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentPage + 1 >= pageCount {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage += 1
|
||||||
|
candidateView.highlightedIndex = 0
|
||||||
|
layoutCandidateView()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func showPreviousPage() -> Bool {
|
public override func showPreviousPage() -> Bool {
|
||||||
scrollPageByOne(false)
|
guard delegate != nil else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentPage == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage -= 1
|
||||||
|
candidateView.highlightedIndex = 0
|
||||||
|
layoutCandidateView()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func highlightNextCandidate() -> Bool {
|
public override func highlightNextCandidate() -> Bool {
|
||||||
moveSelectionByOne(true)
|
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 {
|
public override func highlightPreviousCandidate() -> Bool {
|
||||||
moveSelectionByOne(false)
|
guard delegate != nil else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentIndex = selectedCandidateIndex
|
||||||
|
if currentIndex == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCandidateIndex = currentIndex - 1
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt {
|
public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt {
|
||||||
|
@ -180,285 +310,103 @@ public class VerticalCandidateController: CandidateController {
|
||||||
return UInt.max
|
return UInt.max
|
||||||
}
|
}
|
||||||
|
|
||||||
let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin)
|
let result = currentPage * UInt(keyLabels.count) + index
|
||||||
if firstVisibleRow != -1 {
|
return result < delegate.candidateCountForController(self) ? result : UInt.max
|
||||||
let result = UInt(firstVisibleRow) + index
|
|
||||||
if result < delegate.candidateCountForController(self) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return UInt.max
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override var selectedCandidateIndex: UInt {
|
public override var selectedCandidateIndex: UInt {
|
||||||
get {
|
get {
|
||||||
let selectedRow = tableView.selectedRow
|
currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex
|
||||||
return selectedRow == -1 ? UInt.max : UInt(selectedRow)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
guard let delegate = delegate else {
|
guard let delegate = delegate else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var newIndex = newValue
|
let keyLabelCount = UInt(keyLabels.count)
|
||||||
let selectedRow = tableView.selectedRow
|
if newValue < delegate.candidateCountForController(self) {
|
||||||
let labelCount = keyLabels.count
|
currentPage = newValue / keyLabelCount
|
||||||
let itemCount = delegate.candidateCountForController(self)
|
candidateView.highlightedIndex = newValue % keyLabelCount
|
||||||
|
layoutCandidateView()
|
||||||
if newIndex == UInt.max {
|
|
||||||
if itemCount == 0 {
|
|
||||||
tableView.deselectAll(self)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
newIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastVisibleRow = newValue
|
|
||||||
|
|
||||||
if selectedRow != -1 && itemCount > 0 && itemCount > labelCount {
|
|
||||||
if newIndex > selectedRow && (Int(newIndex) - selectedRow) > 1 {
|
|
||||||
lastVisibleRow = min(newIndex + UInt(labelCount) - 1, itemCount - 1)
|
|
||||||
}
|
|
||||||
// no need to handle the backward case: (newIndex < selectedRow && selectedRow - newIndex > 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if itemCount > labelCount {
|
|
||||||
tableView.scrollRowToVisible(Int(lastVisibleRow))
|
|
||||||
}
|
|
||||||
tableView.selectRowIndexes(IndexSet(integer: Int(newIndex)), byExtendingSelection: false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension VerticalCandidateController: NSTableViewDataSource, NSTableViewDelegate {
|
extension VerticalCandidateController {
|
||||||
|
|
||||||
public func numberOfRows(in tableView: NSTableView) -> Int {
|
private var pageCount: UInt {
|
||||||
Int(delegate?.candidateCountForController(self) ?? 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
|
|
||||||
guard let delegate = delegate else {
|
guard let delegate = delegate else {
|
||||||
return nil
|
return 0
|
||||||
}
|
}
|
||||||
var candidate = ""
|
let totalCount = delegate.candidateCountForController(self)
|
||||||
if row < delegate.candidateCountForController(self) {
|
let keyLabelCount = UInt(keyLabels.count)
|
||||||
candidate = delegate.candidateController(self, candidateAtIndex: UInt(row))
|
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0)
|
||||||
}
|
|
||||||
let attrString = NSAttributedString(string: candidate, attributes: [
|
|
||||||
.font: candidateFont,
|
|
||||||
.paragraphStyle: candidateTextParagraphStyle
|
|
||||||
])
|
|
||||||
|
|
||||||
// we do more work than what this method is expected to; normally not a good practice, but for the amount of data (9 to 10 rows max), we can afford the overhead
|
|
||||||
|
|
||||||
// expand the window width if text overflows
|
|
||||||
let boundingRect = attrString.boundingRect(with: NSSize(width: 10240.0, height: 10240.0), options: .usesLineFragmentOrigin)
|
|
||||||
let textWidth = boundingRect.size.width + candidateTextPadding
|
|
||||||
if textWidth > maxCandidateAttrStringWidth {
|
|
||||||
maxCandidateAttrStringWidth = textWidth
|
|
||||||
layoutCandidateView()
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep track of the highlighted index in the key label strip
|
|
||||||
let count = UInt(keyLabels.count)
|
|
||||||
let selectedRow = tableView.selectedRow
|
|
||||||
|
|
||||||
if selectedRow != -1 {
|
|
||||||
var newHilightIndex = 0
|
|
||||||
|
|
||||||
if keyLabelStripView.highlightedIndex != -1 &&
|
|
||||||
(row >= selectedRow + Int(count) || (selectedRow > count && row <= selectedRow - Int(count))) {
|
|
||||||
newHilightIndex = -1
|
|
||||||
} else {
|
|
||||||
let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin)
|
|
||||||
newHilightIndex = selectedRow - firstVisibleRow
|
|
||||||
if newHilightIndex < -1 {
|
|
||||||
newHilightIndex = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if newHilightIndex != keyLabelStripView.highlightedIndex && newHilightIndex >= 0 {
|
|
||||||
keyLabelStripView.highlightedIndex = UInt(newHilightIndex)
|
|
||||||
keyLabelStripView.setNeedsDisplay(keyLabelStripView.frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
return attrString
|
|
||||||
}
|
|
||||||
|
|
||||||
public func tableViewSelectionDidChange(_ notification: Notification) {
|
|
||||||
let selectedRow = tableView.selectedRow
|
|
||||||
if selectedRow != -1 {
|
|
||||||
// keep track of the highlighted index in the key label strip
|
|
||||||
let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin)
|
|
||||||
// firstVisibleRow cannot be larger than selectedRow.
|
|
||||||
if selectedRow >= firstVisibleRow {
|
|
||||||
keyLabelStripView.highlightedIndex = UInt(selectedRow - firstVisibleRow)
|
|
||||||
} else {
|
|
||||||
keyLabelStripView.highlightedIndex = UInt.max
|
|
||||||
}
|
|
||||||
|
|
||||||
keyLabelStripView.setNeedsDisplay(keyLabelStripView.frame)
|
|
||||||
|
|
||||||
// fix a subtle OS X "bug" that, since we force the scroller to appear,
|
|
||||||
// scrolling sometimes shows a temporarily "broken" scroll bar
|
|
||||||
// (but quickly disappears)
|
|
||||||
if scrollView.hasVerticalScroller {
|
|
||||||
scrollView.verticalScroller?.setNeedsDisplay()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func rowDoubleClicked(_ sender: Any) {
|
|
||||||
let clickedRow = tableView.clickedRow
|
|
||||||
if clickedRow != -1 {
|
|
||||||
delegate?.candidateController(self, didSelectCandidateAtIndex: UInt(clickedRow))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollPageByOne(_ forward: Bool) -> Bool {
|
|
||||||
guard let delegate = delegate else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let labelCount = UInt(keyLabels.count)
|
|
||||||
let itemCount = delegate.candidateCountForController(self)
|
|
||||||
if 0 == itemCount {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if itemCount <= labelCount {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var newIndex = selectedCandidateIndex
|
|
||||||
if forward {
|
|
||||||
if newIndex >= itemCount - 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
newIndex = min(newIndex + labelCount, itemCount - 1)
|
|
||||||
} else {
|
|
||||||
if newIndex == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if newIndex < labelCount {
|
|
||||||
newIndex = 0
|
|
||||||
} else {
|
|
||||||
newIndex -= labelCount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedCandidateIndex = newIndex
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func moveSelectionByOne(_ forward: Bool) -> Bool {
|
|
||||||
guard let delegate = delegate else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let itemCount = delegate.candidateCountForController(self)
|
|
||||||
if 0 == itemCount {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var newIndex = selectedCandidateIndex
|
|
||||||
if newIndex == UInt.max {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if forward {
|
|
||||||
if newIndex >= itemCount - 1 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
newIndex += 1
|
|
||||||
} else {
|
|
||||||
if 0 == newIndex {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
newIndex -= 1
|
|
||||||
}
|
|
||||||
selectedCandidateIndex = newIndex
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func layoutCandidateView() {
|
private func layoutCandidateView() {
|
||||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { [self] in
|
|
||||||
doLayoutCandidateView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func doLayoutCandidateView() {
|
|
||||||
guard let delegate = delegate else {
|
guard let delegate = delegate else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont)
|
||||||
|
var candidates = [String]()
|
||||||
let count = delegate.candidateCountForController(self)
|
let count = delegate.candidateCountForController(self)
|
||||||
if 0 == count {
|
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.map { $0.displayedText}, displayedCandidates: candidates)
|
||||||
|
var newSize = candidateView.sizeForView
|
||||||
|
var frameRect = candidateView.frame
|
||||||
|
frameRect.size = newSize
|
||||||
|
candidateView.frame = frameRect
|
||||||
|
|
||||||
|
if pageCount > 1 {
|
||||||
|
var buttonRect = nextPageButton.frame
|
||||||
|
let spacing:CGFloat = 0.0
|
||||||
|
|
||||||
|
// buttonRect.size.height = floor(candidateTextHeight + cellPadding / 2)
|
||||||
|
|
||||||
|
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)
|
||||||
|
candidateView.setNeedsDisplay(candidateView.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc fileprivate func pageButtonAction(_ sender: Any) {
|
||||||
|
guard let sender = sender as? NSButton else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if sender == nextPageButton {
|
||||||
|
_ = showNextPage()
|
||||||
var tooltipHeight: CGFloat = 0
|
} else if sender == prevPageButton {
|
||||||
var tooltipWidth: CGFloat = 0
|
_ = showPreviousPage()
|
||||||
|
}
|
||||||
if !tooltip.isEmpty {
|
|
||||||
tooltipView.stringValue = tooltip
|
|
||||||
let size = tooltipView.intrinsicContentSize
|
|
||||||
tooltipWidth = size.width + tooltipPadding * 2
|
|
||||||
tooltipHeight = size.height + tooltipPadding * 2
|
|
||||||
self.window?.contentView?.addSubview(tooltipView)
|
|
||||||
} else {
|
|
||||||
tooltipView.removeFromSuperview()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidateFontSize = ceil(candidateFont.pointSize)
|
@objc fileprivate func candidateViewMouseDidClick(_ sender: Any) {
|
||||||
let keyLabelFontSize = ceil(keyLabelFont.pointSize)
|
delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex)
|
||||||
let fontSize = max(candidateFontSize, keyLabelFontSize)
|
|
||||||
|
|
||||||
let controlSize: NSControl.ControlSize = fontSize > 36.0 ? .regular : .small
|
|
||||||
|
|
||||||
var keyLabelCount = UInt(keyLabels.count)
|
|
||||||
var scrollerWidth: CGFloat = 0.0
|
|
||||||
if count <= keyLabelCount {
|
|
||||||
keyLabelCount = count
|
|
||||||
scrollView.hasVerticalScroller = false
|
|
||||||
} else {
|
|
||||||
scrollView.hasVerticalScroller = true
|
|
||||||
let verticalScroller = scrollView.verticalScroller
|
|
||||||
verticalScroller?.controlSize = controlSize
|
|
||||||
verticalScroller?.scrollerStyle = .legacy
|
|
||||||
scrollerWidth = NSScroller.scrollerWidth(for: controlSize, scrollerStyle: .legacy)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
keyLabelStripView.keyLabelFont = keyLabelFont
|
|
||||||
let actualKeyLabels = keyLabels[0..<Int(keyLabelCount)].map { $0.displayedText }
|
|
||||||
keyLabelStripView.keyLabels = actualKeyLabels
|
|
||||||
keyLabelStripView.labelOffsetY = (keyLabelFontSize >= candidateFontSize) ? 0.0 : floor((candidateFontSize - keyLabelFontSize) / 2.0)
|
|
||||||
|
|
||||||
let rowHeight = ceil(fontSize * 1.25)
|
|
||||||
tableView.rowHeight = rowHeight
|
|
||||||
|
|
||||||
var maxKeyLabelWidth = keyLabelFontSize
|
|
||||||
let textAttr: [NSAttributedString.Key: AnyObject] = [.font: keyLabelFont]
|
|
||||||
let boundingBox = NSSize(width: 1600.0, height: 1600.0)
|
|
||||||
|
|
||||||
for label in actualKeyLabels {
|
|
||||||
let rect = (label as NSString).boundingRect(with: boundingBox, options: .usesLineFragmentOrigin, attributes: textAttr)
|
|
||||||
maxKeyLabelWidth = max(rect.size.width, maxKeyLabelWidth)
|
|
||||||
}
|
|
||||||
|
|
||||||
let rowSpacing = tableView.intercellSpacing.height
|
|
||||||
let stripWidth = ceil(maxKeyLabelWidth * 1.20)
|
|
||||||
let tableViewStartWidth = ceil(maxCandidateAttrStringWidth + scrollerWidth)
|
|
||||||
let windowWidth = max(stripWidth + 1.0 + tableViewStartWidth, tooltipWidth)
|
|
||||||
let windowHeight = CGFloat(keyLabelCount) * (rowHeight + rowSpacing) + tooltipHeight
|
|
||||||
|
|
||||||
var frameRect = self.window?.frame ?? NSRect.zero
|
|
||||||
let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height)
|
|
||||||
|
|
||||||
frameRect.size = NSMakeSize(windowWidth, windowHeight)
|
|
||||||
frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height)
|
|
||||||
|
|
||||||
keyLabelStripView.frame = NSRect(x: 0.0, y: 0, width: stripWidth, height: windowHeight - tooltipHeight)
|
|
||||||
scrollView.frame = NSRect(x: stripWidth + 1.0, y: 0, width: (windowWidth - stripWidth - 1), height: windowHeight - tooltipHeight)
|
|
||||||
tooltipView.frame = NSRect(x: tooltipPadding, y: windowHeight - tooltipHeight + tooltipPadding, width: windowWidth, height: tooltipHeight)
|
|
||||||
self.window?.setFrame(frameRect, display: false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue