From d4772ffa992ce40accabc4a5547ce4d2e9ec18fc Mon Sep 17 00:00:00 2001 From: zonble Date: Thu, 13 Jan 2022 23:38:56 +0800 Subject: [PATCH] Adds notifier UI to notify user Chinese conversion on/off. --- McBopomofo.xcodeproj/project.pbxproj | 9 + Packages/NotifierUI/.gitignore | 7 + Packages/NotifierUI/Package.swift | 25 +++ Packages/NotifierUI/README.md | 3 + .../NotifierUI/NotifierController.swift | 171 ++++++++++++++++++ .../Sources/TooltipUI/TooltipController.swift | 2 +- Source/InputMethodController.mm | 6 + Source/en.lproj/Localizable.strings | 4 + Source/zh-Hant.lproj/Localizable.strings | 4 + 9 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 Packages/NotifierUI/.gitignore create mode 100644 Packages/NotifierUI/Package.swift create mode 100644 Packages/NotifierUI/README.md create mode 100644 Packages/NotifierUI/Sources/NotifierUI/NotifierController.swift diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index 99420ef4..bd96b94c 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -43,6 +43,7 @@ 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 */; }; 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 */; }; @@ -170,6 +171,7 @@ 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 = ""; }; 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 = ""; }; @@ -186,6 +188,7 @@ D48550A325EBE689006A204C /* OpenCC in Frameworks */, D427F7A927905E90004A2160 /* TooltipUI in Frameworks */, D427F76A278C9E29004A2160 /* CandidateUI in Frameworks */, + D427F7AE27907B8A004A2160 /* NotifierUI in Frameworks */, 6A0D4EA715FC0D2D00ABF4B3 /* Cocoa.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -397,6 +400,7 @@ children = ( D427F768278C9D0D004A2160 /* CandidateUI */, D427F7A727905E43004A2160 /* TooltipUI */, + D427F7AC27907B7E004A2160 /* NotifierUI */, ); name = Packages; sourceTree = ""; @@ -439,6 +443,7 @@ D48550A225EBE689006A204C /* OpenCC */, D427F769278C9E29004A2160 /* CandidateUI */, D427F7A827905E90004A2160 /* TooltipUI */, + D427F7AD27907B8A004A2160 /* NotifierUI */, ); productName = McBopomofo; productReference = 6A0D4EA215FC0D2D00ABF4B3 /* McBopomofo.app */; @@ -1068,6 +1073,10 @@ isa = XCSwiftPackageProductDependency; productName = TooltipUI; }; + D427F7AD27907B8A004A2160 /* NotifierUI */ = { + isa = XCSwiftPackageProductDependency; + productName = NotifierUI; + }; D48550A225EBE689006A204C /* OpenCC */ = { isa = XCSwiftPackageProductDependency; package = D48550A125EBE689006A204C /* XCRemoteSwiftPackageReference "SwiftyOpenCC" */; 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..ae1bedc9 --- /dev/null +++ b/Packages/NotifierUI/Sources/NotifierUI/NotifierController.swift @@ -0,0 +1,171 @@ +import Cocoa + +protocol NotifierWindowDelegate: AnyObject { + func windowDidBecomeClicked(_ window: NotifierWindow) +} + +class NotifierWindow: NSWindow { + weak var clickDelegate: NotifierWindowDelegate? + + override func mouseDown(with event: NSEvent) { + clickDelegate?.windowDidBecomeClicked(self) + } +} + +let kWindowWidth: CGFloat = 160.0 +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() + } + + static func increaseInstanceCount() { + instanceCount += 1 + } + + static func decreaseInstanceCount() { + instanceCount -= 1 + if instanceCount < 0 { + instanceCount = 0 + } + } + + public 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") + } + + 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) + } + + func show() { + setStartLocation() + moveIn() + NotifierController.increaseInstanceCount() + waitTimer = Timer.scheduledTimer(timeInterval: shouldStay ? 5 : 1, target: self, selector: #selector(fadeOut), userInfo: nil, repeats: false) + } + + @objc func doFadeOut(_ timer: Timer) { + let opacity = self.window?.alphaValue ?? 0 + if opacity <= 0 { + self.close() + } else { + self.window?.alphaValue = opacity - 0.2 + } + } + + @objc 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() + } + + func windowDidBecomeClicked(_ window: NotifierWindow) { + self.fadeOut() + } +} diff --git a/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift b/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift index 337ac026..0103ec12 100644 --- a/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift +++ b/Packages/TooltipUI/Sources/TooltipUI/TooltipController.swift @@ -14,7 +14,7 @@ public class TooltipController: NSWindowController { 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)) + panel.level = NSWindow.Level(Int(kCGPopUpMenuWindowLevel) + 1) panel.hasShadow = true messageTextField = NSTextField() diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index 09439089..1b02accd 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -42,6 +42,7 @@ #import "McBopomofo-Swift.h" @import CandidateUI; +@import NotifierUI; @import TooltipUI; @import OpenCC; @@ -1692,6 +1693,11 @@ 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 diff --git a/Source/en.lproj/Localizable.strings b/Source/en.lproj/Localizable.strings index 0a96077b..7aa8d586 100644 --- a/Source/en.lproj/Localizable.strings +++ b/Source/en.lproj/Localizable.strings @@ -68,3 +68,7 @@ "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/zh-Hant.lproj/Localizable.strings b/Source/zh-Hant.lproj/Localizable.strings index a91f7bc9..98664234 100644 --- a/Source/zh-Hant.lproj/Localizable.strings +++ b/Source/zh-Hant.lproj/Localizable.strings @@ -68,3 +68,7 @@ "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" = "已經切換到繁體中文模式";