mirror of
https://github.com/morgan9e/macos-stats
synced 2026-04-14 00:04:15 +09:00
443 lines
16 KiB
Swift
443 lines
16 KiB
Swift
//
|
|
// popup.swift
|
|
// Kit
|
|
//
|
|
// Created by Serhiy Mytrovtsiy on 11/04/2020.
|
|
// Using Swift 5.0.
|
|
// Running on macOS 10.15.
|
|
//
|
|
// Copyright © 2020 Serhiy Mytrovtsiy. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
|
|
public protocol Popup_p: NSView {
|
|
var keyboardShortcut: [UInt16] { get }
|
|
var sizeCallback: ((NSSize) -> Void)? { get set }
|
|
|
|
func settings() -> NSView?
|
|
|
|
func appear()
|
|
func disappear()
|
|
func setKeyboardShortcut(_ binding: [UInt16])
|
|
}
|
|
|
|
open class PopupWrapper: NSStackView, Popup_p {
|
|
public var title: String
|
|
public var keyboardShortcut: [UInt16] = []
|
|
open var sizeCallback: ((NSSize) -> Void)? = nil
|
|
|
|
public init(_ typ: ModuleType, frame: NSRect) {
|
|
self.title = typ.stringValue
|
|
self.keyboardShortcut = Store.shared.array(key: "\(typ.stringValue)_popup_keyboardShortcut", defaultValue: []) as? [UInt16] ?? []
|
|
|
|
super.init(frame: frame)
|
|
}
|
|
|
|
required public init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
open func settings() -> NSView? { return nil }
|
|
open func appear() {}
|
|
open func disappear() {}
|
|
|
|
open func setKeyboardShortcut(_ binding: [UInt16]) {
|
|
self.keyboardShortcut = binding
|
|
Store.shared.set(key: "\(self.title)_popup_keyboardShortcut", value: binding)
|
|
}
|
|
}
|
|
|
|
public class PopupWindow: NSWindow, NSWindowDelegate {
|
|
private let viewController: PopupViewController
|
|
internal var locked: Bool = false
|
|
internal var openedBy: widget_t? = nil
|
|
|
|
public init(title: String, module: ModuleType, view: Popup_p?, visibilityCallback: @escaping (_ state: Bool) -> Void) {
|
|
self.viewController = PopupViewController(module: module)
|
|
self.viewController.setup(title: title, view: view)
|
|
|
|
super.init(
|
|
contentRect: NSRect(
|
|
x: 0,
|
|
y: 0,
|
|
width: self.viewController.view.frame.width,
|
|
height: self.viewController.view.frame.height
|
|
),
|
|
styleMask: [.titled, .fullSizeContentView],
|
|
backing: .buffered,
|
|
defer: true
|
|
)
|
|
|
|
self.viewController.visibilityCallback = { [weak self] state in
|
|
self?.locked = false
|
|
visibilityCallback(state)
|
|
}
|
|
|
|
self.title = title
|
|
self.titleVisibility = .hidden
|
|
self.contentViewController = self.viewController
|
|
self.titlebarAppearsTransparent = true
|
|
self.animationBehavior = .default
|
|
self.collectionBehavior = .moveToActiveSpace
|
|
self.backgroundColor = .clear
|
|
self.hasShadow = true
|
|
self.setIsVisible(false)
|
|
self.delegate = self
|
|
}
|
|
|
|
public func windowWillMove(_ notification: Notification) {
|
|
self.viewController.setCloseButton(true)
|
|
self.locked = true
|
|
}
|
|
|
|
public func windowDidResignKey(_ notification: Notification) {
|
|
if self.locked {
|
|
return
|
|
}
|
|
|
|
self.viewController.setCloseButton(false)
|
|
self.setIsVisible(false)
|
|
}
|
|
}
|
|
|
|
internal class PopupViewController: NSViewController {
|
|
fileprivate var visibilityCallback: (_ state: Bool) -> Void = {_ in }
|
|
private var popup: PopupView
|
|
|
|
public init(module: ModuleType) {
|
|
self.popup = PopupView(frame: NSRect(
|
|
x: 0,
|
|
y: 0,
|
|
width: Constants.Popup.width + (Constants.Popup.margins * 2),
|
|
height: Constants.Popup.height+Constants.Popup.headerHeight
|
|
), module: module)
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func loadView() {
|
|
self.view = self.popup
|
|
}
|
|
|
|
override func viewWillAppear() {
|
|
super.viewWillAppear()
|
|
|
|
self.popup.appear()
|
|
self.visibilityCallback(true)
|
|
}
|
|
|
|
override func viewWillDisappear() {
|
|
super.viewWillDisappear()
|
|
|
|
self.popup.disappear()
|
|
self.visibilityCallback(false)
|
|
}
|
|
|
|
fileprivate func setup(title: String, view: Popup_p?) {
|
|
self.title = title
|
|
self.popup.setTitle(title)
|
|
self.popup.setView(view)
|
|
}
|
|
|
|
fileprivate func setCloseButton(_ state: Bool) {
|
|
self.popup.setCloseButton(state)
|
|
}
|
|
}
|
|
|
|
internal class PopupView: NSView {
|
|
private var view: Popup_p? = nil
|
|
|
|
private var foreground: NSVisualEffectView
|
|
private var background: NSView
|
|
|
|
private let header: HeaderView
|
|
private let body: NSScrollView
|
|
|
|
override var intrinsicContentSize: CGSize {
|
|
return CGSize(width: self.frame.width, height: self.frame.height)
|
|
}
|
|
private var windowHeight: CGFloat?
|
|
private var containerHeight: CGFloat?
|
|
|
|
init(frame: NSRect, module: ModuleType) {
|
|
self.header = HeaderView(frame: NSRect(
|
|
x: 0,
|
|
y: frame.height - Constants.Popup.headerHeight,
|
|
width: frame.width,
|
|
height: Constants.Popup.headerHeight
|
|
), module: module)
|
|
self.body = NSScrollView(frame: NSRect(
|
|
x: Constants.Popup.margins,
|
|
y: Constants.Popup.margins,
|
|
width: frame.width - Constants.Popup.margins*2,
|
|
height: frame.height - self.header.frame.height - Constants.Popup.margins*2
|
|
))
|
|
self.windowHeight = NSScreen.main?.visibleFrame.height
|
|
self.containerHeight = self.body.documentView?.frame.height
|
|
|
|
self.foreground = NSVisualEffectView(frame: frame)
|
|
self.foreground.material = .titlebar
|
|
self.foreground.blendingMode = .behindWindow
|
|
self.foreground.state = .active
|
|
self.foreground.wantsLayer = true
|
|
self.foreground.layer?.backgroundColor = NSColor.red.cgColor
|
|
self.foreground.layer?.cornerRadius = 6
|
|
|
|
self.background = NSView(frame: frame)
|
|
self.background.wantsLayer = true
|
|
self.foreground.addSubview(self.background)
|
|
|
|
super.init(frame: frame)
|
|
|
|
self.body.drawsBackground = false
|
|
self.body.translatesAutoresizingMaskIntoConstraints = true
|
|
self.body.borderType = .noBorder
|
|
self.body.hasVerticalScroller = true
|
|
self.body.hasHorizontalScroller = false
|
|
self.body.autohidesScrollers = true
|
|
self.body.horizontalScrollElasticity = .none
|
|
|
|
self.addSubview(self.foreground, positioned: .below, relativeTo: .none)
|
|
self.addSubview(self.header)
|
|
self.addSubview(self.body)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func updateLayer() {
|
|
self.background.layer?.backgroundColor = self.isDarkMode ? .clear : NSColor.white.cgColor
|
|
}
|
|
|
|
fileprivate func setView(_ view: Popup_p?) {
|
|
self.view = view
|
|
|
|
var isScrollVisible: Bool = false
|
|
var size: NSSize = NSSize(
|
|
width: (view?.frame.width ?? Constants.Popup.width) + (Constants.Popup.margins*2),
|
|
height: (view?.frame.height ?? 0) + Constants.Popup.headerHeight + (Constants.Popup.margins*2)
|
|
)
|
|
|
|
self.windowHeight = NSScreen.main?.visibleFrame.height // for height recalculate when appear/disappear
|
|
self.containerHeight = self.body.documentView?.frame.height // for scroll diff calculation
|
|
if let screenHeight = NSScreen.main?.visibleFrame.height, size.height > screenHeight {
|
|
size.height = screenHeight - Constants.Widget.height
|
|
isScrollVisible = true
|
|
}
|
|
if let screenWidth = NSScreen.main?.visibleFrame.width, size.width > screenWidth {
|
|
size.width = screenWidth
|
|
}
|
|
|
|
self.setFrameSize(size)
|
|
self.foreground.setFrameSize(size)
|
|
self.background.setFrameSize(size)
|
|
self.body.setFrameSize(NSSize(
|
|
width: size.width - (Constants.Popup.margins*2) + (isScrollVisible ? 20 : 0),
|
|
height: size.height - Constants.Popup.headerHeight - (Constants.Popup.margins*2)
|
|
))
|
|
self.header.setFrameOrigin(NSPoint(x: 0, y: size.height - Constants.Popup.headerHeight))
|
|
|
|
if let view = view {
|
|
self.body.documentView = view
|
|
view.sizeCallback = { [weak self] size in
|
|
self?.recalculateHeight(size)
|
|
}
|
|
}
|
|
}
|
|
|
|
fileprivate func setTitle(_ newTitle: String) {
|
|
self.header.setTitle(newTitle)
|
|
}
|
|
|
|
fileprivate func setCloseButton(_ state: Bool) {
|
|
self.header.setCloseButton(state)
|
|
}
|
|
|
|
internal func appear() {
|
|
self.display()
|
|
self.body.subviews.first?.display()
|
|
|
|
if let screenHeight = NSScreen.main?.visibleFrame.height, let size = self.body.documentView?.frame.size {
|
|
if screenHeight != self.windowHeight {
|
|
self.recalculateHeight(size)
|
|
}
|
|
}
|
|
|
|
if let documentView = self.body.documentView {
|
|
documentView.scroll(NSPoint(x: 0, y: documentView.bounds.size.height))
|
|
}
|
|
|
|
self.view?.appear()
|
|
}
|
|
internal func disappear() {
|
|
self.header.setCloseButton(false)
|
|
self.view?.disappear()
|
|
}
|
|
|
|
private func recalculateHeight(_ size: NSSize) {
|
|
var isScrollVisible: Bool = false
|
|
var windowSize: NSSize = NSSize(
|
|
width: size.width + (Constants.Popup.margins*2),
|
|
height: size.height + Constants.Popup.headerHeight + (Constants.Popup.margins*2)
|
|
)
|
|
let h0 = self.containerHeight ?? 0
|
|
|
|
self.windowHeight = NSScreen.main?.visibleFrame.height // for height recalculate when appear/disappear
|
|
self.containerHeight = self.body.documentView?.frame.height // for scroll diff calculation
|
|
if let screenHeight = NSScreen.main?.visibleFrame.height, windowSize.height > screenHeight {
|
|
windowSize.height = screenHeight - Constants.Widget.height
|
|
isScrollVisible = true
|
|
}
|
|
if let screenWidth = NSScreen.main?.visibleFrame.width, windowSize.width > screenWidth {
|
|
windowSize.width = screenWidth
|
|
}
|
|
|
|
self.window?.setContentSize(windowSize)
|
|
self.foreground.setFrameSize(windowSize)
|
|
self.background.setFrameSize(windowSize)
|
|
self.body.setFrameSize(NSSize(
|
|
width: windowSize.width - (Constants.Popup.margins*2) + (isScrollVisible ? 20 : 0),
|
|
height: windowSize.height - Constants.Popup.headerHeight - (Constants.Popup.margins*2)
|
|
))
|
|
self.header.setFrameOrigin(NSPoint(
|
|
x: self.header.frame.origin.x,
|
|
y: self.body.frame.height + (Constants.Popup.margins*2)
|
|
))
|
|
|
|
if let documentView = self.body.documentView {
|
|
let diff = h0 - (self.body.documentView?.frame.height ?? 0)
|
|
documentView.scroll(NSPoint(
|
|
x: 0,
|
|
y: self.body.documentVisibleRect.origin.y - (diff < 0 ? diff : 0)
|
|
))
|
|
}
|
|
}
|
|
}
|
|
|
|
internal class HeaderView: NSStackView {
|
|
private var titleView: NSTextField? = nil
|
|
private var activityButton: NSButton?
|
|
|
|
private var title: String = ""
|
|
private var isCloseAction: Bool = false
|
|
private let activityMonitor: URL?
|
|
private let calendar: URL?
|
|
|
|
init(frame: NSRect, module: ModuleType) {
|
|
self.activityMonitor = NSWorkspace.shared.urlForApplication(withBundleIdentifier: "com.apple.ActivityMonitor")
|
|
self.calendar = URL(fileURLWithPath: "/System/Applications/Calendar.app")
|
|
|
|
super.init(frame: CGRect(x: frame.origin.x, y: frame.origin.y, width: frame.width, height: frame.height))
|
|
|
|
self.orientation = .horizontal
|
|
self.distribution = .gravityAreas
|
|
self.spacing = 0
|
|
|
|
let activity = NSButtonWithPadding()
|
|
activity.frame = CGRect(x: 0, y: 0, width: 24, height: self.frame.height)
|
|
activity.horizontalPadding = activity.frame.height - 24
|
|
activity.bezelStyle = .regularSquare
|
|
activity.translatesAutoresizingMaskIntoConstraints = false
|
|
activity.imageScaling = .scaleNone
|
|
activity.contentTintColor = .lightGray
|
|
activity.isBordered = false
|
|
activity.target = self
|
|
if module == .clock {
|
|
activity.action = #selector(self.openCalendar)
|
|
activity.image = Bundle(for: type(of: self)).image(forResource: "calendar")!
|
|
activity.toolTip = localizedString("Open Calendar")
|
|
} else {
|
|
activity.action = #selector(self.openActivityMonitor)
|
|
activity.image = Bundle(for: type(of: self)).image(forResource: "chart")!
|
|
activity.toolTip = localizedString("Open Activity Monitor")
|
|
}
|
|
activity.focusRingType = .none
|
|
self.activityButton = activity
|
|
|
|
let title = NSTextField(frame: NSRect(x: 0, y: 0, width: frame.width/2, height: 18))
|
|
title.isEditable = false
|
|
title.isSelectable = false
|
|
title.isBezeled = false
|
|
title.wantsLayer = true
|
|
title.textColor = .textColor
|
|
title.backgroundColor = .clear
|
|
title.canDrawSubviewsIntoLayer = true
|
|
title.alignment = .center
|
|
title.font = NSFont.systemFont(ofSize: 16, weight: .regular)
|
|
title.stringValue = ""
|
|
self.titleView = title
|
|
|
|
let settings = NSButtonWithPadding()
|
|
settings.frame = CGRect(x: 0, y: 0, width: 24, height: self.frame.height)
|
|
settings.horizontalPadding = activity.frame.height - 24
|
|
settings.bezelStyle = .regularSquare
|
|
settings.translatesAutoresizingMaskIntoConstraints = false
|
|
settings.imageScaling = .scaleNone
|
|
settings.image = Bundle(for: type(of: self)).image(forResource: "settings")!
|
|
settings.contentTintColor = .lightGray
|
|
settings.isBordered = false
|
|
settings.action = #selector(self.openSettings)
|
|
settings.target = self
|
|
settings.toolTip = localizedString("Open module settings")
|
|
settings.focusRingType = .none
|
|
|
|
self.addArrangedSubview(activity)
|
|
self.addArrangedSubview(title)
|
|
self.addArrangedSubview(settings)
|
|
|
|
NSLayoutConstraint.activate([
|
|
title.widthAnchor.constraint(
|
|
equalToConstant: self.frame.width - activity.intrinsicContentSize.width - settings.intrinsicContentSize.width
|
|
)
|
|
])
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
fileprivate func setTitle(_ newTitle: String) {
|
|
self.title = newTitle
|
|
self.titleView?.stringValue = localizedString(newTitle)
|
|
}
|
|
|
|
@objc func openActivityMonitor() {
|
|
guard let app = self.activityMonitor else { return }
|
|
NSWorkspace.shared.open([], withApplicationAt: app, configuration: NSWorkspace.OpenConfiguration())
|
|
}
|
|
|
|
@objc func openCalendar() {
|
|
guard let app = self.calendar else { return }
|
|
NSWorkspace.shared.open([], withApplicationAt: app, configuration: NSWorkspace.OpenConfiguration())
|
|
}
|
|
|
|
@objc func openSettings() {
|
|
NotificationCenter.default.post(name: .toggleSettings, object: nil, userInfo: ["module": self.title])
|
|
}
|
|
|
|
@objc private func closePopup() {
|
|
self.window?.setIsVisible(false)
|
|
self.setCloseButton(false)
|
|
return
|
|
}
|
|
|
|
fileprivate func setCloseButton(_ state: Bool) {
|
|
if state && !self.isCloseAction {
|
|
self.activityButton?.image = Bundle(for: type(of: self)).image(forResource: "close")!
|
|
self.activityButton?.toolTip = localizedString("Close")
|
|
self.activityButton?.action = #selector(self.closePopup)
|
|
self.isCloseAction = true
|
|
} else if !state && self.isCloseAction {
|
|
self.activityButton?.image = Bundle(for: type(of: self)).image(forResource: "chart")!
|
|
self.activityButton?.toolTip = localizedString("Open Activity Monitor")
|
|
self.activityButton?.action = #selector(self.openActivityMonitor)
|
|
self.isCloseAction = false
|
|
}
|
|
}
|
|
}
|