diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index dd33b425..7f0a3aac 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -49,6 +49,8 @@ D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; }; D47F7DD5278C25A0002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; }; D47F7DD6278C3075002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; }; + D47F7DDA278C32CD002F9DD7 /* CandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD9278C32CD002F9DD7 /* CandidateController.swift */; }; + D47F7DDC278C39EC002F9DD7 /* HorizontalCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DDB278C39EC002F9DD7 /* HorizontalCandidateController.swift */; }; D48550A325EBE689006A204C /* OpenCC in Frameworks */ = {isa = PBXBuildFile; productRef = D48550A225EBE689006A204C /* OpenCC */; }; /* End PBXBuildFile section */ @@ -181,6 +183,8 @@ D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = ""; }; D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = UserOverrideModel.cpp; sourceTree = ""; }; D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputSourceHelper.swift; sourceTree = ""; }; + D47F7DD9278C32CD002F9DD7 /* CandidateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandidateController.swift; sourceTree = ""; }; + D47F7DDB278C39EC002F9DD7 /* HorizontalCandidateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HorizontalCandidateController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -260,6 +264,8 @@ 6A0D4ED815FC0DA600ABF4B3 /* CandidateUI */ = { isa = PBXGroup; children = ( + D47F7DD9278C32CD002F9DD7 /* CandidateController.swift */, + D47F7DDB278C39EC002F9DD7 /* HorizontalCandidateController.swift */, 6A0D4ED915FC0DA600ABF4B3 /* VTCandidateController.h */, 6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */, 6A0D4EDB15FC0DA600ABF4B3 /* VTHorizontalCandidateController.h */, @@ -575,11 +581,13 @@ 6A0D4EFE15FC0DA600ABF4B3 /* VTCandidateController.m in Sources */, 6A0D4EFF15FC0DA600ABF4B3 /* VTHorizontalCandidateController.m in Sources */, D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */, + D47F7DDC278C39EC002F9DD7 /* HorizontalCandidateController.swift in Sources */, 6A0D4F0015FC0DA600ABF4B3 /* VTHorizontalCandidateView.m in Sources */, 6A0D4F0115FC0DA600ABF4B3 /* VTVerticalCandidateController.m in Sources */, 6A0D4F0215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m in Sources */, 6A0D4F0315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m in Sources */, D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */, + D47F7DDA278C32CD002F9DD7 /* CandidateController.swift in Sources */, D427A9C125ED28CC005D43E0 /* OpenCCBridge.swift in Sources */, D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */, 6A0D4F4515FC0EB100ABF4B3 /* Mandarin.cpp in Sources */, diff --git a/Source/CandidateUI/CandidateController.swift b/Source/CandidateUI/CandidateController.swift new file mode 100644 index 00000000..f0595c7f --- /dev/null +++ b/Source/CandidateUI/CandidateController.swift @@ -0,0 +1,105 @@ +import Cocoa + +@objc(CandidateControllerDelegate) +public protocol CandidateControllerDelegate: AnyObject { + func candidateCountForController(_ controller: CandidateController) -> UInt + func candidateController(_ controller: CandidateController, candidateAtIndex index: UInt) -> String + func candidateController(_ controller: CandidateController, didSelectCandidateAtIndex index: UInt) +} + +@objc(CandidateController) +public class CandidateController: NSWindowController { + @objc public weak var delegate: CandidateControllerDelegate? + @objc public var selectedCandidateIndex: UInt = UInt.max + @objc public var visible: Bool = false { + didSet { + if visible { + window?.perform(#selector(NSWindow.orderFront(_:)), with: self, afterDelay: 0.0) + } else { + window?.perform(#selector(NSWindow.orderOut(_:)), with: self, afterDelay: 0.0) + } + } + } + @objc public var windowTopLeftPoint: NSPoint { + get { + guard let frameRect = window?.frame else { + return NSPoint.zero + } + return NSPoint(x: frameRect.minX, y: frameRect.maxY) + } + set { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) { + self.set(windowTopLeftPoint: newValue, bottomOutOfScreenAdjustmentHeight: 0) + } + } + } + + @objc public var keyLabels: [String] = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] + @objc public var keyLabelFont: NSFont = NSFont.systemFont(ofSize: 14) + @objc public var candidateFont: NSFont = NSFont.systemFont(ofSize: 18) + + @objc public func reloadData() { + } + + @objc public func showNextPage() -> Bool { + return false + } + + @objc public func showPreviousPage() -> Bool { + return false + } + + @objc public func highlightNextCandidate() -> Bool { + return false + } + + @objc public func highlightPreviousCandidate() -> Bool { + return false + } + + func set(windowTopLeftPoint: NSPoint, bottomOutOfScreenAdjustmentHeight height:CGFloat) { + var adjustedPoint = windowTopLeftPoint + var adjustedHeight = height + + var screenFrame = NSScreen.main?.visibleFrame ?? NSRect.zero + for screen in NSScreen.screens { + let frame = screen.visibleFrame + if windowTopLeftPoint.x >= frame.minX && + windowTopLeftPoint.x <= frame.maxX && + windowTopLeftPoint.y >= frame.minY && + windowTopLeftPoint.y <= frame.maxY { + screenFrame = frame + break + } + } + + if adjustedHeight > screenFrame.size.height / 2.0 { + adjustedHeight = 0.0 + } + + let windowSize = window?.frame.size ?? NSSize.zero + + // bottom beneath the screen? + if adjustedPoint.y - windowSize.height < screenFrame.maxY { + adjustedPoint.y = windowTopLeftPoint.y + adjustedHeight + windowSize.height + } + + // top over the screen? + if adjustedPoint.y >= screenFrame.maxY { + adjustedPoint.y = screenFrame.maxY - 1.0 + } + + // right + if adjustedPoint.x + windowSize.width >= screenFrame.maxX { + adjustedPoint.x = NSMaxX(screenFrame) - windowSize.width + } + + // left + if adjustedPoint.x < screenFrame.minX { + adjustedPoint.x = screenFrame.minX + } + + window?.setFrameTopLeftPoint(adjustedPoint) + } + +} diff --git a/Source/CandidateUI/HorizontalCandidateController.swift b/Source/CandidateUI/HorizontalCandidateController.swift new file mode 100644 index 00000000..b6ce204e --- /dev/null +++ b/Source/CandidateUI/HorizontalCandidateController.swift @@ -0,0 +1,382 @@ +import Cocoa + +class HorizontalCandidateView: NSView { + var highlightedIndex: UInt = 0 + var action: Selector? + weak var target: AnyObject? + + private var keyLabels: [String] = [] + private var displayedCandidates: [String] = [] + private var keyLabelHeight: CGFloat = 0 + private var candidateTextHeight: CGFloat = 0 + private var cellPadding: CGFloat = 0 + private var keyLabelAttrDict: [NSAttributedString.Key:AnyObject] = [:] + private var candidateAttrDict: [NSAttributedString.Key:AnyObject] = [:] + private var elementWidths: [CGFloat] = [] + private var trackingHighlightedIndex: UInt = UInt.max + + override var isFlipped: Bool { + true + } + + var sizeForView: NSSize { + var result = NSSize.zero + + if !elementWidths.isEmpty { + result.width = elementWidths.reduce(0, +) + result.width += CGFloat(elementWidths.count) + result.height = keyLabelHeight + candidateTextHeight + 1.0 + + } + return result + } + + @objc(setKeyLabels:displayedCandidates:) + func set(keyLabels labels: [String], displayedCandidates candidates: [String]) { + let count = min(labels.count, candidates.count) + keyLabels = Array(labels[0.. UInt? { + let location = convert(event.locationInWindow, to: nil) + if !NSPointInRect(location, self.bounds) { + return nil + } + var accuWidth: CGFloat = 0.0 + for index in 0..= accuWidth && location.x <= accuWidth + currentWidth { + return UInt(index) + } + accuWidth += currentWidth + 1.0 + } + return nil + + } + + override func mouseUp(with event: NSEvent) { + trackingHighlightedIndex = highlightedIndex + guard let newIndex = findHitIndex(event: event) else { + return + } + highlightedIndex = newIndex + self.setNeedsDisplay(self.bounds) + } + + override func mouseDown(with event: NSEvent) { + guard let newIndex = findHitIndex(event: event) else { + return + } + var triggerAction = false + if newIndex == highlightedIndex { + triggerAction = true + } else { + highlightedIndex = trackingHighlightedIndex; + } + + trackingHighlightedIndex = 0 + self.setNeedsDisplay(self.bounds) + if triggerAction { + if let target = target as? NSObject, let action = action { + target.perform(action, with: self) + } + } + } +} + +public class HorizontalCandidateController : CandidateController { + private var candidateView: HorizontalCandidateView + private var prevPageButton: NSButton + private var nextPageButton: NSButton + private var currentPage: UInt = 0 + + public init() { + var contentRect = NSRect(x: 128.0, y: 128.0, width: 0.0, height: 0.0) + let styleMask: NSWindow.StyleMask = [.borderless, .nonactivatingPanel] + let panel = NSPanel(contentRect: contentRect, styleMask: styleMask, backing: .buffered, defer: false) + + contentRect.origin = NSPoint.zero + candidateView = HorizontalCandidateView(frame: contentRect) + panel.contentView?.addSubview(candidateView) + + contentRect.size = NSSize(width: 36.0, height: 20.0) + nextPageButton = NSButton(frame: contentRect) + prevPageButton = NSButton(frame: contentRect) + + panel.contentView?.addSubview(nextPageButton) + panel.contentView?.addSubview(prevPageButton) + + super.init(window: panel) + + candidateView.target = self + candidateView.action = #selector(candidateViewMouseDidClick(_:)) + + nextPageButton.setButtonType(.momentaryLight) + nextPageButton.bezelStyle = .smallSquare + nextPageButton.title = "»" + nextPageButton.target = self + nextPageButton.action = #selector(pageButtonAction(_:)) + + prevPageButton.setButtonType(.momentaryLight) + prevPageButton.bezelStyle = .smallSquare + prevPageButton.title = "«" + prevPageButton.target = self + prevPageButton.action = #selector(pageButtonAction(_:)) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public override func reloadData() { + candidateView.highlightedIndex = 0 + currentPage = 0 + layoutCandidateView() + } + + public override func showNextPage() -> Bool { + guard delegate != nil else { + return false + } + + if currentPage + 1 >= self.pageCount { + return false + } + + currentPage += 1 + candidateView.highlightedIndex = 0 + layoutCandidateView(); + return true; + } + + public override func showPreviousPage() -> Bool { + guard delegate != nil else { + return false + } + + if currentPage == 0 { + return false + } + + currentPage -= 1 + candidateView.highlightedIndex = 0 + layoutCandidateView(); + return true; + } + + public override func highlightNextCandidate() -> Bool { + guard let delegate = delegate else { + return false + } + + let currentIndex = selectedCandidateIndex + if currentIndex + 1 >= delegate.candidateCountForController(self) { + return false; + } + selectedCandidateIndex = currentIndex + 1 + return true; + } + + public override func highlightPreviousCandidate() -> Bool { + guard delegate != nil else { + return false + } + + let currentIndex = self.selectedCandidateIndex; + if currentIndex == 0 { + return false + } + + selectedCandidateIndex = currentIndex - 1 + return true + } + + @objc public func candidateIndexAtKeyLabelIndex(_ index: UInt) -> UInt { + guard let delegate = delegate else { + return UInt.max + } + + let result = currentPage * UInt(keyLabels.count) + index; + return result < delegate.candidateCountForController(self) ? result : UInt.max; + } + + @objc public override var selectedCandidateIndex: UInt { + get { + return currentPage * UInt(keyLabels.count) + candidateView.highlightedIndex + } + set { + guard let delegate = delegate else { + return + } + let keyLabelCount = UInt(keyLabels.count) + if newValue < delegate.candidateCountForController(self) { + currentPage = newValue / keyLabelCount; + candidateView.highlightedIndex = newValue % keyLabelCount; + layoutCandidateView() + } + } + } +} + +extension HorizontalCandidateController { + + var pageCount: UInt { + guard let delegate = delegate else { + return 0 + } + let totalCount = delegate.candidateCountForController(self) + let keyLabelCount = UInt(keyLabels.count) + return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) + } + + func layoutCandidateView() { + guard let delegate = delegate else { + return + } + + candidateView.set(keyLabelFont: keyLabelFont, candidateFont: candidateFont) + var candidates = [String]() + let count = delegate.candidateCountForController(self) + let keyLabelCount = UInt(keyLabels.count) + + let begin = currentPage * keyLabelCount + for index in begin.. 1 { + var buttonRect = nextPageButton.frame + var spacing = 0.0 + + if newSize.height < 40.0 { + buttonRect.size.height = floor(newSize.height / 2) + } else { + buttonRect.size.height = 20.0 + } + + if newSize.height >= 60.0 { + spacing = ceil(newSize.height * 0.1) + } + + let buttonOriginY = (newSize.height - (buttonRect.size.height * 2.0 + spacing)) / 2.0 + buttonRect.origin = NSPoint(x: newSize.width + 8.0, y: buttonOriginY) + nextPageButton.frame = buttonRect + + buttonRect.origin = NSPoint(x: newSize.width + 8.0, y: buttonOriginY + buttonRect.size.height + spacing) + prevPageButton.frame = buttonRect + + newSize.width += 52.0 + nextPageButton.isHidden = false + prevPageButton.isHidden = false + } else { + nextPageButton.isHidden = true + prevPageButton.isHidden = true + } + + frameRect = window?.frame ?? NSRect.zero + + let topLeftPoint = NSMakePoint(frameRect.origin.x, frameRect.origin.y + frameRect.size.height) + frameRect.size = newSize + frameRect.origin = NSMakePoint(topLeftPoint.x, topLeftPoint.y - frameRect.size.height) + self.window?.setFrame(frameRect, display: false) + self.candidateView.setNeedsDisplay(candidateView.bounds) + } + + @objc func pageButtonAction(_ sender: Any) { + guard let sender = sender as? NSButton else { + return + } + if sender == nextPageButton { + _ = showNextPage() + } else if sender == prevPageButton { + _ = showPreviousPage() + } + } + + @objc func candidateViewMouseDidClick(_ sender: Any) { + delegate?.candidateController(self, didSelectCandidateAtIndex: self.selectedCandidateIndex) + } + +} +