// // CombinedView.swift // Stats // // Created by Serhiy Mytrovtsiy on 09/01/2023 // Using Swift 5.0 // Running on macOS 13.1 // // Copyright © 2023 Serhiy Mytrovtsiy. All rights reserved. // import Cocoa import Kit internal class CombinedView: NSObject, NSGestureRecognizerDelegate { private var menuBarItem: NSStatusItem? = nil private var view: NSView = NSView(frame: NSRect(x: 0, y: 0, width: 0, height: Constants.Widget.height)) private var popup: PopupWindow? = nil private var status: Bool { Store.shared.bool(key: "CombinedModules", defaultValue: false) } private var spacing: CGFloat { CGFloat(Int(Store.shared.string(key: "CombinedModules_spacing", defaultValue: "")) ?? 0) } private var separator: Bool { Store.shared.bool(key: "CombinedModules_separator", defaultValue: false) } private var activeModules: [Module] { modules.filter({ $0.enabled }).sorted(by: { $0.combinedPosition < $1.combinedPosition }) } private var combinedModulesPopup: Bool { get { Store.shared.bool(key: "CombinedModules_popup", defaultValue: true) } set { Store.shared.set(key: "CombinedModules_popup", value: newValue) } } override init() { super.init() modules.forEach { (m: Module) in m.menuBar.callback = { [weak self] in if let s = self?.status, s { DispatchQueue.main.async(execute: { self?.recalculate() }) } } } self.popup = PopupWindow(title: "Combined modules", module: .combined, view: Popup()) { _ in } if self.status { self.enable() } NotificationCenter.default.addObserver(self, selector: #selector(listenForOneView), name: .toggleOneView, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(listenForModuleRearrrange), name: .moduleRearrange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(listenCombinedModulesPopup), name: .combinedModulesPopup, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(listenForModule), name: .toggleModule, object: nil) } deinit { NotificationCenter.default.removeObserver(self, name: .toggleOneView, object: nil) NotificationCenter.default.removeObserver(self, name: .moduleRearrange, object: nil) NotificationCenter.default.removeObserver(self, name: .combinedModulesPopup, object: nil) NotificationCenter.default.removeObserver(self, name: .toggleModule, object: nil) } public func enable() { self.menuBarItem = NSStatusBar.system.statusItem(withLength: 0) DispatchQueue.main.async(execute: { self.menuBarItem?.autosaveName = "CombinedModules" }) self.menuBarItem?.button?.addSubview(self.view) self.menuBarItem?.button?.image = NSImage() self.menuBarItem?.button?.toolTip = localizedString("Combined modules") if !self.combinedModulesPopup { self.activeModules.forEach { (m: Module) in m.menuBar.widgets.forEach { w in w.item.onClick = { if let window = w.item.window { NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [ "module": m.name, "widget": w.type, "origin": window.frame.origin, "center": window.frame.width/2 ]) } } } } } else { self.menuBarItem?.button?.target = self self.menuBarItem?.button?.action = #selector(self.togglePopup) self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown]) } DispatchQueue.main.async(execute: { self.recalculate() }) } public func disable() { self.activeModules.forEach { (m: Module) in m.menuBar.widgets.forEach { w in w.item.onClick = nil } } if let item = self.menuBarItem { NSStatusBar.system.removeStatusItem(item) } self.menuBarItem = nil } private func recalculate() { self.view.subviews.forEach({ $0.removeFromSuperview() }) var w: CGFloat = 0 var i: Int = 0 self.activeModules.forEach { (m: Module) in self.view.addSubview(m.menuBar.view) self.view.subviews[i].setFrameOrigin(NSPoint(x: w, y: 0)) w += m.menuBar.view.frame.width + self.spacing i += 1 if self.separator && i < 2 * self.activeModules.count - 1 { let separator = NSView(frame: NSRect(x: w, y: 3, width: 1, height: Constants.Widget.height-6)) separator.wantsLayer = true separator.layer?.backgroundColor = (separator.isDarkMode ? NSColor.black : NSColor.white).cgColor self.view.addSubview(separator) w += 3 + self.spacing i += 1 } } self.view.setFrameSize(NSSize(width: w, height: self.view.frame.height)) self.menuBarItem?.length = w } // call when popup appear/disappear private func visibilityCallback(_ state: Bool) {} @objc private func togglePopup(_ sender: NSButton) { guard let popup = self.popup, let item = self.menuBarItem, let window = item.button?.window else { return } let openedWindows = NSApplication.shared.windows.filter{ $0 is NSPanel } openedWindows.forEach{ $0.setIsVisible(false) } if popup.occlusionState.rawValue == 8192 { NSApplication.shared.activate(ignoringOtherApps: true) popup.contentView?.invalidateIntrinsicContentSize() let windowCenter = popup.contentView!.intrinsicContentSize.width / 2 var x = window.frame.origin.x - windowCenter + window.frame.width/2 let y = window.frame.origin.y - popup.contentView!.intrinsicContentSize.height - 3 let maxWidth = NSScreen.screens.map{ $0.frame.width }.reduce(0, +) if x + popup.contentView!.intrinsicContentSize.width > maxWidth { x = maxWidth - popup.contentView!.intrinsicContentSize.width - 3 } popup.setFrameOrigin(NSPoint(x: x, y: y)) popup.setIsVisible(true) } else { popup.setIsVisible(false) } } @objc private func listenForOneView(_ notification: Notification) { guard notification.userInfo?["module"] == nil else { return } if self.status { self.enable() } else { self.disable() } } @objc private func listenForModuleRearrrange() { self.recalculate() } @objc private func listenCombinedModulesPopup() { if !self.combinedModulesPopup { self.activeModules.forEach { (m: Module) in m.menuBar.widgets.forEach { w in w.item.onClick = { if let window = w.item.window { NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [ "module": m.name, "widget": w.type, "origin": window.frame.origin, "center": window.frame.width/2 ]) } } } } self.menuBarItem?.button?.action = nil } else { self.activeModules.forEach { (m: Module) in m.menuBar.widgets.forEach { w in w.item.onClick = nil } } self.menuBarItem?.button?.target = self self.menuBarItem?.button?.action = #selector(self.togglePopup) self.menuBarItem?.button?.sendAction(on: [.leftMouseDown, .rightMouseDown]) } } @objc private func listenForModule(_ notification: Notification) { guard let name = notification.userInfo?["module"] as? String, let state = notification.userInfo?["state"] as? Bool, state, let module = self.activeModules.first(where: { $0.name == name }) else { return } module.menuBar.widgets.forEach { w in w.item.onClick = { if let window = w.item.window { NotificationCenter.default.post(name: .togglePopup, object: nil, userInfo: [ "module": module.name, "widget": w.type, "origin": window.frame.origin, "center": window.frame.width/2 ]) } } } } } private class Popup: NSStackView, Popup_p { fileprivate var keyboardShortcut: [UInt16] = [] fileprivate var sizeCallback: ((NSSize) -> Void)? = nil init() { self.keyboardShortcut = Store.shared.array(key: "CombinedModules_popup_keyboardShortcut", defaultValue: []) as? [UInt16] ?? [] super.init(frame: NSRect(x: 0, y: 0, width: Constants.Popup.width, height: 0)) self.orientation = .vertical self.distribution = .fill self.alignment = .width self.spacing = Constants.Popup.spacing self.reinit() NotificationCenter.default.addObserver(self, selector: #selector(reinit), name: .toggleModule, object: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { NotificationCenter.default.removeObserver(self, name: .toggleOneView, object: nil) } fileprivate func settings() -> NSView? { return nil } fileprivate func appear() {} fileprivate func disappear() {} fileprivate func setKeyboardShortcut(_ binding: [UInt16]) { self.keyboardShortcut = binding Store.shared.set(key: "CombinedModules_popup_keyboardShortcut", value: binding) } @objc private func reinit() { self.subviews.forEach({ $0.removeFromSuperview() }) let availableModules = modules.filter({ $0.enabled && $0.portal != nil }) availableModules.forEach { (m: Module) in if let p = m.portal { self.addArrangedSubview(p) } } let h = CGFloat(availableModules.count) * Constants.Popup.portalHeight + (CGFloat(availableModules.count-1)*Constants.Popup.spacing) if h > 0 { self.setFrameSize(NSSize(width: self.frame.width, height: h)) self.sizeCallback?(self.frame.size) } } }