From a97cc5ca6cfa19e2db1642c0c38356a2dae9abd1 Mon Sep 17 00:00:00 2001 From: zonble Date: Tue, 11 Jan 2022 00:03:32 +0800 Subject: [PATCH] Converts VerticalCandidateController to Swift. --- McBopomofo.xcodeproj/project.pbxproj | 6 +- .../HorizontalCandidateController.swift | 11 +- .../VerticalCandidateController.swift | 423 ++++++++++++++++++ Source/InputMethodController.mm | 3 +- 4 files changed, 435 insertions(+), 8 deletions(-) create mode 100644 Source/CandidateUI/VerticalCandidateController.swift diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index 7f0a3aac..de626830 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 6AE210B315FC63CC003659FE /* PlainBopomofo@2x.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 6AE210B115FC63CC003659FE /* PlainBopomofo@2x.tiff */; }; 6AFF97F2253B299E007F1C49 /* NonModalAlertWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */; }; D427A9C125ED28CC005D43E0 /* OpenCCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */; }; + D427F75F278C74B7004A2160 /* VerticalCandidateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427F75E278C74B7004A2160 /* VerticalCandidateController.swift */; }; D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; }; D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */; }; D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD2278C1263002F9DD7 /* UserOverrideModel.cpp */; }; @@ -178,6 +179,7 @@ 6AFF97F0253B299E007F1C49 /* NonModalAlertWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NonModalAlertWindowController.xib; sourceTree = ""; }; D427A9BF25ED28CC005D43E0 /* McBopomofo-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "McBopomofo-Bridging-Header.h"; sourceTree = ""; }; D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenCCBridge.swift; sourceTree = ""; }; + D427F75E278C74B7004A2160 /* VerticalCandidateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerticalCandidateController.swift; sourceTree = ""; }; D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonModalAlertWindowController.swift; sourceTree = ""; }; D47F7DD1278C1263002F9DD7 /* UserOverrideModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UserOverrideModel.h; sourceTree = ""; }; @@ -248,7 +250,6 @@ 6A0D4F4715FC0EB900ABF4B3 /* Resources */, 6A0D4EC315FC0D6400ABF4B3 /* AppDelegate.h */, 6A0D4EC415FC0D6400ABF4B3 /* AppDelegate.m */, - 6A0D4EF615FC0DA600ABF4B3 /* McBopomofo-Prefix.pch */, 6A0D4EC615FC0D6400ABF4B3 /* InputMethodController.h */, 6A0D4EC715FC0D6400ABF4B3 /* InputMethodController.mm */, 6A0D4EC815FC0D6400ABF4B3 /* main.m */, @@ -256,6 +257,7 @@ D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */, D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */, D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */, + 6A0D4EF615FC0DA600ABF4B3 /* McBopomofo-Prefix.pch */, D427A9BF25ED28CC005D43E0 /* McBopomofo-Bridging-Header.h */, ); path = Source; @@ -266,6 +268,7 @@ children = ( D47F7DD9278C32CD002F9DD7 /* CandidateController.swift */, D47F7DDB278C39EC002F9DD7 /* HorizontalCandidateController.swift */, + D427F75E278C74B7004A2160 /* VerticalCandidateController.swift */, 6A0D4ED915FC0DA600ABF4B3 /* VTCandidateController.h */, 6A0D4EDA15FC0DA600ABF4B3 /* VTCandidateController.m */, 6A0D4EDB15FC0DA600ABF4B3 /* VTHorizontalCandidateController.h */, @@ -583,6 +586,7 @@ D47F7DD3278C1263002F9DD7 /* UserOverrideModel.cpp in Sources */, D47F7DDC278C39EC002F9DD7 /* HorizontalCandidateController.swift in Sources */, 6A0D4F0015FC0DA600ABF4B3 /* VTHorizontalCandidateView.m in Sources */, + D427F75F278C74B7004A2160 /* VerticalCandidateController.swift in Sources */, 6A0D4F0115FC0DA600ABF4B3 /* VTVerticalCandidateController.m in Sources */, 6A0D4F0215FC0DA600ABF4B3 /* VTVerticalCandidateTableView.m in Sources */, 6A0D4F0315FC0DA600ABF4B3 /* VTVerticalKeyLabelStripView.m in Sources */, diff --git a/Source/CandidateUI/HorizontalCandidateController.swift b/Source/CandidateUI/HorizontalCandidateController.swift index b6ce204e..ff28cc8e 100644 --- a/Source/CandidateUI/HorizontalCandidateController.swift +++ b/Source/CandidateUI/HorizontalCandidateController.swift @@ -1,6 +1,6 @@ import Cocoa -class HorizontalCandidateView: NSView { +fileprivate class HorizontalCandidateView: NSView { var highlightedIndex: UInt = 0 var action: Selector? weak var target: AnyObject? @@ -159,6 +159,7 @@ class HorizontalCandidateView: NSView { } } +@objc(HorizontalCandidateController) public class HorizontalCandidateController : CandidateController { private var candidateView: HorizontalCandidateView private var prevPageButton: NSButton @@ -295,7 +296,7 @@ public class HorizontalCandidateController : CandidateController { extension HorizontalCandidateController { - var pageCount: UInt { + private var pageCount: UInt { guard let delegate = delegate else { return 0 } @@ -304,7 +305,7 @@ extension HorizontalCandidateController { return totalCount / keyLabelCount + ((totalCount % keyLabelCount) != 0 ? 1 : 0) } - func layoutCandidateView() { + private func layoutCandidateView() { guard let delegate = delegate else { return } @@ -363,7 +364,7 @@ extension HorizontalCandidateController { self.candidateView.setNeedsDisplay(candidateView.bounds) } - @objc func pageButtonAction(_ sender: Any) { + @objc fileprivate func pageButtonAction(_ sender: Any) { guard let sender = sender as? NSButton else { return } @@ -374,7 +375,7 @@ extension HorizontalCandidateController { } } - @objc func candidateViewMouseDidClick(_ sender: Any) { + @objc fileprivate func candidateViewMouseDidClick(_ sender: Any) { delegate?.candidateController(self, didSelectCandidateAtIndex: self.selectedCandidateIndex) } diff --git a/Source/CandidateUI/VerticalCandidateController.swift b/Source/CandidateUI/VerticalCandidateController.swift new file mode 100644 index 00000000..5bd15f51 --- /dev/null +++ b/Source/CandidateUI/VerticalCandidateController.swift @@ -0,0 +1,423 @@ +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 { + 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(VerticalCandidateController) +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.self + 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 + + scrollView.documentView = tableView + panel.contentView?.addSubview(scrollView) + + + let paraStyle = NSMutableParagraphStyle() + paraStyle.setParagraphStyle(NSParagraphStyle.default) + paraStyle.firstLineHeadIndent = candidateTextLeftMargin + paraStyle.lineBreakMode = .byClipping + + candidateTextParagraphStyle = paraStyle + + + if #available(macOS 10.16, *) { + tableView.style = .fullWidth + candidateTextPadding = kCandidateTextPaddingWithMandatedTableViewPadding + candidateTextLeftMargin = kCandidateTextLeftMarginWithMandatedTableViewPadding + } + + 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() + self.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) + } + + @objc public 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 + } + + @objc 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 + self.doLayoutCanaditeView() + } + } + + private func doLayoutCanaditeView() { + 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..= 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) + } +} diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index 7c53d37b..c5ca590f 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -39,7 +39,6 @@ #import "OVStringHelper.h" #import "OVUTF8Helper.h" #import "AppDelegate.h" -#import "OVNonModalAlertWindowController.h" #import "VTHorizontalCandidateController.h" #import "VTVerticalCandidateController.h" #import "McBopomofo-Swift.h" @@ -1589,7 +1588,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } NSLog(@"openUserPhrases called"); if (!LTCheckIfUserLanguageModelFileExists()) { NSString *content = [NSString stringWithFormat:NSLocalizedString(@"Please check the permission of at \"%@\".", @""), LTUserDataFolderPath()]; - [[OVNonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Unable to create the user phrase file.", @"") content:content confirmButtonTitle:NSLocalizedString(@"OK", @"") cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; + [[NonModalAlertWindowController sharedInstance] showWithTitle:NSLocalizedString(@"Unable to create the user phrase file.", @"") content:content confirmButtonTitle:NSLocalizedString(@"OK", @"") cancelButtonTitle:nil cancelAsDefault:NO delegate:nil]; return; }