Zonble: Swiftify // Voltaire.
This commit is contained in:
parent
ab3f5bcb9c
commit
79fcdb3ec0
|
@ -0,0 +1,418 @@
|
|||
//
|
||||
// HorizontalCandidateController.swift
|
||||
//
|
||||
// Copyright (c) 2011 The McBopomofo Project.
|
||||
//
|
||||
// Contributors:
|
||||
// Mengjuei Hsieh (@mjhsieh)
|
||||
// Weizhong Yang (@zonble)
|
||||
//
|
||||
// Based on the Syrup Project and the Formosana Library
|
||||
// by Lukhnos Liu (@lukhnos).
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
fileprivate 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: candidateAttrDict)
|
||||
let cellWidth = max(labelRect.size.width, candidateRect.size.width) + cellPadding;
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc (VTHorizontalCandidateController)
|
||||
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)
|
||||
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel))
|
||||
panel.hasShadow = true
|
||||
|
||||
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)
|
||||
nextPageButton.setButtonType(.momentaryLight)
|
||||
nextPageButton.bezelStyle = .smallSquare
|
||||
nextPageButton.title = "»"
|
||||
|
||||
prevPageButton = NSButton(frame: contentRect)
|
||||
prevPageButton.setButtonType(.momentaryLight)
|
||||
prevPageButton.bezelStyle = .smallSquare
|
||||
prevPageButton.title = "«"
|
||||
|
||||
panel.contentView?.addSubview(nextPageButton)
|
||||
panel.contentView?.addSubview(prevPageButton)
|
||||
|
||||
super.init(window: panel)
|
||||
|
||||
candidateView.target = self
|
||||
candidateView.action = #selector(candidateViewMouseDidClick(_:))
|
||||
|
||||
nextPageButton.target = self
|
||||
nextPageButton.action = #selector(pageButtonAction(_:))
|
||||
|
||||
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 >= 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 = selectedCandidateIndex
|
||||
if currentIndex == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
selectedCandidateIndex = currentIndex - 1
|
||||
return true
|
||||
}
|
||||
|
||||
public override 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
|
||||
}
|
||||
|
||||
public override var selectedCandidateIndex: UInt {
|
||||
get {
|
||||
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 {
|
||||
|
||||
private 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)
|
||||
}
|
||||
|
||||
private 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 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)
|
||||
candidateView.setNeedsDisplay(candidateView.bounds)
|
||||
}
|
||||
|
||||
@objc fileprivate func pageButtonAction(_ sender: Any) {
|
||||
guard let sender = sender as? NSButton else {
|
||||
return
|
||||
}
|
||||
if sender == nextPageButton {
|
||||
_ = showNextPage()
|
||||
} else if sender == prevPageButton {
|
||||
_ = showPreviousPage()
|
||||
}
|
||||
}
|
||||
|
||||
@objc fileprivate func candidateViewMouseDidClick(_ sender: Any) {
|
||||
delegate?.candidateController(self, didSelectCandidateAtIndex: selectedCandidateIndex)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
//
|
||||
// VTCandidateController.h
|
||||
//
|
||||
// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class VTCandidateController;
|
||||
|
||||
@protocol VTCandidateControllerDelegate
|
||||
- (NSUInteger)candidateCountForController:(VTCandidateController *)controller;
|
||||
- (NSString *)candidateController:(VTCandidateController *)controller candidateAtIndex:(NSUInteger)index;
|
||||
- (void)candidateController:(VTCandidateController *)controller didSelectCandidateAtIndex:(NSUInteger)index;
|
||||
@end
|
||||
|
||||
@interface VTCandidateController : NSWindowController
|
||||
{
|
||||
@protected
|
||||
__weak id<VTCandidateControllerDelegate> _delegate;
|
||||
NSArray *_keyLabels;
|
||||
NSFont *_keyLabelFont;
|
||||
NSFont *_candidateFont;
|
||||
BOOL _visible;
|
||||
}
|
||||
|
||||
- (void)reloadData;
|
||||
|
||||
- (BOOL)showNextPage;
|
||||
- (BOOL)showPreviousPage;
|
||||
- (BOOL)highlightNextCandidate;
|
||||
- (BOOL)highlightPreviousCandidate;
|
||||
|
||||
- (void)setWindowTopLeftPoint:(NSPoint)topLeftPoint bottomOutOfScreenAdjustmentHeight:(CGFloat)height;
|
||||
|
||||
- (NSUInteger)candidateIndexAtKeyLabelIndex:(NSUInteger)index;
|
||||
|
||||
@property (weak, nonatomic) id<VTCandidateControllerDelegate> delegate;
|
||||
@property (assign, nonatomic) NSUInteger selectedCandidateIndex;
|
||||
|
||||
@property (assign, nonatomic) BOOL visible;
|
||||
@property (assign, nonatomic) NSPoint windowTopLeftPoint;
|
||||
|
||||
@property (copy, nonatomic) NSArray *keyLabels;
|
||||
@property (copy, nonatomic) NSFont *keyLabelFont;
|
||||
@property (copy, nonatomic) NSFont *candidateFont;
|
||||
@end
|
|
@ -1,178 +0,0 @@
|
|||
//
|
||||
// VTCandidateController.m
|
||||
//
|
||||
// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import "VTCandidateController.h"
|
||||
|
||||
|
||||
@implementation VTCandidateController
|
||||
@synthesize delegate = _delegate;
|
||||
@synthesize keyLabels = _keyLabels;
|
||||
@synthesize keyLabelFont = _keyLabelFont;
|
||||
@synthesize candidateFont = _candidateFont;
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
_keyLabels = nil;
|
||||
_keyLabelFont = nil;
|
||||
_candidateFont = nil;
|
||||
}
|
||||
|
||||
- (id)initWithWindow:(NSWindow *)window
|
||||
{
|
||||
self = [super initWithWindow:window];
|
||||
if (self) {
|
||||
// populate the default values
|
||||
_keyLabels = @[@"1", @"2", @"3", @"4", @"5", @"6", @"7", @"8", @"9"];
|
||||
_keyLabelFont = [NSFont monospacedDigitSystemFontOfSize:14.0 weight:NSFontWeightMedium];
|
||||
_candidateFont = [NSFont systemFontOfSize:18.0 weight:NSFontWeightRegular];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)reloadData
|
||||
{
|
||||
}
|
||||
|
||||
- (BOOL)showNextPage
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)showPreviousPage
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)highlightNextCandidate
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)highlightPreviousCandidate
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)setWindowTopLeftPoint:(NSPoint)topLeftPoint bottomOutOfScreenAdjustmentHeight:(CGFloat)height
|
||||
{
|
||||
// Since layout is now deferred, the origin setting should also be deferred so that
|
||||
// the correct visible frame dimensions are used.
|
||||
NSArray *params = [NSArray arrayWithObjects:[NSValue valueWithPoint:topLeftPoint], [NSNumber numberWithDouble:height], nil];
|
||||
[self performSelector:@selector(deferredSetWindowTopLeftPoint:) withObject:params afterDelay:0.0];
|
||||
}
|
||||
|
||||
- (void)deferredSetWindowTopLeftPoint:(NSArray *)params
|
||||
{
|
||||
NSPoint topLeftPoint = [[params objectAtIndex:0] pointValue];
|
||||
CGFloat height = [[params objectAtIndex:1] doubleValue];
|
||||
|
||||
NSPoint adjustedPoint = topLeftPoint;
|
||||
CGFloat adjustedHeight = height;
|
||||
|
||||
// first, locate the screen the point is in
|
||||
NSRect screenFrame = [[NSScreen mainScreen] visibleFrame];
|
||||
|
||||
for (NSScreen *screen in [NSScreen screens]) {
|
||||
NSRect frame = [screen visibleFrame];
|
||||
if (topLeftPoint.x >= NSMinX(frame) && topLeftPoint.x <= NSMaxX(frame) && topLeftPoint.y >= NSMinY(frame) && topLeftPoint.y <= NSMaxY(frame)) {
|
||||
screenFrame = frame;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we don't have any erratic value
|
||||
if (adjustedHeight > screenFrame.size.height / 2.0) {
|
||||
adjustedHeight = 0.0;
|
||||
}
|
||||
|
||||
NSSize windowSize = [[self window] frame].size;
|
||||
|
||||
// bottom beneath the screen?
|
||||
if (adjustedPoint.y - windowSize.height < NSMinY(screenFrame)) {
|
||||
adjustedPoint.y = topLeftPoint.y + adjustedHeight + windowSize.height;
|
||||
}
|
||||
|
||||
// top over the screen?
|
||||
if (adjustedPoint.y >= NSMaxY(screenFrame)) {
|
||||
adjustedPoint.y = NSMaxY(screenFrame) - 1.0;
|
||||
}
|
||||
|
||||
// right
|
||||
if (adjustedPoint.x + windowSize.width >= NSMaxX(screenFrame)) {
|
||||
adjustedPoint.x = NSMaxX(screenFrame) - windowSize.width;
|
||||
}
|
||||
|
||||
// left
|
||||
if (adjustedPoint.x < NSMinX(screenFrame)) {
|
||||
adjustedPoint.x = NSMinX(screenFrame);
|
||||
}
|
||||
|
||||
[[self window] setFrameTopLeftPoint:adjustedPoint];
|
||||
}
|
||||
|
||||
- (NSUInteger)candidateIndexAtKeyLabelIndex:(NSUInteger)index
|
||||
{
|
||||
return NSUIntegerMax;
|
||||
}
|
||||
|
||||
- (BOOL)visible
|
||||
{
|
||||
// Because setVisible: defers its action, we need to use our own visible. Do not use [[self window] isVisible].
|
||||
return _visible;
|
||||
}
|
||||
|
||||
- (void)setVisible:(BOOL)visible
|
||||
{
|
||||
_visible = visible;
|
||||
if (visible) {
|
||||
[[self window] performSelector:@selector(orderFront:) withObject:self afterDelay:0.0];
|
||||
}
|
||||
else {
|
||||
[[self window] performSelector:@selector(orderOut:) withObject:self afterDelay:0.0];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSPoint)windowTopLeftPoint
|
||||
{
|
||||
NSRect frameRect = [[self window] frame];
|
||||
return NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height);
|
||||
}
|
||||
|
||||
- (void)setWindowTopLeftPoint:(NSPoint)topLeftPoint
|
||||
{
|
||||
[self setWindowTopLeftPoint:topLeftPoint bottomOutOfScreenAdjustmentHeight:0.0];
|
||||
}
|
||||
|
||||
- (NSUInteger)selectedCandidateIndex
|
||||
{
|
||||
return NSUIntegerMax;
|
||||
}
|
||||
|
||||
- (void)setSelectedCandidateIndex:(NSUInteger)newIndex
|
||||
{
|
||||
}
|
||||
@end
|
|
@ -0,0 +1,152 @@
|
|||
//
|
||||
// VTCandidateController.swift
|
||||
//
|
||||
// Voltaire IME Candidate Controller Module
|
||||
//
|
||||
// Copyright (c) 2011-2022 The OpenVanilla Project.
|
||||
//
|
||||
// Contributors:
|
||||
// Lukhnos Liu (@lukhnos) @ OpenVanilla // Original Developer
|
||||
// Weizhong Yang (@zonble) @ OpenVanilla // Rewriter to Swift
|
||||
//
|
||||
// Based on the Syrup Project and the Formosana Library
|
||||
// by Lukhnos Liu (@lukhnos).
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
@objc (VTCandidateControllerDelegate)
|
||||
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 (VTCandidateController)
|
||||
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 {
|
||||
false
|
||||
}
|
||||
|
||||
@objc public func showPreviousPage() -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
@objc public func highlightNextCandidate() -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
@objc public func highlightPreviousCandidate() -> Bool {
|
||||
false
|
||||
}
|
||||
|
||||
@objc public func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt {
|
||||
UInt.max
|
||||
}
|
||||
|
||||
@objc (setWindowTopLeftPoint:bottomOutOfScreenAdjustmentHeight:)
|
||||
public func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height: CGFloat) {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
|
||||
self.doSet(windowTopLeftPoint: windowTopLeftPoint, bottomOutOfScreenAdjustmentHeight: height)
|
||||
}
|
||||
}
|
||||
|
||||
func doSet(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.minY {
|
||||
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 = screenFrame.maxX - windowSize.width
|
||||
}
|
||||
|
||||
// left
|
||||
if adjustedPoint.x < screenFrame.minX {
|
||||
adjustedPoint.x = screenFrame.minX
|
||||
}
|
||||
|
||||
window?.setFrameTopLeftPoint(adjustedPoint)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
//
|
||||
// VTHorizontalCandidateController.h
|
||||
//
|
||||
// Copyright (c) 2021-2022 The vChewing Project.
|
||||
// Copyright (c) 2011-2022 The OpenVanilla Project.
|
||||
//
|
||||
// Contributors:
|
||||
// Lukhnos Liu (@lukhnos) @ OpenVanilla
|
||||
// Shiki Suen (@ShikiSuen) @ vChewing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import "VTCandidateController.h"
|
||||
|
||||
@class VTHorizontalCandidateView;
|
||||
|
||||
@interface VTHorizontalCandidateController : VTCandidateController
|
||||
{
|
||||
@protected
|
||||
VTHorizontalCandidateView *_candidateView;
|
||||
NSButton *_prevPageButton;
|
||||
NSButton *_nextPageButton;
|
||||
NSUInteger _currentPage;
|
||||
}
|
||||
@end
|
|
@ -1,267 +0,0 @@
|
|||
//
|
||||
// VTHorizontalCandidateController.m
|
||||
//
|
||||
// Copyright (c) 2021-2022 The vChewing Project.
|
||||
// Copyright (c) 2011-2022 The OpenVanilla Project.
|
||||
//
|
||||
// Contributors:
|
||||
// Lukhnos Liu (@lukhnos) @ OpenVanilla
|
||||
// Shiki Suen (@ShikiSuen) @ vChewing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import "VTHorizontalCandidateController.h"
|
||||
#import "VTHorizontalCandidateView.h"
|
||||
|
||||
@interface VTHorizontalCandidateController (Private)
|
||||
- (NSUInteger)pageCount;
|
||||
- (void)layoutCandidateView;
|
||||
- (void)pageButtonAction:(id)sender;
|
||||
- (void)candidateViewMouseDidClick:(id)sender;
|
||||
@end
|
||||
|
||||
|
||||
@implementation VTHorizontalCandidateController
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
_candidateView = nil;
|
||||
_prevPageButton = nil;
|
||||
_nextPageButton = nil;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
NSRect contentRect = NSMakeRect(128.0, 128.0, 0.0, 0.0);
|
||||
NSUInteger styleMask = NSBorderlessWindowMask | NSNonactivatingPanelMask;
|
||||
|
||||
NSWindow *panel = [[NSWindow alloc] initWithContentRect:contentRect styleMask:styleMask backing:NSBackingStoreBuffered defer:NO];
|
||||
[panel setLevel:kCGPopUpMenuWindowLevel];
|
||||
[panel setHasShadow:YES];
|
||||
[panel setOpaque:NO];
|
||||
[panel setBackgroundColor:[NSColor clearColor]];
|
||||
|
||||
self = [self initWithWindow:panel];
|
||||
if (self) {
|
||||
contentRect.origin = NSMakePoint(0.0, 0.0);
|
||||
_candidateView = [[VTHorizontalCandidateView alloc] initWithFrame:contentRect];
|
||||
_candidateView.target = self;
|
||||
_candidateView.action = @selector(candidateViewMouseDidClick:);
|
||||
[[panel contentView] addSubview:_candidateView];
|
||||
|
||||
contentRect.size = NSMakeSize(16.0, 20.0);
|
||||
_nextPageButton = [[NSButton alloc] initWithFrame:contentRect];
|
||||
_prevPageButton = [[NSButton alloc] initWithFrame:contentRect];
|
||||
[_nextPageButton setButtonType:NSMomentaryLightButton];
|
||||
[_nextPageButton setBezelStyle:NSBezelStyleSmallSquare];
|
||||
[_nextPageButton setTitle:@"↓"];
|
||||
[_nextPageButton setTarget:self];
|
||||
[_nextPageButton setAction:@selector(pageButtonAction:)];
|
||||
[_nextPageButton setWantsLayer: YES];
|
||||
[_nextPageButton.layer setCornerRadius: 3];
|
||||
[_nextPageButton.layer setBorderColor: [NSColor clearColor].CGColor];
|
||||
[_nextPageButton.layer setBorderWidth: 3];
|
||||
[_nextPageButton.layer setBackgroundColor: [NSColor windowBackgroundColor].CGColor];
|
||||
|
||||
[_prevPageButton setButtonType:NSMomentaryLightButton];
|
||||
[_prevPageButton setBezelStyle:NSBezelStyleSmallSquare];
|
||||
[_prevPageButton setTitle:@"↑"];
|
||||
[_prevPageButton setTarget:self];
|
||||
[_prevPageButton setAction:@selector(pageButtonAction:)];
|
||||
[_prevPageButton setWantsLayer: YES];
|
||||
[_prevPageButton.layer setCornerRadius: 3];
|
||||
[_prevPageButton.layer setBorderColor: [NSColor clearColor].CGColor];
|
||||
[_prevPageButton.layer setBorderWidth: 3];
|
||||
[_prevPageButton.layer setBackgroundColor: [NSColor windowBackgroundColor].CGColor];
|
||||
|
||||
[[panel contentView] addSubview:_nextPageButton];
|
||||
[[panel contentView] addSubview:_prevPageButton];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)reloadData
|
||||
{
|
||||
_candidateView.highlightedIndex = 0;
|
||||
_currentPage = 0;
|
||||
[self layoutCandidateView];
|
||||
}
|
||||
|
||||
- (BOOL)showNextPage
|
||||
{
|
||||
if (_currentPage + 1 >= [self pageCount]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
_currentPage++;
|
||||
_candidateView.highlightedIndex = 0;
|
||||
[self layoutCandidateView];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)showPreviousPage
|
||||
{
|
||||
if (_currentPage == 0) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
_currentPage--;
|
||||
_candidateView.highlightedIndex = 0;
|
||||
[self layoutCandidateView];
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)highlightNextCandidate
|
||||
{
|
||||
NSUInteger currentIndex = self.selectedCandidateIndex;
|
||||
if (currentIndex + 1 >= [_delegate candidateCountForController:self]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
self.selectedCandidateIndex = currentIndex + 1;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)highlightPreviousCandidate
|
||||
{
|
||||
NSUInteger currentIndex = self.selectedCandidateIndex;
|
||||
if (currentIndex == 0) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
self.selectedCandidateIndex = currentIndex - 1;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSUInteger)candidateIndexAtKeyLabelIndex:(NSUInteger)index
|
||||
{
|
||||
NSUInteger result = _currentPage * [_keyLabels count] + index;
|
||||
return result < [_delegate candidateCountForController:self] ? result : NSUIntegerMax;
|
||||
}
|
||||
|
||||
|
||||
- (NSUInteger)selectedCandidateIndex
|
||||
{
|
||||
return _currentPage * [_keyLabels count] + _candidateView.highlightedIndex;
|
||||
}
|
||||
|
||||
- (void)setSelectedCandidateIndex:(NSUInteger)newIndex
|
||||
{
|
||||
NSUInteger keyLabelCount = [_keyLabels count];
|
||||
if (newIndex < [_delegate candidateCountForController:self]) {
|
||||
_currentPage = newIndex / keyLabelCount;
|
||||
_candidateView.highlightedIndex = newIndex % keyLabelCount;
|
||||
[self layoutCandidateView];
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
|
||||
@implementation VTHorizontalCandidateController (Private)
|
||||
- (NSUInteger)pageCount
|
||||
{
|
||||
NSUInteger totalCount = [_delegate candidateCountForController:self];
|
||||
NSUInteger keyLabelCount = [_keyLabels count];
|
||||
return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
- (void)layoutCandidateView
|
||||
{
|
||||
[_candidateView setKeyLabelFont:_keyLabelFont candidateFont:_candidateFont];
|
||||
|
||||
NSMutableArray *candidates = [NSMutableArray array];
|
||||
NSUInteger count = [_delegate candidateCountForController:self];
|
||||
NSUInteger keyLabelCount = [_keyLabels count];
|
||||
for (NSUInteger index = _currentPage * keyLabelCount, j = 0; index < count && j < keyLabelCount; index++, j++) {
|
||||
[candidates addObject:[_delegate candidateController:self candidateAtIndex:index]];
|
||||
}
|
||||
|
||||
[_candidateView setKeyLabels:_keyLabels displayedCandidates:candidates];
|
||||
NSSize newSize = _candidateView.sizeForView;
|
||||
|
||||
NSRect frameRect = [_candidateView frame];
|
||||
frameRect.size = newSize;
|
||||
[_candidateView setFrame:frameRect];
|
||||
|
||||
if ([self pageCount] > 1) {
|
||||
NSRect buttonRect = [_nextPageButton frame];
|
||||
CGFloat 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);
|
||||
}
|
||||
|
||||
CGFloat buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) / 2.0;
|
||||
buttonRect.origin = NSMakePoint(newSize.width + 8.0, buttonOriginY);
|
||||
[_nextPageButton setFrame:buttonRect];
|
||||
|
||||
buttonRect.origin = NSMakePoint(newSize.width + 8.0, buttonOriginY + buttonRect.size.height + spacing);
|
||||
[_prevPageButton setFrame:buttonRect];
|
||||
|
||||
[_nextPageButton setEnabled:(_currentPage + 1 < [self pageCount])];
|
||||
[_prevPageButton setEnabled:(_currentPage != 0)];
|
||||
|
||||
newSize.width += 52.0;
|
||||
|
||||
[_nextPageButton setHidden:NO];
|
||||
[_prevPageButton setHidden:NO];
|
||||
}
|
||||
else {
|
||||
[_nextPageButton setHidden:YES];
|
||||
[_prevPageButton setHidden:YES];
|
||||
}
|
||||
|
||||
frameRect = [[self window] frame];
|
||||
NSPoint 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:NO];
|
||||
[_candidateView setNeedsDisplay:YES];
|
||||
|
||||
}
|
||||
|
||||
- (void)pageButtonAction:(id)sender
|
||||
{
|
||||
if (sender == _nextPageButton) {
|
||||
[self showNextPage];
|
||||
}
|
||||
else if (sender == _prevPageButton) {
|
||||
[self showPreviousPage];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)candidateViewMouseDidClick:(id)sender
|
||||
{
|
||||
[_delegate candidateController:self didSelectCandidateAtIndex:self.selectedCandidateIndex];
|
||||
}
|
||||
@end
|
|
@ -1,59 +0,0 @@
|
|||
//
|
||||
// VTHorizontalCandidateView.h
|
||||
//
|
||||
// Copyright (c) 2021-2022 The vChewing Project.
|
||||
// Copyright (c) 2011-2022 The OpenVanilla Project.
|
||||
//
|
||||
// Contributors:
|
||||
// Lukhnos Liu (@lukhnos) @ OpenVanilla
|
||||
// Shiki Suen (@ShikiSuen) @ vChewing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface VTHorizontalCandidateView : NSView
|
||||
{
|
||||
@protected
|
||||
NSArray *_keyLabels;
|
||||
NSArray *_displayedCandidates;
|
||||
CGFloat _keyLabelHeight;
|
||||
CGFloat _candidateTextHeight;
|
||||
CGFloat _cellPadding;
|
||||
NSDictionary *_keyLabelAttrDict;
|
||||
NSDictionary *_candidateAttrDict;
|
||||
NSArray *_elementWidths;
|
||||
NSUInteger _highlightedIndex;
|
||||
NSUInteger _trackingHighlightedIndex;
|
||||
SEL _action;
|
||||
__weak id _target;
|
||||
}
|
||||
|
||||
- (void)setKeyLabels:(NSArray *)labels displayedCandidates:(NSArray *)candidates;
|
||||
- (void)setKeyLabelFont:(NSFont *)labelFont candidateFont:(NSFont *)candidateFont;
|
||||
|
||||
@property (readonly, nonatomic) NSSize sizeForView;
|
||||
@property (assign, nonatomic) NSUInteger highlightedIndex;
|
||||
@property (assign, nonatomic) SEL action;
|
||||
@property (weak, nonatomic) id target;
|
||||
@end
|
|
@ -1,239 +0,0 @@
|
|||
//
|
||||
// VTHorizontalCandidateView.m
|
||||
//
|
||||
// Copyright (c) 2021-2022 The vChewing Project.
|
||||
// Copyright (c) 2011-2022 The OpenVanilla Project.
|
||||
//
|
||||
// Contributors:
|
||||
// Lukhnos Liu (@lukhnos) @ OpenVanilla
|
||||
// Shiki Suen (@ShikiSuen) @ vChewing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import "VTHorizontalCandidateView.h"
|
||||
|
||||
// use these instead of MIN/MAX macro to keep compilers happy with pedantic warnings on
|
||||
NS_INLINE CGFloat min(CGFloat a, CGFloat b) { return a < b ? a : b; }
|
||||
NS_INLINE CGFloat max(CGFloat a, CGFloat b) { return a > b ? a : b; }
|
||||
|
||||
@implementation VTHorizontalCandidateView
|
||||
|
||||
@synthesize highlightedIndex = _highlightedIndex;
|
||||
@synthesize action = _action;
|
||||
@synthesize target = _target;
|
||||
|
||||
static NSColor *colorFromRGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a)
|
||||
{
|
||||
return [NSColor colorWithDeviceRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:(a/255.0f)];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
_keyLabels = nil;
|
||||
_displayedCandidates = nil;
|
||||
_keyLabelAttrDict = nil;
|
||||
_candidateAttrDict = nil;
|
||||
_elementWidths = nil;
|
||||
}
|
||||
|
||||
- (void)setKeyLabels:(NSArray *)labels displayedCandidates:(NSArray *)candidates
|
||||
{
|
||||
NSUInteger count = min([labels count], [candidates count]);
|
||||
_keyLabels = [labels subarrayWithRange:NSMakeRange(0, count)];
|
||||
_displayedCandidates = [candidates subarrayWithRange:NSMakeRange(0, count)];
|
||||
|
||||
NSMutableArray *newWidths = [NSMutableArray array];
|
||||
|
||||
NSSize baseSize = NSMakeSize(10240.0, 10240.0);
|
||||
for (NSUInteger index = 0; index < count; index++) {
|
||||
NSRect labelRect = [[_keyLabels objectAtIndex:index] boundingRectWithSize:baseSize options:NSStringDrawingUsesLineFragmentOrigin attributes:_keyLabelAttrDict];
|
||||
|
||||
NSRect candidateRect = [[_displayedCandidates objectAtIndex:index] boundingRectWithSize:baseSize options:NSStringDrawingUsesLineFragmentOrigin attributes:_candidateAttrDict];
|
||||
|
||||
CGFloat width = max(labelRect.size.width, candidateRect.size.width) + _cellPadding;
|
||||
[newWidths addObject:[NSNumber numberWithDouble:width]];
|
||||
}
|
||||
|
||||
_elementWidths = newWidths;
|
||||
}
|
||||
|
||||
- (void)setKeyLabelFont:(NSFont *)labelFont candidateFont:(NSFont *)candidateFont
|
||||
{
|
||||
NSColor *clrCandidateText = colorFromRGBA(233,233,233,255);
|
||||
NSColor *clrCandidateTextIndex = colorFromRGBA(233,233,233,213);
|
||||
NSMutableParagraphStyle *paraStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
|
||||
[paraStyle setAlignment:NSCenterTextAlignment];
|
||||
|
||||
_keyLabelAttrDict = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
labelFont, NSFontAttributeName,
|
||||
paraStyle, NSParagraphStyleAttributeName,
|
||||
clrCandidateTextIndex, NSForegroundColorAttributeName,
|
||||
nil];
|
||||
_candidateAttrDict = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
candidateFont, NSFontAttributeName,
|
||||
paraStyle, NSParagraphStyleAttributeName,
|
||||
clrCandidateText, NSForegroundColorAttributeName,
|
||||
nil];
|
||||
|
||||
CGFloat labelFontSize = [labelFont pointSize];
|
||||
CGFloat candidateFontSize = [candidateFont pointSize];
|
||||
CGFloat biggestSize = max(labelFontSize, candidateFontSize);
|
||||
|
||||
_keyLabelHeight = ceil(labelFontSize * 1.20);
|
||||
_candidateTextHeight = ceil(candidateFontSize * 1.20);
|
||||
_cellPadding = ceil(biggestSize / 2.0);
|
||||
}
|
||||
|
||||
|
||||
- (NSSize)sizeForView
|
||||
{
|
||||
NSSize result = NSMakeSize(0.0, 0.0);
|
||||
if ([_elementWidths count]) {
|
||||
for (NSNumber *w in _elementWidths) {
|
||||
result.width += [w doubleValue];
|
||||
}
|
||||
|
||||
result.width += [_elementWidths count];
|
||||
result.height = _keyLabelHeight + _candidateTextHeight + 1.0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)isFlipped
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect
|
||||
{
|
||||
NSColor *clrCandidateSelectedBG = [NSColor alternateSelectedControlColor];
|
||||
NSColor *clrCandidateSelectedText = colorFromRGBA(233,233,233,255);
|
||||
NSColor *clrCandidateWindowBorder = colorFromRGBA(255,255,255,75);
|
||||
NSColor *clrCandidateWindowBG = colorFromRGBA(28,28,28,255);
|
||||
NSColor *clrCandidateBG = colorFromRGBA(28,28,28,255);
|
||||
|
||||
[self setWantsLayer: YES];
|
||||
[self.layer setBorderColor: [clrCandidateWindowBorder CGColor]];
|
||||
[self.layer setBorderWidth: 1];
|
||||
[self.layer setCornerRadius: 6];
|
||||
|
||||
NSRect bounds = [self bounds];
|
||||
|
||||
[clrCandidateWindowBG setFill];
|
||||
[NSBezierPath fillRect:bounds];
|
||||
|
||||
NSUInteger count = [_elementWidths count];
|
||||
CGFloat accuWidth = 0.0;
|
||||
|
||||
for (NSUInteger index = 0; index < count; index++) {
|
||||
NSDictionary *activeCandidateAttr = _candidateAttrDict;
|
||||
CGFloat currentWidth = [[_elementWidths objectAtIndex:index] doubleValue];
|
||||
NSRect labelRect = NSMakeRect(accuWidth, 0.0, currentWidth + 1.0, _keyLabelHeight + 1.0);
|
||||
NSRect candidateRect = NSMakeRect(accuWidth, _keyLabelHeight + 1.0, currentWidth + 1.0, _candidateTextHeight);
|
||||
|
||||
if (index == _highlightedIndex) {
|
||||
[clrCandidateSelectedBG setFill];
|
||||
}
|
||||
else {
|
||||
[clrCandidateBG setFill];
|
||||
}
|
||||
|
||||
[NSBezierPath fillRect:labelRect];
|
||||
[[_keyLabels objectAtIndex:index] drawInRect:labelRect withAttributes:_keyLabelAttrDict];
|
||||
|
||||
if (index == _highlightedIndex) {
|
||||
[clrCandidateSelectedBG setFill];
|
||||
activeCandidateAttr = [_candidateAttrDict mutableCopy];
|
||||
[(NSMutableDictionary *)activeCandidateAttr setObject:clrCandidateSelectedText forKey:NSForegroundColorAttributeName];
|
||||
}
|
||||
else {
|
||||
[clrCandidateBG setFill];
|
||||
}
|
||||
|
||||
[NSBezierPath fillRect:candidateRect];
|
||||
[[_displayedCandidates objectAtIndex:index] drawInRect:candidateRect withAttributes:activeCandidateAttr];
|
||||
|
||||
accuWidth += currentWidth + 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSUInteger)findHitIndex:(NSEvent *)theEvent
|
||||
{
|
||||
NSUInteger result = NSUIntegerMax;
|
||||
|
||||
NSPoint location = [self convertPoint:[theEvent locationInWindow] toView:nil];
|
||||
if (!NSPointInRect(location, [self bounds])) {
|
||||
return result;
|
||||
}
|
||||
|
||||
NSUInteger count = [_elementWidths count];
|
||||
CGFloat accuWidth = 0.0;
|
||||
for (NSUInteger index = 0; index < count; index++) {
|
||||
CGFloat currentWidth = [[_elementWidths objectAtIndex:index] doubleValue];
|
||||
|
||||
if (location.x >= accuWidth && location.x <= accuWidth + currentWidth) {
|
||||
result = index;
|
||||
break;
|
||||
}
|
||||
accuWidth += currentWidth;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)mouseDown:(NSEvent *)theEvent
|
||||
{
|
||||
NSUInteger newIndex = [self findHitIndex:theEvent];
|
||||
_trackingHighlightedIndex = _highlightedIndex;
|
||||
|
||||
if (newIndex != NSUIntegerMax) {
|
||||
_highlightedIndex = newIndex;
|
||||
[self setNeedsDisplay:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseUp:(NSEvent *)theEvent
|
||||
{
|
||||
NSUInteger newIndex = [self findHitIndex:theEvent];
|
||||
BOOL triggerAction = NO;
|
||||
|
||||
if (newIndex == _highlightedIndex) {
|
||||
triggerAction = YES;
|
||||
}
|
||||
else {
|
||||
_highlightedIndex = _trackingHighlightedIndex;
|
||||
}
|
||||
|
||||
_trackingHighlightedIndex = 0;
|
||||
[self setNeedsDisplay:YES];
|
||||
|
||||
# pragma clang diagnostic push
|
||||
# pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
if (triggerAction && _target && _action) {
|
||||
[_target performSelector:_action withObject:self];
|
||||
}
|
||||
# pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
@end
|
|
@ -1,41 +0,0 @@
|
|||
//
|
||||
// VTVerticalCandidateController.h
|
||||
//
|
||||
// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import "VTCandidateController.h"
|
||||
|
||||
@class VTVerticalKeyLabelStripView;
|
||||
|
||||
@interface VTVerticalCandidateController : VTCandidateController
|
||||
{
|
||||
@protected
|
||||
VTVerticalKeyLabelStripView *_keyLabelStripView;
|
||||
NSScrollView *_scrollView;
|
||||
NSTableView *_tableView;
|
||||
NSMutableParagraphStyle *_candidateTextParagraphStyle;
|
||||
CGFloat _maxCandidateAttrStringWidth;
|
||||
}
|
||||
@end
|
|
@ -1,469 +0,0 @@
|
|||
//
|
||||
// VTVerticalCandidateController.m
|
||||
//
|
||||
// Copyright (c) 2021-2022 The vChewing Project.
|
||||
// Copyright (c) 2011-2022 The OpenVanilla Project.
|
||||
//
|
||||
// Contributors:
|
||||
// Lukhnos Liu (@lukhnos) @ OpenVanilla
|
||||
// Shiki Suen (ShikiSuen) @ vChewing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import "VTVerticalCandidateController.h"
|
||||
#import "VTVerticalKeyLabelStripView.h"
|
||||
#import "VTVerticalCandidateTableView.h"
|
||||
|
||||
// use these instead of MIN/MAX macro to keep compilers happy with pedantic warnings on
|
||||
NS_INLINE CGFloat min(CGFloat a, CGFloat b) { return a < b ? a : b; }
|
||||
NS_INLINE CGFloat max(CGFloat a, CGFloat b) { return a > b ? a : b; }
|
||||
|
||||
static const CGFloat kCandidateTextPadding = 24.0;
|
||||
static const CGFloat kCandidateTextLeftMargin = 8.0;
|
||||
|
||||
static NSColor *colorFromRGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a)
|
||||
{
|
||||
return [NSColor colorWithDeviceRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:(a/255.0f)];
|
||||
}
|
||||
|
||||
#if defined(__MAC_11_0)
|
||||
static const CGFloat kCandidateTextPaddingWithMandatedTableViewPadding = 18.0;
|
||||
static const CGFloat kCandidateTextLeftMarginWithMandatedTableViewPadding = 0.0;
|
||||
#endif
|
||||
|
||||
@interface VTVerticalCandidateController (Private) <NSTableViewDataSource, NSTableViewDelegate>
|
||||
- (void)rowDoubleClicked:(id)sender;
|
||||
- (BOOL)scrollPageByOne:(BOOL)forward;
|
||||
- (BOOL)moveSelectionByOne:(BOOL)forward;
|
||||
- (void)layoutCandidateView;
|
||||
@end
|
||||
|
||||
@implementation VTVerticalCandidateController
|
||||
{
|
||||
// Total padding added to the left and the right of the table view cell text.
|
||||
CGFloat _candidateTextPadding;
|
||||
|
||||
// The indent of the table view cell text from the left.
|
||||
CGFloat _candidateTextLeftMargin;
|
||||
}
|
||||
|
||||
|
||||
- (id)init
|
||||
{
|
||||
// NSColor *clrCandidateSelectedBG = [NSColor systemBlueColor];
|
||||
NSColor *clrCandidateSelectedText = [[NSColor whiteColor] colorWithAlphaComponent: 0.8];
|
||||
NSColor *clrCandidateWindowBorder = colorFromRGBA(255,255,255,75);
|
||||
NSColor *clrCandidateWindowBG = colorFromRGBA(28,28,28,255);
|
||||
// NSColor *clrCandidateBG = colorFromRGBA(28,28,28,255);
|
||||
|
||||
NSRect contentRect = NSMakeRect(128.0, 128.0, 0.0, 0.0);
|
||||
NSUInteger styleMask = NSBorderlessWindowMask | NSNonactivatingPanelMask;
|
||||
NSView *panelView = [[NSView alloc] initWithFrame:contentRect];
|
||||
NSWindow *panel = [[NSWindow alloc] initWithContentRect:contentRect styleMask:styleMask backing:NSBackingStoreBuffered defer:NO];
|
||||
[panel setLevel:kCGPopUpMenuWindowLevel];
|
||||
[panel setContentView: panelView];
|
||||
[panel setHasShadow:YES];
|
||||
[panel setOpaque:NO];
|
||||
[panel setBackgroundColor: [NSColor clearColor]];
|
||||
[panel setOpaque:false];
|
||||
[panelView setWantsLayer: YES];
|
||||
[panelView.layer setBorderColor: [clrCandidateWindowBorder CGColor]];
|
||||
[panelView.layer setBorderWidth: 1];
|
||||
[panelView.layer setCornerRadius: 6];
|
||||
[panelView.layer setBackgroundColor: [clrCandidateWindowBG CGColor]];
|
||||
|
||||
self = [self initWithWindow:panel];
|
||||
if (self) {
|
||||
contentRect.origin = NSMakePoint(0.0, 0.0);
|
||||
|
||||
NSRect stripRect = contentRect;
|
||||
stripRect.size.width = 10.0;
|
||||
_keyLabelStripView = [[VTVerticalKeyLabelStripView alloc] initWithFrame:stripRect];
|
||||
[_keyLabelStripView setWantsLayer: YES];
|
||||
[_keyLabelStripView.layer setBorderWidth: 0];
|
||||
|
||||
[[panel contentView] addSubview:_keyLabelStripView];
|
||||
|
||||
NSRect scrollViewRect = contentRect;
|
||||
scrollViewRect.origin.x = stripRect.size.width;
|
||||
scrollViewRect.size.width -= stripRect.size.width;
|
||||
|
||||
_scrollView = [[NSScrollView alloc] initWithFrame:scrollViewRect];
|
||||
[_scrollView setAutohidesScrollers: YES];
|
||||
[_scrollView setWantsLayer: YES];
|
||||
[_scrollView.layer setBorderWidth: 0];
|
||||
[_scrollView setDrawsBackground:NO];
|
||||
|
||||
// >=10.7 only, elastic scroll causes some drawing issues with visible scroller, so we disable it
|
||||
if ([_scrollView respondsToSelector:@selector(setVerticalScrollElasticity:)]) {
|
||||
[_scrollView setVerticalScrollElasticity:NSScrollElasticityNone];
|
||||
}
|
||||
|
||||
_tableView = [[VTVerticalCandidateTableView alloc] initWithFrame:contentRect];
|
||||
[_tableView setDataSource:self];
|
||||
[_tableView setDelegate:self];
|
||||
|
||||
NSTableColumn *column = [[NSTableColumn alloc] initWithIdentifier:@"candidate"];
|
||||
[column setDataCell:[[NSTextFieldCell alloc] init]];
|
||||
[column setEditable:NO];
|
||||
[column.dataCell setTextColor: clrCandidateSelectedText];
|
||||
// [column.dataCell setSelectionColor: clrCandidateSelectedBG];
|
||||
|
||||
_candidateTextPadding = kCandidateTextPadding;
|
||||
_candidateTextLeftMargin = kCandidateTextLeftMargin;
|
||||
|
||||
[_tableView addTableColumn:column];
|
||||
[_tableView setIntercellSpacing:NSMakeSize(0.0, 1.0)];
|
||||
[_tableView setHeaderView:nil];
|
||||
[_tableView setAllowsMultipleSelection:NO];
|
||||
[_tableView setAllowsEmptySelection:YES];
|
||||
[_tableView setDoubleAction:@selector(rowDoubleClicked:)];
|
||||
[_tableView setTarget:self];
|
||||
[_tableView setBackgroundColor:[NSColor clearColor]];
|
||||
[_tableView setGridColor:[NSColor clearColor]];
|
||||
|
||||
#if defined(__MAC_11_0)
|
||||
if (@available(macOS 11.0, *)) {
|
||||
[_tableView setStyle:NSTableViewStyleFullWidth];
|
||||
_candidateTextPadding = kCandidateTextPaddingWithMandatedTableViewPadding;
|
||||
_candidateTextLeftMargin = kCandidateTextLeftMarginWithMandatedTableViewPadding;
|
||||
}
|
||||
#endif
|
||||
|
||||
[_scrollView setDocumentView:_tableView];
|
||||
[[panel contentView] addSubview:_scrollView];
|
||||
|
||||
_candidateTextParagraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
|
||||
[_candidateTextParagraphStyle setFirstLineHeadIndent:_candidateTextLeftMargin];
|
||||
[_candidateTextParagraphStyle setLineBreakMode:NSLineBreakByClipping];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)reloadData
|
||||
{
|
||||
_maxCandidateAttrStringWidth = ceil([_candidateFont pointSize] * 2.0 + _candidateTextPadding);
|
||||
|
||||
[_tableView reloadData];
|
||||
[self layoutCandidateView];
|
||||
|
||||
if ([_delegate candidateCountForController:self]) {
|
||||
self.selectedCandidateIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)showNextPage
|
||||
{
|
||||
return [self scrollPageByOne:YES];
|
||||
}
|
||||
|
||||
- (BOOL)showPreviousPage
|
||||
{
|
||||
return [self scrollPageByOne:NO];
|
||||
}
|
||||
|
||||
- (BOOL)highlightNextCandidate
|
||||
{
|
||||
return [self moveSelectionByOne:YES];
|
||||
}
|
||||
|
||||
- (BOOL)highlightPreviousCandidate
|
||||
{
|
||||
return [self moveSelectionByOne:NO];
|
||||
}
|
||||
|
||||
- (NSUInteger)candidateIndexAtKeyLabelIndex:(NSUInteger)index
|
||||
{
|
||||
NSInteger firstVisibleRow = [_tableView rowAtPoint:[_scrollView documentVisibleRect].origin];
|
||||
if (firstVisibleRow != -1) {
|
||||
NSUInteger result = firstVisibleRow + index;
|
||||
if (result < [_delegate candidateCountForController:self]) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return NSUIntegerMax;
|
||||
}
|
||||
|
||||
- (NSUInteger)selectedCandidateIndex
|
||||
{
|
||||
NSInteger selectedRow = [_tableView selectedRow];
|
||||
return (selectedRow == -1) ? NSUIntegerMax : selectedRow;
|
||||
}
|
||||
|
||||
- (void)setSelectedCandidateIndex:(NSUInteger)aNewIndex
|
||||
{
|
||||
NSUInteger newIndex = aNewIndex;
|
||||
|
||||
NSInteger selectedRow = [_tableView selectedRow];
|
||||
|
||||
NSUInteger labelCount = [_keyLabels count];
|
||||
NSUInteger itemCount = [_delegate candidateCountForController:self];
|
||||
|
||||
if (newIndex == NSUIntegerMax) {
|
||||
if (itemCount == 0) {
|
||||
[_tableView deselectAll:self];
|
||||
return;
|
||||
}
|
||||
newIndex = 0;
|
||||
}
|
||||
|
||||
NSUInteger lastVisibleRow = newIndex;
|
||||
if (selectedRow != -1 && itemCount > 0 && itemCount > labelCount) {
|
||||
if (newIndex > selectedRow && (newIndex - selectedRow) > 1) {
|
||||
lastVisibleRow = min(newIndex + labelCount - 1, itemCount - 1);
|
||||
}
|
||||
|
||||
// no need to handle the backward case: (newIndex < selectedRow && selectedRow - newIndex > 1)
|
||||
}
|
||||
|
||||
if (itemCount > labelCount) {
|
||||
[_tableView scrollRowToVisible:lastVisibleRow];
|
||||
}
|
||||
|
||||
[_tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:newIndex] byExtendingSelection:NO];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation VTVerticalCandidateController (Private)
|
||||
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
|
||||
{
|
||||
return [_delegate candidateCountForController:self];
|
||||
}
|
||||
|
||||
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
|
||||
{
|
||||
NSString *candidate = @"";
|
||||
|
||||
// rendering can occur when the delegate is already gone or data goes stale; in that case we ignore it
|
||||
|
||||
if (row < [_delegate candidateCountForController:self]) {
|
||||
candidate = [_delegate candidateController:self candidateAtIndex:row];
|
||||
}
|
||||
|
||||
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:candidate attributes:[NSDictionary dictionaryWithObjectsAndKeys:_candidateFont, NSFontAttributeName, _candidateTextParagraphStyle, NSParagraphStyleAttributeName, nil]];
|
||||
|
||||
// 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
|
||||
NSRect boundingRect = [attrString boundingRectWithSize:NSMakeSize(10240.0, 10240.0) options:NSStringDrawingUsesLineFragmentOrigin];
|
||||
CGFloat textWidth = boundingRect.size.width + _candidateTextPadding;
|
||||
if (textWidth > _maxCandidateAttrStringWidth) {
|
||||
_maxCandidateAttrStringWidth = textWidth;
|
||||
[self layoutCandidateView];
|
||||
}
|
||||
|
||||
// keep track of the highlighted index in the key label strip
|
||||
NSUInteger count = [_keyLabels count];
|
||||
NSInteger selectedRow = [_tableView selectedRow];
|
||||
if (selectedRow != -1) {
|
||||
// cast this into signed integer to make our life easier
|
||||
NSInteger newHilightIndex;
|
||||
|
||||
if (_keyLabelStripView.highlightedIndex != -1 && (row >= selectedRow + count || (selectedRow > count && row <= selectedRow - count))) {
|
||||
newHilightIndex = -1;
|
||||
}
|
||||
else {
|
||||
NSInteger firstVisibleRow = [_tableView rowAtPoint:[_scrollView documentVisibleRect].origin];
|
||||
|
||||
newHilightIndex = selectedRow - firstVisibleRow;
|
||||
if (newHilightIndex < -1) {
|
||||
newHilightIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (newHilightIndex != _keyLabelStripView.highlightedIndex && newHilightIndex >= 0) {
|
||||
_keyLabelStripView.highlightedIndex = newHilightIndex;
|
||||
[_keyLabelStripView setNeedsDisplay:YES];
|
||||
}
|
||||
}
|
||||
|
||||
return attrString;
|
||||
}
|
||||
|
||||
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification
|
||||
{
|
||||
NSInteger selectedRow = [_tableView selectedRow];
|
||||
if (selectedRow != -1) {
|
||||
// keep track of the highlighted index in the key label strip
|
||||
NSInteger firstVisibleRow = [_tableView rowAtPoint:[_scrollView documentVisibleRect].origin];
|
||||
_keyLabelStripView.highlightedIndex = selectedRow - firstVisibleRow;
|
||||
[_keyLabelStripView setNeedsDisplay:YES];
|
||||
|
||||
// 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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)rowDoubleClicked:(id)sender
|
||||
{
|
||||
NSInteger clickedRow = [_tableView clickedRow];
|
||||
if (clickedRow != -1) {
|
||||
[_delegate candidateController:self didSelectCandidateAtIndex:clickedRow];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)scrollPageByOne:(BOOL)forward
|
||||
{
|
||||
NSUInteger labelCount = [_keyLabels count];
|
||||
NSUInteger itemCount = [_delegate candidateCountForController:self];
|
||||
|
||||
if (0 == itemCount) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (itemCount <= labelCount) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSUInteger newIndex = self.selectedCandidateIndex;
|
||||
|
||||
if (forward) {
|
||||
if (newIndex == itemCount - 1) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
newIndex = min(newIndex + labelCount, itemCount - 1);
|
||||
}
|
||||
else {
|
||||
if (newIndex == 0) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (newIndex < labelCount) {
|
||||
newIndex = 0;
|
||||
}
|
||||
else {
|
||||
newIndex -= labelCount;
|
||||
}
|
||||
}
|
||||
|
||||
self.selectedCandidateIndex = newIndex;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)moveSelectionByOne:(BOOL)forward
|
||||
{
|
||||
NSUInteger itemCount = [_delegate candidateCountForController:self];
|
||||
|
||||
if (0 == itemCount) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSUInteger newIndex = self.selectedCandidateIndex;
|
||||
|
||||
if (forward) {
|
||||
if (newIndex == itemCount - 1) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
newIndex++;
|
||||
}
|
||||
else {
|
||||
if (0 == newIndex) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
newIndex--;
|
||||
}
|
||||
|
||||
self.selectedCandidateIndex = newIndex;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)layoutCandidateView
|
||||
{
|
||||
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(doLayoutCanaditeView) object:nil];
|
||||
[self performSelector:@selector(doLayoutCanaditeView) withObject:nil afterDelay:0.0];
|
||||
}
|
||||
|
||||
- (void)doLayoutCanaditeView
|
||||
{
|
||||
NSUInteger count = [_delegate candidateCountForController:self];
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat candidateFontSize = ceil([_candidateFont pointSize]);
|
||||
CGFloat keyLabelFontSize = ceil([_keyLabelFont pointSize]);
|
||||
CGFloat fontSize = max(candidateFontSize, keyLabelFontSize);
|
||||
|
||||
NSControlSize controlSize = (fontSize > 36.0) ? NSRegularControlSize : NSSmallControlSize;
|
||||
|
||||
NSUInteger keyLabelCount = [_keyLabels count];
|
||||
CGFloat scrollerWidth = 0.0;
|
||||
if (count <= keyLabelCount) {
|
||||
keyLabelCount = count;
|
||||
[_scrollView setHasVerticalScroller:NO];
|
||||
}
|
||||
else {
|
||||
[_scrollView setHasVerticalScroller:YES];
|
||||
|
||||
NSScroller *verticalScroller = [_scrollView verticalScroller];
|
||||
[verticalScroller setControlSize:controlSize];
|
||||
[verticalScroller setScrollerStyle:NSScrollerStyleOverlay];
|
||||
scrollerWidth = [NSScroller scrollerWidthForControlSize:controlSize scrollerStyle:NSScrollerStyleOverlay];
|
||||
}
|
||||
|
||||
_keyLabelStripView.keyLabelFont = _keyLabelFont;
|
||||
_keyLabelStripView.keyLabels = [_keyLabels subarrayWithRange:NSMakeRange(0, keyLabelCount)];
|
||||
_keyLabelStripView.labelOffsetY = (keyLabelFontSize >= candidateFontSize) ? 0.0 : floor((candidateFontSize - keyLabelFontSize) / 2.0);
|
||||
|
||||
CGFloat rowHeight = ceil(fontSize * 1.25);
|
||||
[_tableView setRowHeight:rowHeight];
|
||||
|
||||
CGFloat maxKeyLabelWidth = keyLabelFontSize;
|
||||
NSDictionary *textAttr = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
_keyLabelFont, NSFontAttributeName,
|
||||
nil];
|
||||
NSSize boundingBox = NSMakeSize(1600.0, 1600.0);
|
||||
for (NSString *label in _keyLabels) {
|
||||
NSRect rect = [label boundingRectWithSize:boundingBox options:NSStringDrawingUsesLineFragmentOrigin attributes:textAttr];
|
||||
maxKeyLabelWidth = max(rect.size.width, maxKeyLabelWidth);
|
||||
}
|
||||
|
||||
CGFloat rowSpacing = [_tableView intercellSpacing].height;
|
||||
CGFloat stripWidth = ceil(maxKeyLabelWidth);
|
||||
CGFloat tableViewStartWidth = ceil(_maxCandidateAttrStringWidth + scrollerWidth);;
|
||||
CGFloat windowWidth = stripWidth + tableViewStartWidth;
|
||||
CGFloat windowHeight = keyLabelCount * (rowHeight + rowSpacing);
|
||||
|
||||
NSRect frameRect = [[self window] frame];
|
||||
NSPoint 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 setFrame:NSMakeRect(0.0, 0.0, stripWidth, windowHeight)];
|
||||
[_scrollView setFrame:NSMakeRect(stripWidth, 0.0, tableViewStartWidth, windowHeight)];
|
||||
[[self window] setFrame:frameRect display:NO];
|
||||
}
|
||||
@end
|
|
@ -1,31 +0,0 @@
|
|||
//
|
||||
// VTVerticalCandidateTableView.h
|
||||
//
|
||||
// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface VTVerticalCandidateTableView : NSTableView
|
||||
@end
|
|
@ -1,38 +0,0 @@
|
|||
//
|
||||
// VTVerticalCandidateTableView.m
|
||||
//
|
||||
// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import "VTVerticalCandidateTableView.h"
|
||||
|
||||
@implementation VTVerticalCandidateTableView
|
||||
- (NSRect)adjustScroll:(NSRect)newVisible
|
||||
{
|
||||
NSRect scrollRect = newVisible;
|
||||
CGFloat rowHeightPlusSpacing = [self rowHeight] + [self intercellSpacing].height;
|
||||
scrollRect.origin.y = (NSInteger)(scrollRect.origin.y / rowHeightPlusSpacing) * rowHeightPlusSpacing;
|
||||
return scrollRect;
|
||||
}
|
||||
@end
|
|
@ -1,43 +0,0 @@
|
|||
//
|
||||
// VTVerticalKeyLabelStripView.m
|
||||
//
|
||||
// Copyright (c) 2012 Lukhnos D. Liu (http://lukhnos.org)
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface VTVerticalKeyLabelStripView : NSView
|
||||
{
|
||||
@protected
|
||||
NSFont *_keyLabelFont;
|
||||
CGFloat _labelOffsetY;
|
||||
NSArray *_keyLabels;
|
||||
NSInteger _highlightedIndex;
|
||||
}
|
||||
|
||||
@property (retain, nonatomic) NSFont *keyLabelFont;
|
||||
@property (assign, nonatomic) CGFloat labelOffsetY;
|
||||
@property (retain, nonatomic) NSArray *keyLabels;
|
||||
@property (assign, nonatomic) NSInteger highlightedIndex;
|
||||
@end
|
|
@ -1,114 +0,0 @@
|
|||
//
|
||||
// VTVerticalKeyLabelStripView.h
|
||||
//
|
||||
// Copyright (c) 2021-2022 The vChewing Project.
|
||||
// Copyright (c) 2011-2022 The OpenVanilla Project.
|
||||
//
|
||||
// Contributors:
|
||||
// Lukhnos Liu (@lukhnos) @ OpenVanilla
|
||||
// Shiki Suen (ShikiSuen) @ vChewing
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
#import "VTVerticalKeyLabelStripView.h"
|
||||
|
||||
@implementation VTVerticalKeyLabelStripView
|
||||
@synthesize keyLabelFont = _keyLabelFont;
|
||||
@synthesize labelOffsetY = _labelOffsetY;
|
||||
@synthesize keyLabels = _keyLabels;
|
||||
@synthesize highlightedIndex = _highlightedIndex;
|
||||
|
||||
static NSColor *colorFromRGBA(unsigned char r, unsigned char g, unsigned char b, unsigned char a)
|
||||
{
|
||||
return [NSColor colorWithDeviceRed:(r/255.0f) green:(g/255.0f) blue:(b/255.0f) alpha:(a/255.0f)];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
_keyLabelFont = nil;
|
||||
_keyLabels = nil;
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(NSRect)frameRect
|
||||
{
|
||||
self = [super initWithFrame:frameRect];
|
||||
if (self) {
|
||||
_keyLabelFont = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isFlipped
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect
|
||||
{
|
||||
NSRect bounds = [self bounds];
|
||||
[[NSColor clearColor] setFill];
|
||||
[NSBezierPath fillRect:bounds];
|
||||
|
||||
NSUInteger count = [_keyLabels count];
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGFloat cellHeight = bounds.size.height / count;
|
||||
NSColor *clrCandidateBG = colorFromRGBA(28,28,28,255);
|
||||
NSColor *clrCandidateTextIndex = colorFromRGBA(233,233,233,213);
|
||||
NSColor *clrCandidateSelectedBG = [NSColor alternateSelectedControlColor];
|
||||
|
||||
NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
|
||||
[style setAlignment:NSCenterTextAlignment];
|
||||
|
||||
NSDictionary *textAttr = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
_keyLabelFont, NSFontAttributeName,
|
||||
clrCandidateTextIndex, NSForegroundColorAttributeName,
|
||||
style, NSParagraphStyleAttributeName,
|
||||
nil];
|
||||
|
||||
for (NSUInteger index = 0; index < count; index++) {
|
||||
NSRect textRect = NSMakeRect(0.0, index * cellHeight + _labelOffsetY, bounds.size.width, cellHeight - _labelOffsetY);
|
||||
NSRect cellRect = NSMakeRect(0.0, index * cellHeight, bounds.size.width, cellHeight);
|
||||
|
||||
// fill in the last cell
|
||||
if (index + 1 >= count) {
|
||||
cellRect.size.height += 1.0;
|
||||
}
|
||||
|
||||
if (index == _highlightedIndex) {
|
||||
[clrCandidateSelectedBG setFill];
|
||||
}
|
||||
else {
|
||||
[clrCandidateBG setFill];
|
||||
}
|
||||
|
||||
[NSBezierPath fillRect:cellRect];
|
||||
|
||||
NSString *text = [_keyLabels objectAtIndex:index];
|
||||
[text drawInRect:textRect withAttributes:textAttr];
|
||||
}
|
||||
}
|
||||
@end
|
|
@ -0,0 +1,455 @@
|
|||
//
|
||||
// VerticalCandidateController.swift
|
||||
//
|
||||
// Copyright (c) 2011 The McBopomofo Project.
|
||||
//
|
||||
// Contributors:
|
||||
// Mengjuei Hsieh (@mjhsieh)
|
||||
// Weizhong Yang (@zonble)
|
||||
//
|
||||
// Based on the Syrup Project and the Formosana Library
|
||||
// by Lukhnos Liu (@lukhnos).
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person
|
||||
// obtaining a copy of this software and associated documentation
|
||||
// files (the "Software"), to deal in the Software without
|
||||
// restriction, including without limitation the rights to use,
|
||||
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the
|
||||
// Software is furnished to do so, subject to the following
|
||||
// conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be
|
||||
// included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
// OTHER DEALINGS IN THE SOFTWARE.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
fileprivate class VerticalKeyLabelStripView: NSView {
|
||||
var keyLabelFont: NSFont = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
|
||||
var labelOffsetY: CGFloat = 0
|
||||
var keyLabels: [String] = []
|
||||
var highlightedIndex: UInt = UInt.max
|
||||
|
||||
override var isFlipped: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
let bounds = self.bounds
|
||||
NSColor.white.setFill()
|
||||
NSBezierPath.fill(bounds)
|
||||
|
||||
let count = UInt(keyLabels.count)
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
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)
|
||||
|
||||
let paraStyle = NSMutableParagraphStyle()
|
||||
paraStyle.setParagraphStyle(NSParagraphStyle.default)
|
||||
paraStyle.alignment = .center
|
||||
|
||||
let textAttr: [NSAttributedString.Key: AnyObject] = [
|
||||
.font: keyLabelFont,
|
||||
.foregroundColor: black,
|
||||
.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 {
|
||||
cellRect.size.height += 1.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 = 24.0
|
||||
private let kCandidateTextLeftMargin = 8.0
|
||||
private let kCandidateTextPaddingWithMandatedTableViewPadding = 18.0
|
||||
private let kCandidateTextLeftMarginWithMandatedTableViewPadding = 0.0
|
||||
|
||||
|
||||
@objc (VTVerticalCandidateController)
|
||||
public class VerticalCandidateController: CandidateController {
|
||||
private var keyLabelStripView: VerticalKeyLabelStripView
|
||||
private var scrollView: NSScrollView
|
||||
private var tableView: NSTableView
|
||||
private var candidateTextParagraphStyle: NSMutableParagraphStyle
|
||||
private var candidateTextPadding: CGFloat = kCandidateTextPadding
|
||||
private var candidateTextLeftMargin: CGFloat = kCandidateTextLeftMargin
|
||||
private var maxCandidateAttrStringWidth: CGFloat = 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)
|
||||
panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1)
|
||||
panel.hasShadow = true
|
||||
|
||||
contentRect.origin = NSPoint.zero
|
||||
var stripRect = contentRect
|
||||
stripRect.size.width = 10.0
|
||||
keyLabelStripView = VerticalKeyLabelStripView(frame: stripRect)
|
||||
panel.contentView?.addSubview(keyLabelStripView)
|
||||
|
||||
var scrollViewRect = contentRect
|
||||
scrollViewRect.origin.x = stripRect.size.width
|
||||
scrollViewRect.size.width -= stripRect.size.width
|
||||
scrollView = NSScrollView(frame: scrollViewRect)
|
||||
scrollView.verticalScrollElasticity = .none
|
||||
|
||||
tableView = NSTableView(frame: contentRect)
|
||||
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue: "candidate"))
|
||||
column.dataCell = NSTextFieldCell()
|
||||
column.isEditable = false
|
||||
|
||||
candidateTextPadding = kCandidateTextPadding
|
||||
candidateTextLeftMargin = kCandidateTextLeftMargin
|
||||
|
||||
tableView.addTableColumn(column)
|
||||
tableView.intercellSpacing = NSSize(width: 0.0, height: 1.0)
|
||||
tableView.headerView = nil
|
||||
tableView.allowsMultipleSelection = false
|
||||
tableView.allowsEmptySelection = false
|
||||
|
||||
if #available(macOS 10.16, *) {
|
||||
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)
|
||||
tableView.dataSource = self
|
||||
tableView.delegate = self
|
||||
tableView.doubleAction = #selector(rowDoubleClicked(_:))
|
||||
tableView.target = self
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public override func reloadData() {
|
||||
maxCandidateAttrStringWidth = ceil(candidateFont.pointSize * 2.0 + candidateTextPadding)
|
||||
tableView.reloadData()
|
||||
layoutCandidateView()
|
||||
if delegate?.candidateCountForController(self) ?? 0 > 0 {
|
||||
selectedCandidateIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
public override func showNextPage() -> Bool {
|
||||
scrollPageByOne(true)
|
||||
}
|
||||
|
||||
public override func showPreviousPage() -> Bool {
|
||||
scrollPageByOne(false)
|
||||
}
|
||||
|
||||
public override func highlightNextCandidate() -> Bool {
|
||||
moveSelectionByOne(true)
|
||||
}
|
||||
|
||||
public override func highlightPreviousCandidate() -> Bool {
|
||||
moveSelectionByOne(false)
|
||||
}
|
||||
|
||||
public override func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt {
|
||||
guard let delegate = delegate else {
|
||||
return UInt.max
|
||||
}
|
||||
|
||||
let firstVisibleRow = tableView.row(at: scrollView.documentVisibleRect.origin)
|
||||
if firstVisibleRow != -1 {
|
||||
let result = UInt(firstVisibleRow) + index
|
||||
if result < delegate.candidateCountForController(self) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return UInt.max
|
||||
}
|
||||
|
||||
public override var selectedCandidateIndex: UInt {
|
||||
get {
|
||||
let selectedRow = tableView.selectedRow
|
||||
return selectedRow == -1 ? UInt.max : UInt(selectedRow)
|
||||
|
||||
}
|
||||
set {
|
||||
guard let delegate = delegate else {
|
||||
return
|
||||
}
|
||||
var newIndex = newValue
|
||||
let selectedRow = tableView.selectedRow
|
||||
let labelCount = keyLabels.count
|
||||
let itemCount = delegate.candidateCountForController(self)
|
||||
|
||||
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 {
|
||||
|
||||
public func numberOfRows(in tableView: NSTableView) -> Int {
|
||||
Int(delegate?.candidateCountForController(self) ?? 0)
|
||||
}
|
||||
|
||||
public func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? {
|
||||
guard let delegate = delegate else {
|
||||
return nil
|
||||
}
|
||||
var candidate = ""
|
||||
if row < delegate.candidateCountForController(self) {
|
||||
candidate = delegate.candidateController(self, candidateAtIndex: UInt(row))
|
||||
}
|
||||
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)
|
||||
keyLabelStripView.highlightedIndex = UInt(selectedRow - firstVisibleRow)
|
||||
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 forward {
|
||||
if newIndex == itemCount - 1 {
|
||||
return false
|
||||
}
|
||||
newIndex += 1
|
||||
} else {
|
||||
if 0 == newIndex {
|
||||
return false
|
||||
}
|
||||
newIndex -= 1
|
||||
}
|
||||
selectedCandidateIndex = newIndex
|
||||
return true
|
||||
}
|
||||
|
||||
private func layoutCandidateView() {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { [self] in
|
||||
doLayoutCandidateView()
|
||||
}
|
||||
}
|
||||
|
||||
private func doLayoutCandidateView() {
|
||||
guard let delegate = delegate else {
|
||||
return
|
||||
}
|
||||
let count = delegate.candidateCountForController(self)
|
||||
if 0 == count {
|
||||
return
|
||||
}
|
||||
|
||||
let candidateFontSize = ceil(candidateFont.pointSize)
|
||||
let keyLabelFontSize = ceil(keyLabelFont.pointSize)
|
||||
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
|
||||
keyLabelStripView.keyLabels = Array(keyLabels[0..<Int(keyLabelCount)])
|
||||
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 keyLabels {
|
||||
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 = stripWidth + 1.0 + tableViewStartWidth
|
||||
let windowHeight = CGFloat(keyLabelCount) * (rowHeight + rowSpacing)
|
||||
|
||||
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.0, width: stripWidth, height: windowHeight)
|
||||
scrollView.frame = NSRect(x: stripWidth + 1.0, y: 0.0, width: tableViewStartWidth, height: windowHeight)
|
||||
self.window?.setFrame(frameRect, display: false)
|
||||
}
|
||||
}
|
|
@ -43,8 +43,6 @@
|
|||
#import "OVStringHelper.h"
|
||||
#import "OVUTF8Helper.h"
|
||||
#import "AppDelegate.h"
|
||||
#import "VTHorizontalCandidateController.h"
|
||||
#import "VTVerticalCandidateController.h"
|
||||
#import "OVNonModalAlertWindowController.h"
|
||||
#import "vChewing-Swift.h"
|
||||
|
||||
|
|
|
@ -12,6 +12,9 @@
|
|||
5B1958522788A2BF00FAEB14 /* MITLicense.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B58E87D278413E7003EA2AD /* MITLicense.txt */; };
|
||||
5B42B64027876FDC00BB9B9F /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 5B42B63E27876FDC00BB9B9F /* UserOverrideModel.cpp */; };
|
||||
5B58E87F278413E7003EA2AD /* MITLicense.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5B58E87D278413E7003EA2AD /* MITLicense.txt */; };
|
||||
5BC3EE1B278FC48C00F5E44C /* VerticalCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC3EE18278FC48C00F5E44C /* VerticalCandidateController.swift */; };
|
||||
5BC3EE1C278FC48C00F5E44C /* VTCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC3EE19278FC48C00F5E44C /* VTCandidateController.swift */; };
|
||||
5BC3EE1D278FC48C00F5E44C /* HorizontalCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC3EE1A278FC48C00F5E44C /* HorizontalCandidateController.swift */; };
|
||||
5BC3FB83278492DE0022E99A /* data-chs.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5BC3FB82278492DE0022E99A /* data-chs.txt */; };
|
||||
5BF4A6FE27844738007DC6E7 /* frmAboutWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 5BF4A6FC27844738007DC6E7 /* frmAboutWindow.m */; };
|
||||
5BF4A70027844DC5007DC6E7 /* frmAboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5BF4A70227844DC5007DC6E7 /* frmAboutWindow.xib */; };
|
||||
|
@ -22,12 +25,6 @@
|
|||
6A0D4ED315FC0D6400ABF4B3 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EC815FC0D6400ABF4B3 /* main.m */; };
|
||||
6A0D4ED415FC0D6400ABF4B3 /* OVInputSourceHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4ECA15FC0D6400ABF4B3 /* OVInputSourceHelper.m */; };
|
||||
6A0D4ED515FC0D6400ABF4B3 /* PreferencesWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4ECC15FC0D6400ABF4B3 /* PreferencesWindowController.m */; };
|
||||
6A0D4EFE15FC0DA600ABF4B3 /* VTCandidateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */; };
|
||||
6A0D4EFF15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EDC15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m */; };
|
||||
6A0D4F0015FC0DA600ABF4B3 /* VTHorizontalCandidateView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EDE15FC0DA600ABF4B3 /* VTHorizontalCandidateView.m */; };
|
||||
6A0D4F0115FC0DA600ABF4B3 /* VTVerticalCandidateController.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EE015FC0DA600ABF4B3 /* VTVerticalCandidateController.m */; };
|
||||
6A0D4F0215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EE215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m */; };
|
||||
6A0D4F0315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4EE415FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m */; };
|
||||
6A0D4F0815FC0DA600ABF4B3 /* Bopomofo.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EEF15FC0DA600ABF4B3 /* Bopomofo.tiff */; };
|
||||
6A0D4F0915FC0DA600ABF4B3 /* Bopomofo@2x.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EF015FC0DA600ABF4B3 /* Bopomofo@2x.tiff */; };
|
||||
6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4F2015FC0EB100ABF4B3 /* Mandarin.cpp */; };
|
||||
|
@ -91,6 +88,9 @@
|
|||
5B9781D72763850700897999 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "Source/zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
5B9781D82763850700897999 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "Source/zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
5B9781D92763850700897999 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = "zh-Hans"; path = "zh-Hans.lproj/MainMenu.xib"; sourceTree = "<group>"; };
|
||||
5BC3EE18278FC48C00F5E44C /* VerticalCandidateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerticalCandidateController.swift; sourceTree = "<group>"; };
|
||||
5BC3EE19278FC48C00F5E44C /* VTCandidateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VTCandidateController.swift; sourceTree = "<group>"; };
|
||||
5BC3EE1A278FC48C00F5E44C /* HorizontalCandidateController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorizontalCandidateController.swift; sourceTree = "<group>"; };
|
||||
5BC3FB82278492DE0022E99A /* data-chs.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "data-chs.txt"; sourceTree = "<group>"; };
|
||||
5BF4A6FB27844738007DC6E7 /* frmAboutWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = frmAboutWindow.h; sourceTree = "<group>"; };
|
||||
5BF4A6FC27844738007DC6E7 /* frmAboutWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = frmAboutWindow.m; sourceTree = "<group>"; };
|
||||
|
@ -113,18 +113,6 @@
|
|||
6A0D4ECA15FC0D6400ABF4B3 /* OVInputSourceHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OVInputSourceHelper.m; sourceTree = "<group>"; };
|
||||
6A0D4ECB15FC0D6400ABF4B3 /* PreferencesWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PreferencesWindowController.h; sourceTree = "<group>"; };
|
||||
6A0D4ECC15FC0D6400ABF4B3 /* PreferencesWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PreferencesWindowController.m; sourceTree = "<group>"; };
|
||||
6A0D4ED915FC0DA600ABF4B3 /* VTCandidateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTCandidateController.h; sourceTree = "<group>"; };
|
||||
6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTCandidateController.m; sourceTree = "<group>"; };
|
||||
6A0D4EDB15FC0DA600ABF4B3 /* VTHorizontalCandidateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTHorizontalCandidateController.h; sourceTree = "<group>"; };
|
||||
6A0D4EDC15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTHorizontalCandidateController.m; sourceTree = "<group>"; };
|
||||
6A0D4EDD15FC0DA600ABF4B3 /* VTHorizontalCandidateView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTHorizontalCandidateView.h; sourceTree = "<group>"; };
|
||||
6A0D4EDE15FC0DA600ABF4B3 /* VTHorizontalCandidateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTHorizontalCandidateView.m; sourceTree = "<group>"; };
|
||||
6A0D4EDF15FC0DA600ABF4B3 /* VTVerticalCandidateController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTVerticalCandidateController.h; sourceTree = "<group>"; };
|
||||
6A0D4EE015FC0DA600ABF4B3 /* VTVerticalCandidateController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTVerticalCandidateController.m; sourceTree = "<group>"; };
|
||||
6A0D4EE115FC0DA600ABF4B3 /* VTVerticalCandidateTableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTVerticalCandidateTableView.h; sourceTree = "<group>"; };
|
||||
6A0D4EE215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTVerticalCandidateTableView.m; sourceTree = "<group>"; };
|
||||
6A0D4EE315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VTVerticalKeyLabelStripView.h; sourceTree = "<group>"; };
|
||||
6A0D4EE415FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VTVerticalKeyLabelStripView.m; sourceTree = "<group>"; };
|
||||
6A0D4EEF15FC0DA600ABF4B3 /* Bopomofo.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = Bopomofo.tiff; sourceTree = "<group>"; };
|
||||
6A0D4EF015FC0DA600ABF4B3 /* Bopomofo@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; path = "Bopomofo@2x.tiff"; sourceTree = "<group>"; };
|
||||
6A0D4EF515FC0DA600ABF4B3 /* vChewing-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "vChewing-Info.plist"; sourceTree = "<group>"; };
|
||||
|
@ -307,18 +295,9 @@
|
|||
6A0D4ED815FC0DA600ABF4B3 /* CandidateUI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
6A0D4ED915FC0DA600ABF4B3 /* VTCandidateController.h */,
|
||||
6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */,
|
||||
6A0D4EDB15FC0DA600ABF4B3 /* VTHorizontalCandidateController.h */,
|
||||
6A0D4EDC15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m */,
|
||||
6A0D4EDD15FC0DA600ABF4B3 /* VTHorizontalCandidateView.h */,
|
||||
6A0D4EDE15FC0DA600ABF4B3 /* VTHorizontalCandidateView.m */,
|
||||
6A0D4EDF15FC0DA600ABF4B3 /* VTVerticalCandidateController.h */,
|
||||
6A0D4EE015FC0DA600ABF4B3 /* VTVerticalCandidateController.m */,
|
||||
6A0D4EE115FC0DA600ABF4B3 /* VTVerticalCandidateTableView.h */,
|
||||
6A0D4EE215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m */,
|
||||
6A0D4EE315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.h */,
|
||||
6A0D4EE415FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m */,
|
||||
5BC3EE19278FC48C00F5E44C /* VTCandidateController.swift */,
|
||||
5BC3EE1A278FC48C00F5E44C /* HorizontalCandidateController.swift */,
|
||||
5BC3EE18278FC48C00F5E44C /* VerticalCandidateController.swift */,
|
||||
);
|
||||
path = CandidateUI;
|
||||
sourceTree = "<group>";
|
||||
|
@ -646,18 +625,15 @@
|
|||
6A0D4ED315FC0D6400ABF4B3 /* main.m in Sources */,
|
||||
5BF4A6FE27844738007DC6E7 /* frmAboutWindow.m in Sources */,
|
||||
6A0D4ED415FC0D6400ABF4B3 /* OVInputSourceHelper.m in Sources */,
|
||||
5BC3EE1B278FC48C00F5E44C /* VerticalCandidateController.swift in Sources */,
|
||||
5B42B64027876FDC00BB9B9F /* UserOverrideModel.cpp in Sources */,
|
||||
6A0D4ED515FC0D6400ABF4B3 /* PreferencesWindowController.m in Sources */,
|
||||
6A0D4EFE15FC0DA600ABF4B3 /* VTCandidateController.m in Sources */,
|
||||
6A0D4EFF15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m in Sources */,
|
||||
6A0D4F0015FC0DA600ABF4B3 /* VTHorizontalCandidateView.m in Sources */,
|
||||
6AFF97F3253B299E007F1C49 /* OVNonModalAlertWindowController.m in Sources */,
|
||||
6A0D4F0115FC0DA600ABF4B3 /* VTVerticalCandidateController.m in Sources */,
|
||||
6A0D4F0215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m in Sources */,
|
||||
6A0D4F0315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m in Sources */,
|
||||
D427A9C125ED28CC005D43E0 /* OpenCCBridge.swift in Sources */,
|
||||
6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */,
|
||||
6A0421A815FEF3F50061ED63 /* FastLM.cpp in Sources */,
|
||||
5BC3EE1C278FC48C00F5E44C /* VTCandidateController.swift in Sources */,
|
||||
5BC3EE1D278FC48C00F5E44C /* HorizontalCandidateController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue