diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index 70883d91..6633cae7 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -39,15 +39,16 @@ D41355D8278D74B5005E5CBD /* LanguageModelManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = D41355D7278D7409005E5CBD /* LanguageModelManager.mm */; }; D41355DB278E6D17005E5CBD /* McBopomofoLM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D41355D9278E6D17005E5CBD /* McBopomofoLM.cpp */; }; D41355DE278EA3ED005E5CBD /* UserPhrasesLM.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D41355DC278EA3ED005E5CBD /* UserPhrasesLM.cpp */; }; - D427A9C125ED28CC005D43E0 /* OpenCCBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */; }; D427F76A278C9E29004A2160 /* CandidateUI in Frameworks */ = {isa = PBXBuildFile; productRef = D427F769278C9E29004A2160 /* CandidateUI */; }; D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427F76B278CA1BA004A2160 /* AppDelegate.swift */; }; + D427F7A927905E90004A2160 /* TooltipUI in Frameworks */ = {isa = PBXBuildFile; productRef = D427F7A827905E90004A2160 /* TooltipUI */; }; + D427F7AE27907B8A004A2160 /* NotifierUI in Frameworks */ = {isa = PBXBuildFile; productRef = D427F7AD27907B8A004A2160 /* NotifierUI */; }; + D427F7B4279086DC004A2160 /* InputSourceHelper in Frameworks */ = {isa = PBXBuildFile; productRef = D427F7B3279086DC004A2160 /* InputSourceHelper */; }; + D427F7B6279086F6004A2160 /* InputSourceHelper in Frameworks */ = {isa = PBXBuildFile; productRef = D427F7B5279086F6004A2160 /* InputSourceHelper */; }; + D427F7C127908EFC004A2160 /* OpenCCBridge in Frameworks */ = {isa = PBXBuildFile; productRef = D427F7C027908EFC004A2160 /* OpenCCBridge */; }; 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 */; }; - D47F7DD5278C25A0002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; }; - D47F7DD6278C3075002F9DD7 /* InputSourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */; }; - D48550A325EBE689006A204C /* OpenCC in Frameworks */ = {isa = PBXBuildFile; productRef = D48550A225EBE689006A204C /* OpenCC */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -165,14 +166,16 @@ D41355DC278EA3ED005E5CBD /* UserPhrasesLM.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = UserPhrasesLM.cpp; sourceTree = ""; }; D41355DD278EA3ED005E5CBD /* UserPhrasesLM.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UserPhrasesLM.h; 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 = ""; }; D427F768278C9D0D004A2160 /* CandidateUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = CandidateUI; path = Packages/CandidateUI; sourceTree = ""; }; D427F76B278CA1BA004A2160 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + D427F7A727905E43004A2160 /* TooltipUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TooltipUI; path = Packages/TooltipUI; sourceTree = ""; }; + D427F7AC27907B7E004A2160 /* NotifierUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NotifierUI; path = Packages/NotifierUI; sourceTree = ""; }; + D427F7B2279086B5004A2160 /* InputSourceHelper */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = InputSourceHelper; path = Packages/InputSourceHelper; sourceTree = ""; }; + D427F7BF27908EAC004A2160 /* OpenCCBridge */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = OpenCCBridge; path = Packages/OpenCCBridge; 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 = ""; }; 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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -180,9 +183,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D427F7B4279086DC004A2160 /* InputSourceHelper in Frameworks */, + D427F7C127908EFC004A2160 /* OpenCCBridge in Frameworks */, 6A38BC2815FC158A00A8A51F /* InputMethodKit.framework in Frameworks */, - D48550A325EBE689006A204C /* OpenCC in Frameworks */, + D427F7A927905E90004A2160 /* TooltipUI in Frameworks */, D427F76A278C9E29004A2160 /* CandidateUI in Frameworks */, + D427F7AE27907B8A004A2160 /* NotifierUI in Frameworks */, 6A0D4EA715FC0D2D00ABF4B3 /* Cocoa.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -191,6 +197,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D427F7B6279086F6004A2160 /* InputSourceHelper in Frameworks */, 6ACA41CD15FC1D7500935EF6 /* Cocoa.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -241,10 +248,8 @@ D41355D7278D7409005E5CBD /* LanguageModelManager.mm */, 6A0D4EC815FC0D6400ABF4B3 /* main.m */, D427F76B278CA1BA004A2160 /* AppDelegate.swift */, - D47F7DD4278C25A0002F9DD7 /* InputSourceHelper.swift */, D47F7DCF278C0897002F9DD7 /* NonModalAlertWindowController.swift */, D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */, - D427A9C025ED28CC005D43E0 /* OpenCCBridge.swift */, 6A0D4EF615FC0DA600ABF4B3 /* McBopomofo-Prefix.pch */, D427A9BF25ED28CC005D43E0 /* McBopomofo-Bridging-Header.h */, ); @@ -393,6 +398,10 @@ isa = PBXGroup; children = ( D427F768278C9D0D004A2160 /* CandidateUI */, + D427F7A727905E43004A2160 /* TooltipUI */, + D427F7AC27907B7E004A2160 /* NotifierUI */, + D427F7B2279086B5004A2160 /* InputSourceHelper */, + D427F7BF27908EAC004A2160 /* OpenCCBridge */, ); name = Packages; sourceTree = ""; @@ -432,8 +441,11 @@ ); name = McBopomofo; packageProductDependencies = ( - D48550A225EBE689006A204C /* OpenCC */, D427F769278C9E29004A2160 /* CandidateUI */, + D427F7A827905E90004A2160 /* TooltipUI */, + D427F7AD27907B8A004A2160 /* NotifierUI */, + D427F7B3279086DC004A2160 /* InputSourceHelper */, + D427F7C027908EFC004A2160 /* OpenCCBridge */, ); productName = McBopomofo; productReference = 6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */; @@ -454,6 +466,9 @@ 6ACA420115FC1DCC00935EF6 /* PBXTargetDependency */, ); name = McBopomofoInstaller; + packageProductDependencies = ( + D427F7B5279086F6004A2160 /* InputSourceHelper */, + ); productName = McBopomofoInstaller; productReference = 6ACA41CB15FC1D7500935EF6 /* McBopomofoInstaller.app */; productType = "com.apple.product-type.application"; @@ -482,7 +497,6 @@ ); mainGroup = 6A0D4E9215FC0CFA00ABF4B3; packageReferences = ( - D48550A125EBE689006A204C /* XCRemoteSwiftPackageReference "SwiftyOpenCC" */, ); productRefGroup = 6A0D4EA315FC0D2D00ABF4B3 /* Products */; projectDirPath = ""; @@ -558,9 +572,7 @@ files = ( D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */, 6A0D4ED315FC0D6400ABF4B3 /* main.m in Sources */, - D47F7DD5278C25A0002F9DD7 /* InputSourceHelper.swift in Sources */, D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */, - D427A9C125ED28CC005D43E0 /* OpenCCBridge.swift in Sources */, D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */, 6A0D4ED215FC0D6400ABF4B3 /* InputMethodController.mm in Sources */, D41355DB278E6D17005E5CBD /* McBopomofoLM.cpp in Sources */, @@ -579,7 +591,6 @@ 6ACA41F915FC1D9000935EF6 /* AppDelegate.m in Sources */, 6A225A232367A1D700F685C6 /* ArchiveUtil.m in Sources */, 6ACA41FF15FC1D9000935EF6 /* main.m in Sources */, - D47F7DD6278C3075002F9DD7 /* InputSourceHelper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1043,26 +1054,30 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - D48550A125EBE689006A204C /* XCRemoteSwiftPackageReference "SwiftyOpenCC" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ddddxxx/SwiftyOpenCC.git"; - requirement = { - kind = revision; - revision = 1d8105a0f7199c90af722bff62728050c858e777; - }; - }; -/* End XCRemoteSwiftPackageReference section */ - /* Begin XCSwiftPackageProductDependency section */ D427F769278C9E29004A2160 /* CandidateUI */ = { isa = XCSwiftPackageProductDependency; productName = CandidateUI; }; - D48550A225EBE689006A204C /* OpenCC */ = { + D427F7A827905E90004A2160 /* TooltipUI */ = { isa = XCSwiftPackageProductDependency; - package = D48550A125EBE689006A204C /* XCRemoteSwiftPackageReference "SwiftyOpenCC" */; - productName = OpenCC; + productName = TooltipUI; + }; + D427F7AD27907B8A004A2160 /* NotifierUI */ = { + isa = XCSwiftPackageProductDependency; + productName = NotifierUI; + }; + D427F7B3279086DC004A2160 /* InputSourceHelper */ = { + isa = XCSwiftPackageProductDependency; + productName = InputSourceHelper; + }; + D427F7B5279086F6004A2160 /* InputSourceHelper */ = { + isa = XCSwiftPackageProductDependency; + productName = InputSourceHelper; + }; + D427F7C027908EFC004A2160 /* OpenCCBridge */ = { + isa = XCSwiftPackageProductDependency; + productName = OpenCCBridge; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift b/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift index c7d7d505..05d67a42 100644 --- a/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift +++ b/Packages/CandidateUI/Sources/CandidateUI/HorizontalCandidateController.swift @@ -203,7 +203,7 @@ public class HorizontalCandidateController: CandidateController { 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.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) panel.hasShadow = true contentRect.origin = NSPoint.zero diff --git a/Packages/InputSourceHelper/.gitignore b/Packages/InputSourceHelper/.gitignore new file mode 100644 index 00000000..bb460e7b --- /dev/null +++ b/Packages/InputSourceHelper/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/Packages/InputSourceHelper/Package.swift b/Packages/InputSourceHelper/Package.swift new file mode 100644 index 00000000..4543d050 --- /dev/null +++ b/Packages/InputSourceHelper/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "InputSourceHelper", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "InputSourceHelper", + targets: ["InputSourceHelper"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "InputSourceHelper", + dependencies: []), + ] +) diff --git a/Packages/InputSourceHelper/README.md b/Packages/InputSourceHelper/README.md new file mode 100644 index 00000000..cc27cfb9 --- /dev/null +++ b/Packages/InputSourceHelper/README.md @@ -0,0 +1,3 @@ +# InputSourceHelper + +A description of this package. diff --git a/Source/InputSourceHelper.swift b/Packages/InputSourceHelper/Sources/InputSourceHelper/InputSourceHelper.swift similarity index 100% rename from Source/InputSourceHelper.swift rename to Packages/InputSourceHelper/Sources/InputSourceHelper/InputSourceHelper.swift diff --git a/Packages/NotifierUI/.gitignore b/Packages/NotifierUI/.gitignore new file mode 100644 index 00000000..bb460e7b --- /dev/null +++ b/Packages/NotifierUI/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/Packages/NotifierUI/Package.swift b/Packages/NotifierUI/Package.swift new file mode 100644 index 00000000..c25baecb --- /dev/null +++ b/Packages/NotifierUI/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "NotifierUI", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "NotifierUI", + targets: ["NotifierUI"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "NotifierUI", + dependencies: []), + ] +) diff --git a/Packages/NotifierUI/README.md b/Packages/NotifierUI/README.md new file mode 100644 index 00000000..51230c35 --- /dev/null +++ b/Packages/NotifierUI/README.md @@ -0,0 +1,3 @@ +# NotifierUI + +A description of this package. diff --git a/Packages/NotifierUI/Sources/NotifierUI/NotifierController.swift b/Packages/NotifierUI/Sources/NotifierUI/NotifierController.swift new file mode 100644 index 00000000..b940f4f9 --- /dev/null +++ b/Packages/NotifierUI/Sources/NotifierUI/NotifierController.swift @@ -0,0 +1,171 @@ +import Cocoa + +private protocol NotifierWindowDelegate: AnyObject { + func windowDidBecomeClicked(_ window: NotifierWindow) +} + +private class NotifierWindow: NSWindow { + weak var clickDelegate: NotifierWindowDelegate? + + override func mouseDown(with event: NSEvent) { + clickDelegate?.windowDidBecomeClicked(self) + } +} + +private let kWindowWidth: CGFloat = 160.0 +private let kWindowHeight: CGFloat = 80.0 + +public class NotifierController: NSWindowController, NotifierWindowDelegate { + private var messageTextField: NSTextField + + private var message: String = "" { + didSet { + let paraStyle = NSMutableParagraphStyle() + paraStyle.setParagraphStyle(NSParagraphStyle.default) + paraStyle.alignment = .center + let attr: [NSAttributedString.Key: AnyObject] = [ + .foregroundColor: foregroundColor, + .font: NSFont.systemFont(ofSize: NSFont.systemFontSize(for: .regular)), + .paragraphStyle: paraStyle + ] + let attrString = NSAttributedString(string: message, attributes: attr) + messageTextField.attributedStringValue = attrString + let width = window?.frame.width ?? kWindowWidth + let rect = attrString.boundingRect(with: NSSize(width: width, height: 1600), options: .usesLineFragmentOrigin) + let height = rect.height + let x = messageTextField.frame.origin.x + let y = ((window?.frame.height ?? kWindowHeight) - height) / 2 + let newFrame = NSRect(x: x, y: y, width: width, height: height) + messageTextField.frame = newFrame + } + } + private var shouldStay: Bool = false + private var backgroundColor: NSColor = .black { + didSet { + self.window?.backgroundColor = backgroundColor + self.messageTextField.backgroundColor = backgroundColor + } + } + private var foregroundColor: NSColor = .white { + didSet { + self.messageTextField.textColor = foregroundColor + } + } + private var waitTimer: Timer? + private var fadeTimer: Timer? + + private static var instanceCount = 0 + private static var lastLocation = NSPoint.zero + + @objc public static func notify(message: String, stay: Bool = false) { + let controller = NotifierController() + controller.message = message + controller.shouldStay = stay + controller.show() + } + + private static func increaseInstanceCount() { + instanceCount += 1 + } + + private static func decreaseInstanceCount() { + instanceCount -= 1 + if instanceCount < 0 { + instanceCount = 0 + } + } + + private init() { + let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero + let contentRect = NSRect(x: 0, y: 0, width: kWindowWidth, height: kWindowHeight) + var windowRect = contentRect + windowRect.origin.x = screenRect.maxX - windowRect.width - 10 + windowRect.origin.y = screenRect.maxY - windowRect.height - 10 + let styleMask: NSWindow.StyleMask = [.borderless] + let panel = NotifierWindow(contentRect: windowRect, styleMask: styleMask, backing: .buffered, defer: false) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel)) + panel.hasShadow = true + panel.backgroundColor = backgroundColor + + messageTextField = NSTextField() + messageTextField.frame = contentRect + messageTextField.isEditable = false + messageTextField.isSelectable = false + messageTextField.isBezeled = false + messageTextField.textColor = foregroundColor + messageTextField.drawsBackground = true + messageTextField.backgroundColor = backgroundColor + messageTextField.font = .systemFont(ofSize: NSFont.systemFontSize(for: .small)) + panel.contentView?.addSubview(messageTextField) + + super.init(window: panel) + + panel.clickDelegate = self + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func show() { + func setStartLocation() { + if NotifierController.instanceCount == 0 { + return + } + let lastLocation = NotifierController.lastLocation + let screenRect = NSScreen.main?.visibleFrame ?? NSRect.zero + var windowRect = self.window?.frame ?? NSRect.zero + windowRect.origin.x = lastLocation.x + windowRect.origin.y = lastLocation.y - 10 - windowRect.height + + if windowRect.origin.y < screenRect.minY { + return + } + + self.window?.setFrame(windowRect, display: true) + } + + func moveIn() { + let afterRect = self.window?.frame ?? NSRect.zero + NotifierController.lastLocation = afterRect.origin + var beforeRect = afterRect + beforeRect.origin.y += 10 + window?.setFrame(beforeRect, display: true) + window?.orderFront(self) + window?.setFrame(afterRect, display: true, animate: true) + } + + setStartLocation() + moveIn() + NotifierController.increaseInstanceCount() + waitTimer = Timer.scheduledTimer(timeInterval: shouldStay ? 5 : 1, target: self, selector: #selector(fadeOut), userInfo: nil, repeats: false) + } + + @objc private func doFadeOut(_ timer: Timer) { + let opacity = self.window?.alphaValue ?? 0 + if opacity <= 0 { + self.close() + } else { + self.window?.alphaValue = opacity - 0.2 + } + } + + @objc private func fadeOut() { + waitTimer?.invalidate() + waitTimer = nil + NotifierController.decreaseInstanceCount() + fadeTimer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(doFadeOut(_:)), userInfo: nil, repeats: true) + } + + public override func close() { + waitTimer?.invalidate() + waitTimer = nil + fadeTimer?.invalidate() + fadeTimer = nil + super.close() + } + + fileprivate func windowDidBecomeClicked(_ window: NotifierWindow) { + self.fadeOut() + } +} diff --git a/Packages/OpenCCBridge/.gitignore b/Packages/OpenCCBridge/.gitignore new file mode 100644 index 00000000..bb460e7b --- /dev/null +++ b/Packages/OpenCCBridge/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/Packages/OpenCCBridge/Package.resolved b/Packages/OpenCCBridge/Package.resolved new file mode 100644 index 00000000..f306e3fb --- /dev/null +++ b/Packages/OpenCCBridge/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "SwiftyOpenCC", + "repositoryURL": "https://github.com/ddddxxx/SwiftyOpenCC.git", + "state": { + "branch": null, + "revision": "a802c02cbf1c6fcd529575f11a9876d94fc813f4", + "version": null + } + } + ] + }, + "version": 1 +} diff --git a/Packages/OpenCCBridge/Package.swift b/Packages/OpenCCBridge/Package.swift new file mode 100644 index 00000000..4c178660 --- /dev/null +++ b/Packages/OpenCCBridge/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "OpenCCBridge", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "OpenCCBridge", + targets: ["OpenCCBridge"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + .package(name: "SwiftyOpenCC", url: "https://github.com/ddddxxx/SwiftyOpenCC.git", revision: "a802c02cbf1c6fcd529575f11a9876d94fc813f4") + + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "OpenCCBridge", + dependencies: [ + .product(name: "OpenCC", package: "SwiftyOpenCC") + ]), + ] +) diff --git a/Packages/OpenCCBridge/README.md b/Packages/OpenCCBridge/README.md new file mode 100644 index 00000000..2a347ed0 --- /dev/null +++ b/Packages/OpenCCBridge/README.md @@ -0,0 +1,3 @@ +# OpenCCBridge + +A description of this package. diff --git a/Source/OpenCCBridge.swift b/Packages/OpenCCBridge/Sources/OpenCCBridge/OpenCCBridge.swift similarity index 66% rename from Source/OpenCCBridge.swift rename to Packages/OpenCCBridge/Sources/OpenCCBridge/OpenCCBridge.swift index f63a2726..4999a686 100644 --- a/Source/OpenCCBridge.swift +++ b/Packages/OpenCCBridge/Sources/OpenCCBridge/OpenCCBridge.swift @@ -3,20 +3,16 @@ import OpenCC // Since SwiftyOpenCC only provide Swift classes, we create an NSObject subclass // in Swift in order to bridge the Swift classes into our Objective-C++ project. -class OpenCCBridge: NSObject { +public class OpenCCBridge: NSObject { private static let shared = OpenCCBridge() private var converter: ChineseConverter? - override init() { + private override init() { try? converter = ChineseConverter(options: .simplify) super.init() } - @objc static func convert(_ string: String) -> String? { + @objc public static func convert(_ string: String) -> String? { shared.converter?.convert(string) } - - private func convert(_ string: String) -> String? { - converter?.convert(string) - } } diff --git a/Packages/OpenCCBridge/Tests/OpenCCBridgeTests/OpenCCBridgeTests.swift b/Packages/OpenCCBridge/Tests/OpenCCBridgeTests/OpenCCBridgeTests.swift new file mode 100644 index 00000000..2cda116c --- /dev/null +++ b/Packages/OpenCCBridge/Tests/OpenCCBridgeTests/OpenCCBridgeTests.swift @@ -0,0 +1,11 @@ +import XCTest +@testable import OpenCCBridge + +final class OpenCCBridgeTests: XCTestCase { + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct + // results. + XCTAssertEqual(OpenCCBridge().text, "Hello, World!") + } +} diff --git a/Packages/TooltipUI/.gitignore b/Packages/TooltipUI/.gitignore new file mode 100644 index 00000000..bb460e7b --- /dev/null +++ b/Packages/TooltipUI/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata diff --git a/Packages/TooltipUI/Package.swift b/Packages/TooltipUI/Package.swift new file mode 100644 index 00000000..a708808e --- /dev/null +++ b/Packages/TooltipUI/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TooltipUI", + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "TooltipUI", + targets: ["TooltipUI"]), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + // .package(url: /* package url */, from: "1.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "TooltipUI", + dependencies: []), + ] +) diff --git a/Packages/TooltipUI/README.md b/Packages/TooltipUI/README.md new file mode 100644 index 00000000..94ec7530 --- /dev/null +++ b/Packages/TooltipUI/README.md @@ -0,0 +1,3 @@ +# TooltipUI + +A description of this package. diff --git a/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift b/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift new file mode 100644 index 00000000..dbbbeb78 --- /dev/null +++ b/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift @@ -0,0 +1,65 @@ +import Cocoa + +public class TooltipController: NSWindowController { + private let backgroundColor = NSColor(calibratedHue: 0.16, saturation: 0.22, brightness: 0.97, alpha: 1.0) + private var messageTextField: NSTextField + private var tooltip: String = "" { + didSet { + messageTextField.stringValue = tooltip + adjustSize() + } + } + + public init() { + let contentRect = NSRect(x: 128.0, y: 128.0, width: 300.0, height: 20.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 + + messageTextField = NSTextField() + messageTextField.isEditable = false + messageTextField.isSelectable = false + messageTextField.isBezeled = false + messageTextField.textColor = .black + messageTextField.drawsBackground = true + messageTextField.backgroundColor = backgroundColor + messageTextField.font = .systemFont(ofSize: NSFont.systemFontSize(for: .small)) + panel.contentView?.addSubview(messageTextField) + + super.init(window: panel) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc(showTooltip:atPoint:) + public func show(tooltip: String, at point: NSPoint) { + self.tooltip = tooltip + window?.orderFront(nil) + set(windowLocation: point) + } + + @objc + public func hide() { + window?.orderOut(nil) + } + + private func set(windowLocation location: NSPoint) { + var newPoint = location + if location.y > 5 { + newPoint.y -= 5 + } + window?.setFrameTopLeftPoint(newPoint) + } + + private func adjustSize() { + let attrString = messageTextField.attributedStringValue; + var rect = attrString.boundingRect(with: NSSize(width: 1600.0, height: 1600.0), options: .usesLineFragmentOrigin) + rect.size.width += 10 + messageTextField.frame = rect + window?.setFrame(rect, display: true) + } + +} diff --git a/README.markdown b/README.markdown index f0fa1229..93497acb 100644 --- a/README.markdown +++ b/README.markdown @@ -2,6 +2,8 @@ ## 系統需求 +小麥注音輸入法可以在 macOS 10.10 以上版本運作。如果您要自行編譯小麥注音輸入法,或參與開發,您需要: + - macOS 10.15 Catalina 以上版本 - Xcode 12.0 以上版本 @@ -18,4 +20,3 @@ ## 軟體授權 本專案採用 MIT License 釋出,使用者可自由使用、散播本軟體,惟散播時必須完整保留版權聲明及軟體授權([詳全文](https://github.com/openvanilla/McBopomofo/blob/master/LICENSE.txt))。 - diff --git a/Source/InputMethodController.h b/Source/InputMethodController.h index 404c0ab4..10a18e5a 100644 --- a/Source/InputMethodController.h +++ b/Source/InputMethodController.h @@ -75,5 +75,6 @@ // if Chinese conversion is enabled BOOL _chineseConversionEnabled; + BOOL _halfWidthPunctuationEnabled; } @end diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index a7fe79a0..50140750 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -42,7 +42,9 @@ #import "McBopomofo-Swift.h" @import CandidateUI; -@import OpenCC; +@import NotifierUI; +@import TooltipUI; +@import OpenCCBridge; // C++ namespace usages using namespace std; @@ -78,6 +80,7 @@ static NSString *const kUseHorizontalCandidateListPreferenceKey = @"UseHorizonta static NSString *const kComposingBufferSizePreferenceKey = @"ComposingBufferSize"; static NSString *const kChooseCandidateUsingSpaceKey = @"ChooseCandidateUsingSpaceKey"; static NSString *const kChineseConversionEnabledKey = @"ChineseConversionEnabledKey"; +static NSString *const kHalfWidthPunctuationEnabledKey = @"HalfWidthPunctuationEnabledKey"; static NSString *const kEscToCleanInputBufferKey = @"EscToCleanInputBufferKey"; // advanced (usually optional) settings @@ -103,6 +106,16 @@ enum { kDeleteKeyCode = 117 }; +typedef NS_ENUM(NSUInteger, McBpomofoEmacsKey) { + McBpomofoEmacsKeyNone, + McBpomofoEmacsKeyForward, + McBpomofoEmacsKeyBackward, + McBpomofoEmacsKeyHome, + McBpomofoEmacsKeyEnd, + McBpomofoEmacsKeyDelete, + McBpomofoEmacsKeyNextPage, +}; + VTCandidateController *gCurrentCandidateController = nil; // if DEBUG is defined, a DOT file (GraphViz format) will be written to the @@ -189,6 +202,7 @@ static double FindHighestScore(const vector& nodes, double epsilon) _inputMode = kBopomofoModeIdentifier; _chineseConversionEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:kChineseConversionEnabledKey]; + _halfWidthPunctuationEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:kHalfWidthPunctuationEnabledKey]; } return self; @@ -201,11 +215,15 @@ static double FindHighestScore(const vector& nodes, double epsilon) NSMenuItem *preferenceMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"McBopomofo Preferences", @"") action:@selector(showPreferences:) keyEquivalent:@""]; [menu addItem:preferenceMenuItem]; - NSMenuItem *chineseConversionMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Chinese Conversion", @"") action:@selector(toggleChineseConverter:) keyEquivalent:@"G"]; + NSMenuItem *chineseConversionMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Chinese Conversion", @"") action:@selector(toggleChineseConverter:) keyEquivalent:@"g"]; chineseConversionMenuItem.keyEquivalentModifierMask = NSEventModifierFlagCommand | NSEventModifierFlagControl; chineseConversionMenuItem.state = _chineseConversionEnabled ? NSControlStateValueOn : NSControlStateValueOff; [menu addItem:chineseConversionMenuItem]; + NSMenuItem *halfWidthPunctuationMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Use Half-Width Punctuations", @"") action:@selector(toggleHalfWidthPunctuation:) keyEquivalent:@""]; + halfWidthPunctuationMenuItem.state = _halfWidthPunctuationEnabled ? NSControlStateValueOn : NSControlStateValueOff; + [menu addItem:halfWidthPunctuationMenuItem]; + [menu addItem:[NSMenuItem separatorItem]]; [menu addItemWithTitle:NSLocalizedString(@"User Phrases", @"") action:NULL keyEquivalent:@""]; @@ -319,6 +337,8 @@ static double FindHighestScore(const vector& nodes, double epsilon) gCurrentCandidateController.delegate = nil; gCurrentCandidateController.visible = NO; [_candidates removeAllObjects]; + + [self _hideTooltip]; } - (void)setValue:(id)value forTag:(long)tag client:(id)sender @@ -382,7 +402,8 @@ static double FindHighestScore(const vector& nodes, double epsilon) // Chinese conversion. NSString *buffer = _composingBuffer; - if (_chineseConversionEnabled) { + BOOL chineseConversionEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:kChineseConversionEnabledKey]; + if (chineseConversionEnabled) { buffer = [OpenCCBridge convert:_composingBuffer]; } @@ -393,6 +414,7 @@ static double FindHighestScore(const vector& nodes, double epsilon) [_composingBuffer setString:@""]; gCurrentCandidateController.visible = NO; [_candidates removeAllObjects]; + [self _hideTooltip]; } NS_INLINE size_t min(size_t a, size_t b) { return a < b ? a : b; } @@ -472,6 +494,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } // i.e. the client app needs to take care of where to put ths composing buffer [client setMarkedText:attrString selectionRange:NSMakeRange((NSInteger)_builder->markerCursorIndex(), 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; _latestReadingCursor = (NSInteger)_builder->markerCursorIndex(); + [self _showCurrentMarkedTextTooltipWithClient:client]; } else { // we must use NSAttributedString so that the cursor is visible -- @@ -484,6 +507,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } // i.e. the client app needs to take care of where to put ths composing buffer [client setMarkedText:attrString selectionRange:NSMakeRange(cursorIndex, 0) replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; _latestReadingCursor = cursorIndex; + [self _hideTooltip]; } } @@ -542,7 +566,8 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } NodeAnchor &anchor = _walkedNodes[0]; NSString *popedText = [NSString stringWithUTF8String:anchor.node->currentKeyValue().value.c_str()]; // Chinese conversion. - if (_chineseConversionEnabled) { + BOOL chineseConversionEnabled = [[NSUserDefaults standardUserDefaults] boolForKey:kChineseConversionEnabledKey]; + if (chineseConversionEnabled) { popedText = [OpenCCBridge convert:popedText]; } [client insertText:popedText replacementRange:NSMakeRange(NSNotFound, NSNotFound)]; @@ -559,7 +584,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } NSBeep(); } -- (string)currentLayout +- (string)_currentLayout { string layout = string("Standard_");; NSInteger keyboardLayout = [[NSUserDefaults standardUserDefaults] integerForKey:kKeyboardLayoutPreferenceKey]; @@ -588,45 +613,32 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return layout; } -- (NSString *)_currentMarkedText +- (McBpomofoEmacsKey)_detectEmacsKeyFromCharCode:(UniChar)charCode modifiers:(NSUInteger)flags { - if (_builder->markerCursorIndex() < 0) { - return @""; + if (flags & NSControlKeyMask) { + char c = charCode + 'a' - 1; + if (c == 'a') { + return McBpomofoEmacsKeyHome; + } + else if (c == 'e') { + return McBpomofoEmacsKeyEnd; + } + else if (c == 'f') { + return McBpomofoEmacsKeyForward; + } + else if (c == 'b') { + return McBpomofoEmacsKeyBackward; + } + else if (c == 'd') { + return McBpomofoEmacsKeyDelete; + } + else if (c == 'v') { + return McBpomofoEmacsKeyNextPage; + } } - if (!_bpmfReadingBuffer->isEmpty()) { - return @""; - } - - size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); - size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); - // A phrase should contian at least two characters. - if (end - begin < 2) { - return @""; - } - - NSRange range = NSMakeRange((NSInteger)begin, (NSInteger)(end - begin)); - NSString *reading = [_composingBuffer substringWithRange:range]; - NSMutableString *string = [[NSMutableString alloc] init]; - [string appendString:reading]; - [string appendString:@" "]; - NSMutableArray *readingsArray = [[NSMutableArray alloc] init]; - vector v = _builder->readingsAtRange(begin, end); - for(vector::iterator it_i=v.begin(); it_i!=v.end(); ++it_i) { - [readingsArray addObject:[NSString stringWithUTF8String:it_i->c_str()]]; - } - [string appendString:[readingsArray componentsJoinedByString:@"-"]]; - return string; + return McBpomofoEmacsKeyNone; } -- (BOOL)_writeUserPhrase -{ - NSString *currentMarkedPhrase = [self _currentMarkedText]; - if (![currentMarkedPhrase length]) { - return NO; - } - - return [LanguageModelManager writeUserPhrase:currentMarkedPhrase]; -} - (BOOL)handleInputText:(NSString*)inputText key:(NSInteger)keyCode modifiers:(NSUInteger)flags client:(id)client { @@ -653,6 +665,8 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } // get the unicode character code UniChar charCode = [inputText length] ? [inputText characterAtIndex:0] : 0; + McBpomofoEmacsKey emacsKey = [self _detectEmacsKeyFromCharCode:charCode modifiers:flags]; + if ([[client bundleIdentifier] isEqualToString:@"com.apple.Terminal"] && [NSStringFromClass([client class]) isEqualToString:@"IPMDServerClientWrapper"]) { // special handling for com.apple.Terminal _currentDeferredClient = client; @@ -664,7 +678,9 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } // if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it - if (![_composingBuffer length] && _bpmfReadingBuffer->isEmpty() && ((flags & NSCommandKeyMask) || (flags & NSControlKeyMask) || (flags & NSAlternateKeyMask) || (flags & NSNumericPadKeyMask))) { + if (![_composingBuffer length] && + _bpmfReadingBuffer->isEmpty() && + ((flags & NSCommandKeyMask) || (flags & NSControlKeyMask) || (flags & NSAlternateKeyMask) || (flags & NSNumericPadKeyMask))) { return NO; } @@ -708,7 +724,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } // if we have candidate, it means we need to pass the event to the candidate handler if ([_candidates count]) { - return [self handleCandidateEventWithInputText:inputText charCode:charCode keyCode:keyCode]; + return [self _handleCandidateEventWithInputText:inputText charCode:charCode keyCode:keyCode emacsKey:(McBpomofoEmacsKey)emacsKey]; } // If we have marker index. @@ -731,7 +747,8 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } // Shift + left - if (keyCode == cursorBackwardKey && (flags & NSShiftKeyMask)) { + if ((keyCode == cursorBackwardKey || emacsKey == McBpomofoEmacsKeyBackward) + && (flags & NSShiftKeyMask)) { if (_builder->markerCursorIndex() > 0) { _builder->setMarkerCursorIndex(_builder->markerCursorIndex() - 1); } @@ -742,7 +759,8 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } // Shift + Right - if (keyCode == cursorForwardKey && (flags & NSShiftKeyMask)) { + if ((keyCode == cursorForwardKey || emacsKey == McBpomofoEmacsKeyForward) + && (flags & NSShiftKeyMask)) { if (_builder->markerCursorIndex() < _builder->length()) { _builder->setMarkerCursorIndex(_builder->markerCursorIndex() + 1); } @@ -875,7 +893,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } // handle cursor backward - if (keyCode == cursorBackwardKey) { + if (keyCode == cursorBackwardKey || emacsKey == McBpomofoEmacsKeyBackward) { if (!_bpmfReadingBuffer->isEmpty()) { [self beep]; } @@ -907,7 +925,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } // handle cursor forward - if (keyCode == cursorForwardKey) { + if (keyCode == cursorForwardKey || emacsKey == McBpomofoEmacsKeyForward) { if (!_bpmfReadingBuffer->isEmpty()) { [self beep]; } @@ -937,7 +955,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } - if (keyCode == kHomeKeyCode) { + if (keyCode == kHomeKeyCode || emacsKey == McBpomofoEmacsKeyHome) { if (!_bpmfReadingBuffer->isEmpty()) { [self beep]; } @@ -958,7 +976,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } - if (keyCode == kEndKeyCode) { + if (keyCode == kEndKeyCode || emacsKey == McBpomofoEmacsKeyEnd) { if (!_bpmfReadingBuffer->isEmpty()) { [self beep]; } @@ -1011,7 +1029,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } // Delete - if (keyCode == kDeleteKeyCode) { + if (keyCode == kDeleteKeyCode || emacsKey == McBpomofoEmacsKeyDelete) { if (_bpmfReadingBuffer->isEmpty()) { if (![_composingBuffer length]) { return NO; @@ -1033,7 +1051,6 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } - // Enter if (charCode == 13) { if (![_composingBuffer length]) { @@ -1061,15 +1078,16 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } // if nothing is matched, see if it's a punctuation key for current layout. - string layout = [self currentLayout]; - string customPunctuation = string("_punctuation_") + layout + string(1, (char)charCode); - if ([self handlePunctuation:customPunctuation usingVerticalMode:useVerticalMode client:client]) { + string layout = [self _currentLayout]; + string punctuationNamePrefix = (_halfWidthPunctuationEnabled ? string("_half_punctuation_"): string("_punctuation_")); + string customPunctuation = punctuationNamePrefix + layout + string(1, (char)charCode); + if ([self _handlePunctuation:customPunctuation usingVerticalMode:useVerticalMode client:client]) { return YES; } // if nothing is matched, see if it's a punctuation key. - string punctuation = string("_punctuation_") + string(1, (char)charCode); - if ([self handlePunctuation:punctuation usingVerticalMode:useVerticalMode client:client]) { + string punctuation = punctuationNamePrefix + string(1, (char)charCode); + if ([self _handlePunctuation:punctuation usingVerticalMode:useVerticalMode client:client]) { return YES; } @@ -1085,7 +1103,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return NO; } -- (BOOL)handlePunctuation:(string)customPunctuation usingVerticalMode:(BOOL)useVerticalMode client:(id)client +- (BOOL)_handlePunctuation:(string)customPunctuation usingVerticalMode:(BOOL)useVerticalMode client:(id)client { if (_languageModel->hasUnigramsForKey(customPunctuation)) { if (_bpmfReadingBuffer->isEmpty()) { @@ -1111,7 +1129,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return NO; } -- (BOOL)handleCandidateEventWithInputText:(NSString *)inputText charCode:(UniChar)charCode keyCode:(NSUInteger)keyCode +- (BOOL)_handleCandidateEventWithInputText:(NSString *)inputText charCode:(UniChar)charCode keyCode:(NSUInteger)keyCode emacsKey:(McBpomofoEmacsKey)emacsKey { BOOL cancelCandidateKey = (charCode == 27) || @@ -1134,7 +1152,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self candidateController:gCurrentCandidateController didSelectCandidateAtIndex:gCurrentCandidateController.selectedCandidateIndex]; return YES; } - else if (charCode == 32 || keyCode == kPageDownKeyCode) { + else if (charCode == 32 || keyCode == kPageDownKeyCode || emacsKey == McBpomofoEmacsKeyNextPage) { BOOL updated = [gCurrentCandidateController showNextPage]; if (!updated) { [self beep]; @@ -1168,6 +1186,14 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } } + else if (emacsKey == McBpomofoEmacsKeyBackward) { + BOOL updated = [gCurrentCandidateController highlightPreviousCandidate]; + if (!updated) { + [self beep]; + } + [self updateClientComposingBuffer:_currentCandidateClient]; + return YES; + } else if (keyCode == kRightKeyCode) { if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { BOOL updated = [gCurrentCandidateController highlightNextCandidate]; @@ -1186,6 +1212,14 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } } + else if (emacsKey == McBpomofoEmacsKeyForward) { + BOOL updated = [gCurrentCandidateController highlightNextCandidate]; + if (!updated) { + [self beep]; + } + [self updateClientComposingBuffer:_currentCandidateClient]; + return YES; + } else if (keyCode == kUpKeyCode) { if ([gCurrentCandidateController isKindOfClass:[VTHorizontalCandidateController class]]) { BOOL updated = [gCurrentCandidateController showPreviousPage]; @@ -1222,7 +1256,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } return YES; } } - else if (keyCode == kHomeKeyCode) { + else if (keyCode == kHomeKeyCode || emacsKey == McBpomofoEmacsKeyHome) { if (gCurrentCandidateController.selectedCandidateIndex == 0) { [self beep]; @@ -1234,7 +1268,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } [self updateClientComposingBuffer:_currentCandidateClient]; return YES; } - else if (keyCode == kEndKeyCode && [_candidates count] > 0) { + else if ((keyCode == kEndKeyCode || emacsKey == McBpomofoEmacsKeyEnd) && [_candidates count] > 0) { if (gCurrentCandidateController.selectedCandidateIndex == [_candidates count] - 1) { [self beep]; } @@ -1264,7 +1298,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } } if (_inputMode == kPlainBopomofoModeIdentifier) { - string layout = [self currentLayout]; + string layout = [self _currentLayout]; string customPunctuation = string("_punctuation_") + layout + string(1, (char)charCode); string punctuation = string("_punctuation_") + string(1, (char)charCode); @@ -1333,24 +1367,30 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } + (VTHorizontalCandidateController *)horizontalCandidateController { static VTHorizontalCandidateController *instance = nil; - @synchronized(self) { - if (!instance) { - instance = [[VTHorizontalCandidateController alloc] init]; - } - } - + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[VTHorizontalCandidateController alloc] init]; + }); return instance; } + (VTVerticalCandidateController *)verticalCandidateController { static VTVerticalCandidateController *instance = nil; - @synchronized(self) { - if (!instance) { - instance = [[VTVerticalCandidateController alloc] init]; - } - } + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[VTVerticalCandidateController alloc] init]; + }); + return instance; +} ++ (TooltipController *)tooltipController +{ + static TooltipController *instance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + instance = [[TooltipController alloc] init]; + }); return instance; } @@ -1474,6 +1514,113 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } gCurrentCandidateController.visible = YES; } +#pragma mark - User phrases + +- (NSString *)_currentMarkedText +{ + if (_builder->markerCursorIndex() < 0) { + return @""; + } + if (!_bpmfReadingBuffer->isEmpty()) { + return @""; + } + + size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); + size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); + // A phrase should contian at least two characters. + if (end - begin < 1) { + return @""; + } + + NSRange range = NSMakeRange((NSInteger)begin, (NSInteger)(end - begin)); + NSString *selectedText = [_composingBuffer substringWithRange:range]; + return selectedText; +} + +- (NSString *)_currentMarkedTextAndReadings +{ + if (_builder->markerCursorIndex() < 0) { + return @""; + } + if (!_bpmfReadingBuffer->isEmpty()) { + return @""; + } + + size_t begin = min(_builder->markerCursorIndex(), _builder->cursorIndex()); + size_t end = max(_builder->markerCursorIndex(), _builder->cursorIndex()); + // A phrase should contian at least two characters. + if (end - begin < 2) { + return @""; + } + + NSRange range = NSMakeRange((NSInteger)begin, (NSInteger)(end - begin)); + NSString *selectedText = [_composingBuffer substringWithRange:range]; + NSMutableString *string = [[NSMutableString alloc] init]; + [string appendString:selectedText]; + [string appendString:@" "]; + NSMutableArray *readingsArray = [[NSMutableArray alloc] init]; + vector v = _builder->readingsAtRange(begin, end); + for(vector::iterator it_i=v.begin(); it_i!=v.end(); ++it_i) { + [readingsArray addObject:[NSString stringWithUTF8String:it_i->c_str()]]; + } + [string appendString:[readingsArray componentsJoinedByString:@"-"]]; + return string; +} + +- (BOOL)_writeUserPhrase +{ + NSString *currentMarkedPhrase = [self _currentMarkedTextAndReadings]; + if (![currentMarkedPhrase length]) { + return NO; + } + + return [LanguageModelManager writeUserPhrase:currentMarkedPhrase]; +} + +- (void)_showCurrentMarkedTextTooltipWithClient:(id)client +{ + NSString *text = [self _currentMarkedText]; + NSInteger length = text.length; + if (!length) { + [self _hideTooltip]; + } + else if (length == 1) { + NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". You can add a phrase with two or more characters.", @""), text]; + [self _showTooltip:messsage client:client]; + } + else { + NSString *messsage = [NSString stringWithFormat:NSLocalizedString(@"You are now selecting \"%@\". Press enter to add a new phrase.", @""), text]; + [self _showTooltip:messsage client:client]; + } +} + +- (void)_showTooltip:(NSString *)tooltip client:(id)client +{ + NSRect lineHeightRect = NSMakeRect(0.0, 0.0, 16.0, 16.0); + + NSInteger cursor = _latestReadingCursor; + if (cursor == [_composingBuffer length] && cursor != 0) { + cursor--; + } + + // some apps (e.g. Twitter for Mac's search bar) handle this call incorrectly, hence the try-catch + @try { + [client attributesForCharacterIndex:cursor lineHeightRectangle:&lineHeightRect]; + } + @catch (NSException *exception) { + NSLog(@"%@", exception); + } + + [[McBopomofoInputMethodController tooltipController] showTooltip:tooltip atPoint:lineHeightRect.origin]; +} + +- (void)_hideTooltip +{ + if ([McBopomofoInputMethodController tooltipController].window.isVisible) { + [[McBopomofoInputMethodController tooltipController] hide]; + } +} + #pragma mark - Misc menu items - (void)showPreferences:(id)sender @@ -1546,8 +1693,20 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } { _chineseConversionEnabled = !_chineseConversionEnabled; [[NSUserDefaults standardUserDefaults] setBool:_chineseConversionEnabled forKey:kChineseConversionEnabledKey]; + + [NotifierController notifyWithMessage: + _chineseConversionEnabled ? + NSLocalizedString(@"Chinese conversion on", @"") : + NSLocalizedString(@"Chinese conversion off", @"") stay:NO]; } +- (void)toggleHalfWidthPunctuation:(id)sender +{ + _halfWidthPunctuationEnabled = !_halfWidthPunctuationEnabled; + [[NSUserDefaults standardUserDefaults] setBool:_halfWidthPunctuationEnabled forKey:kHalfWidthPunctuationEnabledKey]; +} + + @end #pragma mark - diff --git a/Source/Installer/AppDelegate.m b/Source/Installer/AppDelegate.m index 62f7acc5..84dbd224 100644 --- a/Source/Installer/AppDelegate.m +++ b/Source/Installer/AppDelegate.m @@ -27,7 +27,8 @@ #import "AppDelegate.h" #import -#import "McBopomofoInstaller-Swift.h" +//#import "McBopomofoInstaller-Swift.h" +@import InputSourceHelper; static NSString *const kTargetBin = @"McBopomofo"; static NSString *const kTargetType = @"app"; diff --git a/Source/en.lproj/Localizable.strings b/Source/en.lproj/Localizable.strings index a9b5048e..7aa8d586 100644 --- a/Source/en.lproj/Localizable.strings +++ b/Source/en.lproj/Localizable.strings @@ -62,3 +62,13 @@ "Please check the permission of at \"%@\"." = "Please check the permission of at \"%@\"."; "Edit Excluded Phrases" = "Edit Excluded Phrases"; + +"Use Half-Width Punctuations" = "Use Half-Width Punctuations"; + +"You are now selecting \"%@\". You can add a phrase with two or more characters." = "You are now selecting \"%@\". You can add a phrase with two or more characters."; + +"You are now selecting \"%@\". Press enter to add a new phrase." = "You are now selecting \"%@\". Press enter to add a new phrase."; + +"Chinese conversion on" = "Chinese conversion on"; + +"Chinese conversion off" = "Chinese conversion off"; diff --git a/Source/main.m b/Source/main.m index a98863b0..f65d13a7 100644 --- a/Source/main.m +++ b/Source/main.m @@ -34,7 +34,7 @@ #import #import -#import "McBopomofo-Swift.h" +@import InputSourceHelper; static NSString *const kConnectionName = @"McBopomofo_1_Connection"; diff --git a/Source/zh-Hant.lproj/Localizable.strings b/Source/zh-Hant.lproj/Localizable.strings index cdd2b6d2..98664234 100644 --- a/Source/zh-Hant.lproj/Localizable.strings +++ b/Source/zh-Hant.lproj/Localizable.strings @@ -63,3 +63,12 @@ "Edit Excluded Phrases" = "編輯要排除的詞彙"; +"Use Half-Width Punctuations" = "使用半型標點符號"; + +"You are now selecting \"%@\". You can add a phrase with two or more characters." = "您目前選擇了 \"%@\"。請選擇兩個字以上,才能加入使用者詞彙。"; + +"You are now selecting \"%@\". Press enter to add a new phrase." = "您目前選擇了 \"%@\"。按下 Enter 就可以加入到使用者詞彙中。"; + +"Chinese conversion on" = "已經切換到簡體中文模式"; + +"Chinese conversion off" = "已經切換到繁體中文模式";