vChewing-macOS/Packages/vChewing_NotifierUI/Sources/NotifierUI/NotifierUI.swift

194 lines
6.9 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Cocoa
public class Notifier: NSWindowController {
public static func notify(message: String) {
DispatchQueue.main.async { Self.message = message }
}
static var message: String = "" {
didSet {
if !Self.message.isEmpty {
Self.message = Notifier(message).blankValue
}
}
}
private var currentMessage: String //
private var isNew = true //
// MARK: - Private Declarations
private static var instanceSet: NSMutableOrderedSet = .init()
private let blankValue = ""
@discardableResult private init(_ message: String) {
currentMessage = message
let rawMessage = message.replacingOccurrences(of: "\n", with: "")
let isDuplicated: Bool = {
if let firstInstanceExisted = Self.instanceSet.firstNotifier {
return message == firstInstanceExisted.currentMessage && firstInstanceExisted.isNew
}
return false
}()
guard let screenRect = NSScreen.main?.visibleFrame, !rawMessage.isEmpty, !isDuplicated else {
super.init(window: nil)
return
}
// Swift
while Self.instanceSet.count > 3 {
if let instanceToRemove = Self.instanceSet.lastNotifier {
instanceToRemove.close()
Self.instanceSet.remove(instanceToRemove)
}
}
//
defer {
// 0.3 false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.isNew = false
}
}
let kLargeFontSize: Double = 17
let kSmallFontSize: Double = 15
let messageArray = message.components(separatedBy: "\n")
let paraStyle = NSMutableParagraphStyle()
paraStyle.setParagraphStyle(NSParagraphStyle.default)
paraStyle.alignment = .center
let attrTitle: [NSAttributedString.Key: AnyObject] = [
.foregroundColor: NSColor.controlTextColor,
.font: NSFont.boldSystemFont(ofSize: kLargeFontSize),
.paragraphStyle: paraStyle,
]
let attrString = NSMutableAttributedString(string: messageArray[0], attributes: attrTitle)
let attrAlt: [NSAttributedString.Key: AnyObject] = [
.foregroundColor: NSColor.secondaryLabelColor,
.font: NSFont.systemFont(ofSize: kSmallFontSize),
.paragraphStyle: paraStyle,
]
let additionalString = messageArray.count > 1 ? "\n\(messageArray[1])" : ""
let attrStringAlt = NSMutableAttributedString(string: additionalString, attributes: attrAlt)
attrString.insert(attrStringAlt, at: attrString.length)
let textRect: NSRect = attrString.boundingRect(
with: NSSize(width: 1600.0, height: 1600.0), options: [.usesLineFragmentOrigin]
)
let windowWidth = Double(4) * kLargeFontSize + textRect.width
let contentRect = NSRect(x: 0, y: 0, width: windowWidth, height: 60.0)
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 transparentVisualEffect = NSVisualEffectView()
transparentVisualEffect.blendingMode = .behindWindow
transparentVisualEffect.state = .active
let theWindow = NSWindow(
contentRect: windowRect, styleMask: styleMask, backing: .buffered, defer: false
)
theWindow.contentView = transparentVisualEffect
theWindow.isMovableByWindowBackground = true
theWindow.level = NSWindow.Level(Int(max(CGShieldingWindowLevel(), kCGPopUpMenuWindowLevel)))
theWindow.hasShadow = true
theWindow.backgroundColor = .textBackgroundColor
theWindow.title = ""
theWindow.titlebarAppearsTransparent = true
theWindow.titleVisibility = .hidden
theWindow.showsToolbarButton = false
theWindow.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true
theWindow.standardWindowButton(NSWindow.ButtonType.miniaturizeButton)?.isHidden = true
theWindow.standardWindowButton(NSWindow.ButtonType.closeButton)?.isHidden = true
theWindow.standardWindowButton(NSWindow.ButtonType.zoomButton)?.isHidden = true
theWindow.isReleasedWhenClosed = true
theWindow.isMovable = false
let lblMessage = NSTextField()
lblMessage.attributedStringValue = attrString
lblMessage.drawsBackground = false
lblMessage.font = .boldSystemFont(ofSize: NSFont.systemFontSize(for: .regular))
lblMessage.frame = contentRect
lblMessage.isBezeled = false
lblMessage.isEditable = false
lblMessage.isSelectable = false
lblMessage.textColor = .controlTextColor
theWindow.contentView?.addSubview(lblMessage)
let x = lblMessage.frame.origin.x
let y = ((theWindow.frame.height) - textRect.height) / 1.9
let newFrame = NSRect(x: x, y: y, width: theWindow.frame.width, height: textRect.height)
lblMessage.frame = newFrame
super.init(window: theWindow)
display()
}
@available(*, unavailable)
required init?(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func close() {
super.close()
}
}
// MARK: - Private Functions
extension Notifier {
private func shiftExistingWindowPositions() {
guard let window = window else { return }
Self.instanceSet.arrayOfWindows.forEach { theInstanceWindow in
var theOrigin = theInstanceWindow.frame
theOrigin.origin.y -= (10 + window.frame.height)
theInstanceWindow.setFrame(theOrigin, display: true)
}
}
private func fadeIn() {
guard let window = window else { return }
let afterRect = window.frame
var beforeRect = afterRect
beforeRect.origin.x -= 20
window.setFrame(beforeRect, display: true)
window.orderFront(self)
window.setFrame(afterRect, display: true, animate: true)
}
private func display() {
Self.instanceSet.arrayOfWindows.forEach {
$0.alphaValue -= 0.1
$0.contentView?.subviews.forEach { $0.alphaValue *= 0.5 }
}
shiftExistingWindowPositions()
fadeIn()
Self.instanceSet.insert(self, at: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.3) {
self.close()
Self.instanceSet.remove(self)
}
}
}
private extension NSMutableOrderedSet {
var arrayOfWindows: [NSWindow] { compactMap { ($0 as? Notifier)?.window } }
var firstNotifier: Notifier? {
for neta in self { if let result = neta as? Notifier { return result } }
return nil
}
var lastNotifier: Notifier? {
for neta in reversed { if let result = neta as? Notifier { return result } }
return nil
}
}