From 41ade578562e6c37205534d5d36163157353f9f2 Mon Sep 17 00:00:00 2001 From: jordane-cit Date: Wed, 7 Jan 2026 11:23:34 -0500 Subject: [PATCH 1/2] Added visibility toggle and options menu --- Clippy Shared/CGImageExtensions.swift | 2 + Clippy macOS/AgentViewController.swift | 120 +++++++++++++++++-------- Clippy macOS/AgentWindow.swift | 16 +++- Clippy macOS/AppDelegate.swift | 92 ++++++++++++++----- Clippy.xcodeproj/project.pbxproj | 6 +- 5 files changed, 170 insertions(+), 66 deletions(-) diff --git a/Clippy Shared/CGImageExtensions.swift b/Clippy Shared/CGImageExtensions.swift index a5850f9..ca7ab0d 100644 --- a/Clippy Shared/CGImageExtensions.swift +++ b/Clippy Shared/CGImageExtensions.swift @@ -3,10 +3,12 @@ // Clippy // // Created by Devran on 08.09.19. +// Updated by JordanE on 01.07.26. // Copyright © 2019 Devran. All rights reserved. // import Foundation +import CoreGraphics extension CGImage { static func mergeImages(_ images: [CGImage]) -> CGImage? { diff --git a/Clippy macOS/AgentViewController.swift b/Clippy macOS/AgentViewController.swift index 56ed1aa..5356e03 100644 --- a/Clippy macOS/AgentViewController.swift +++ b/Clippy macOS/AgentViewController.swift @@ -3,15 +3,18 @@ // Clippy // // Created by Devran on 04.09.19. +// Updated by JordanE on 01.07.26. // Copyright © 2019 Devran. All rights reserved. // + import AppKit + class AgentViewController: NSViewController { var agentController: AgentController var agentView: AgentView - + init() { agentView = AgentView() agentController = AgentController(agentView: agentView) @@ -19,30 +22,30 @@ class AgentViewController: NSViewController { super.init(nibName: nil, bundle: nil) agentController.delegate = self } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func loadView() { let size = CGSize(width: 100, height: 200) view = NSView(frame: CGRect(origin: CGPoint.zero, size: size)) view.wantsLayer = true view.layer?.backgroundColor = .clear } - + override func viewDidLoad() { super.viewDidLoad() view.addSubview(agentView) setupConstraints() setupTrackingArea() } - - + + override func viewDidAppear() { super.viewDidAppear() view.window?.makeFirstResponder(self) - + let lastUsedName = (NSApplication.shared.delegate as? AppDelegate)?.lastUsedAgent let name = lastUsedName ?? Agent.randomAgentName() if let name = name { @@ -50,7 +53,7 @@ class AgentViewController: NSViewController { agentController.show() } } - + func setupConstraints() { agentView.translatesAutoresizingMaskIntoConstraints = false agentView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true @@ -58,7 +61,7 @@ class AgentViewController: NSViewController { view.rightAnchor.constraint(equalTo: agentView.rightAnchor).isActive = true view.bottomAnchor.constraint(equalTo: agentView.bottomAnchor).isActive = true } - + func setupTrackingArea() { let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .inVisibleRect, .activeAlways] let trackingArea = NSTrackingArea(rect: view.frame, options: options, owner: self, userInfo: nil) @@ -66,32 +69,38 @@ class AgentViewController: NSViewController { } } + extension AgentViewController { override func mouseEntered(with event: NSEvent) { self.view.superview?.window?.alphaValue = 1.0 } - + override func mouseExited(with event: NSEvent) { - self.view.superview?.window?.alphaValue = 0.5 + // Check if transparency is enabled in AppDelegate + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + if appDelegate.isTransparencyEnabled { + self.view.superview?.window?.alphaValue = 0.5 + } + } } - + @objc func animateAction() { agentController.animate() } - + @objc func chooseAssistantAction() { guard let name = Agent.randomAgentName() else { return } try? agentController.load(name: name) } - + override var acceptsFirstResponder: Bool { return true } - + override func becomeFirstResponder() -> Bool { return true } - + override func keyDown(with event: NSEvent) { guard let agent = agentController.agent else { super.keyDown(with: event) @@ -120,52 +129,87 @@ extension AgentViewController { super.keyDown(with: event) } } - + override func mouseDown(with event: NSEvent) { if event.clickCount == 2 { agentController.animate() } } - + + func createOptionsSubmenu() -> NSMenu { + let optionsMenu = NSMenu(title: "Options") + + // Mute toggle + let muteItem = NSMenuItem(title: "Mute", action: #selector(toggleMuteAction(sender:)), keyEquivalent: "") + if let isMuted = AppDelegate.agentController?.isMuted { + muteItem.state = isMuted ? .on : .off + } + optionsMenu.addItem(muteItem) + + // Transparency toggle + let transparencyItem = NSMenuItem(title: "Transparency When Inactive", action: #selector(toggleTransparencyAction(sender:)), keyEquivalent: "") + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + transparencyItem.state = appDelegate.isTransparencyEnabled ? .on : .off + } + optionsMenu.addItem(transparencyItem) + + return optionsMenu + } + override func rightMouseDown(with event: NSEvent) { guard let _ = agentController.agent else { return } - + let menu = NSMenu(title: "Agent") + + // Create Options menu + let optionsMenuItem = NSMenuItem(title: "Options", action: nil, keyEquivalent: "") + optionsMenuItem.submenu = createOptionsSubmenu() + let menuItems = [ - NSMenuItem(title: "Hide", - action: #selector(hideAction(sender:)), - keyEquivalent: ""), + optionsMenuItem, NSMenuItem.separator(), - NSMenuItem(title: "Options…", - action: nil, - keyEquivalent: ""), NSMenuItem(title: "Choose Assistant…", action: #selector(chooseAssistantAction), keyEquivalent: ""), NSMenuItem(title: "Animate!", action: #selector(animateAction), + keyEquivalent: ""), + NSMenuItem.separator(), + NSMenuItem(title: "Hide", + action: #selector(hideAction(sender:)), keyEquivalent: "") ] - + for (index, menuItem) in menuItems.enumerated() { menu.insertItem(menuItem, at: index) } NSMenu.popUpContextMenu(menu, with: event, for: agentView) } - + @objc func hideAction(sender: AnyObject) { agentController.hide() } - - @objc func optionsAction(sender: AnyObject) { - let viewController = BalloonViewController(nibName: nil, bundle: nil) - print(viewController) - let popOver = NSPopover() - popOver.behavior = .semitransient - popOver.contentSize = CGSize(width: 200, height: 300) - popOver.animates = true - popOver.contentViewController = viewController - let rect = self.view.frame - popOver.show(relativeTo: rect, of: view, preferredEdge: NSRectEdge.maxY) + + @objc func toggleMuteAction(sender: AnyObject) { + guard let menuItem = sender as? NSMenuItem else { return } + guard let isMuted = AppDelegate.agentController?.isMuted else { return } + let newValue = !isMuted + AppDelegate.agentController?.isMuted = newValue + menuItem.state = newValue ? .on : .off + + NotificationCenter.default.post(name: .muteDidChange, object: nil) + } + + @objc func toggleTransparencyAction(sender: AnyObject) { + guard let menuItem = sender as? NSMenuItem else { return } + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } + + appDelegate.isTransparencyEnabled = !appDelegate.isTransparencyEnabled + menuItem.state = appDelegate.isTransparencyEnabled ? .on : .off + + if let window = view.window, !window.isKeyWindow { + window.alphaValue = appDelegate.isTransparencyEnabled ? 0.5 : 1.0 + } + } } diff --git a/Clippy macOS/AgentWindow.swift b/Clippy macOS/AgentWindow.swift index 187da87..e7e989b 100644 --- a/Clippy macOS/AgentWindow.swift +++ b/Clippy macOS/AgentWindow.swift @@ -3,11 +3,13 @@ // Clippy macOS // // Created by Devran on 07.09.19. +// Updated by JordanE on 01.07.26. // Copyright © 2019 Devran. All rights reserved. // import Cocoa + class AgentWindow: NSWindow { override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) @@ -23,23 +25,29 @@ class AgentWindow: NSWindow { } else { backgroundColor = .clear } - + /// Fixes glitches hasShadow = false isOpaque = true delegate = self } - + override var canBecomeKey: Bool { return true } } + extension AgentWindow: NSWindowDelegate { func windowDidResignKey(_ notification: Notification) { - alphaValue = 0.5 + // Check if transparency is enabled in AppDelegate + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + if appDelegate.isTransparencyEnabled { + alphaValue = 0.5 + } + } } - + func windowDidBecomeKey(_ notification: Notification) { alphaValue = 1.0 } diff --git a/Clippy macOS/AppDelegate.swift b/Clippy macOS/AppDelegate.swift index 8c95d42..431bd97 100644 --- a/Clippy macOS/AppDelegate.swift +++ b/Clippy macOS/AppDelegate.swift @@ -3,11 +3,13 @@ // Clippy macOS // // Created by Devran on 03.09.19. +// Updated by JordanE on 01.07.26. // Copyright © 2019 Devran. All rights reserved. // import Cocoa + class AppDelegate: NSObject, NSApplicationDelegate { let applicationName = "Clippy" var window: NSWindow? @@ -15,9 +17,18 @@ class AppDelegate: NSObject, NSApplicationDelegate { var agentsMenuItem: NSMenuItem? static var agentController: AgentController? var lastUsedAgent: String? - + + // Track transparency state + var isTransparencyEnabled: Bool = true { + didSet { + UserDefaults.standard.set(isTransparencyEnabled, forKey: "transparencyEnabled") + NotificationCenter.default.post(name: .transparencyDidChange, object: nil) + } + } + func applicationDidFinishLaunching(_ aNotification: Notification) { - + isTransparencyEnabled = UserDefaults.standard.object(forKey: "transparencyEnabled") as? Bool ?? true + window = AgentWindow(contentRect: CGRect.zero, styleMask: [], backing: .buffered, defer: true) window?.title = applicationName window?.contentViewController = AgentViewController() @@ -25,32 +36,32 @@ class AppDelegate: NSObject, NSApplicationDelegate { window?.makeKeyAndOrderFront(self) } window?.center() - + setupStatusBar() } - + func applicationWillTerminate(_ aNotification: Notification) { // Insert code here to tear down your application } - + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } - + func setupStatusBar() { let statusBar = NSStatusBar.system statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength) if let button = statusItem?.button { button.title = "📎" } - + setupStatusBarMenu() } - + func createAgentsMenu() -> NSMenu { let agentsMenu = NSMenu(title: "Agents") let agentNames = Agent.agentNames() - + if agentNames.isEmpty { agentsMenu.addItem(withTitle: "No Agents found.", action: nil, @@ -71,15 +82,22 @@ class AppDelegate: NSObject, NSApplicationDelegate { keyEquivalent: "") return agentsMenu } - + func setupStatusBarMenu() { // Status bar menu let statusBarMenu = NSMenu(title: "Clippy") + statusBarMenu.delegate = self agentsMenuItem = NSMenuItem(title: "Agents", action: nil, keyEquivalent: "") - + statusBarMenu.addItem(withTitle: "Show", action: #selector(showAction(sender:)), keyEquivalent: "") statusBarMenu.addItem(withTitle: "Hide", action: #selector(hideAction(sender:)), keyEquivalent: "") statusBarMenu.addItem(withTitle: "Mute", action: #selector(toggleMuteAction(sender:)), keyEquivalent: "") + + // Add transparency toggle menu item + let transparencyItem = NSMenuItem(title: "Transparency When Inactive", action: #selector(toggleTransparencyAction(sender:)), keyEquivalent: "") + transparencyItem.state = isTransparencyEnabled ? .on : .off + statusBarMenu.addItem(transparencyItem) + statusBarMenu.addItem(NSMenuItem.separator()) guard let menuItem = agentsMenuItem else { return } statusBarMenu.addItem(menuItem) @@ -88,33 +106,33 @@ class AppDelegate: NSObject, NSApplicationDelegate { keyEquivalent: "") statusBarMenu.addItem(NSMenuItem.separator()) statusBarMenu.addItem(withTitle: "Quit \(applicationName)", action: #selector(quitAction(sender:)), keyEquivalent: "") - + // Agents menu statusBarMenu.setSubmenu(createAgentsMenu(), for: menuItem) - + statusItem?.menu = statusBarMenu } - + @objc func quitAction(sender: AnyObject) { NSApplication.shared.terminate(self) } - + @objc func reloadAction(sender: AnyObject) { agentsMenuItem?.submenu = createAgentsMenu() } - + @objc func openFolderAction(sender: AnyObject) { NSWorkspace.shared.open(Agent.agentsURL()) } - + @objc func hideAction(sender: AnyObject) { AppDelegate.agentController?.hide() } - + @objc func showAction(sender: AnyObject) { window?.makeKeyAndOrderFront(self) } - + @objc func toggleMuteAction(sender: AnyObject) { guard let menuItem = sender as? NSMenuItem else { return } guard let isMuted = AppDelegate.agentController?.isMuted else { return } @@ -122,11 +140,21 @@ class AppDelegate: NSObject, NSApplicationDelegate { AppDelegate.agentController?.isMuted = newValue menuItem.state = newValue ? .on : .off } - + + @objc func toggleTransparencyAction(sender: AnyObject) { + guard let menuItem = sender as? NSMenuItem else { return } + isTransparencyEnabled = !isTransparencyEnabled + menuItem.state = isTransparencyEnabled ? .on : .off + + if let agentWindow = window, !agentWindow.isKeyWindow { + agentWindow.alphaValue = isTransparencyEnabled ? 0.5 : 1.0 + } + } + @objc func selectAgent(sender: AnyObject) { guard let menuItem = sender as? NSMenuItem else { return } let name = menuItem.title.lowercased() - + if let isVisible = window?.isVisible, isVisible == true { try? AppDelegate.agentController?.load(name: name) if let animation = AppDelegate.agentController?.agent?.findAnimation("Show") { @@ -136,7 +164,27 @@ class AppDelegate: NSObject, NSApplicationDelegate { lastUsedAgent = name window?.makeKeyAndOrderFront(self) } - + agentsMenuItem?.submenu = createAgentsMenu() } } + +extension AppDelegate: NSMenuDelegate { + func menuNeedsUpdate(_ menu: NSMenu) { + // Update menu item states when menu is about to open + for item in menu.items { + if item.title == "Mute" { + if let isMuted = AppDelegate.agentController?.isMuted { + item.state = isMuted ? .on : .off + } + } else if item.title == "Transparency When Inactive" { + item.state = isTransparencyEnabled ? .on : .off + } + } + } +} + +extension Notification.Name { + static let transparencyDidChange = Notification.Name("transparencyDidChange") + static let muteDidChange = Notification.Name("muteDidChange") +} diff --git a/Clippy.xcodeproj/project.pbxproj b/Clippy.xcodeproj/project.pbxproj index b7adc4e..be17efb 100644 --- a/Clippy.xcodeproj/project.pbxproj +++ b/Clippy.xcodeproj/project.pbxproj @@ -518,10 +518,11 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = macOSAppIcon; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 2; - DEVELOPMENT_TEAM = Z3ZG8RPGHF; + DEVELOPMENT_TEAM = YY62Y6T8GK; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Clippy macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( @@ -541,10 +542,11 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = macOSAppIcon; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 2; - DEVELOPMENT_TEAM = Z3ZG8RPGHF; + DEVELOPMENT_TEAM = YY62Y6T8GK; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Clippy macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( From 920dc7c0d90be67b0938cd84cc860a46750171f6 Mon Sep 17 00:00:00 2001 From: jordane-cit Date: Wed, 7 Jan 2026 17:08:56 -0500 Subject: [PATCH 2/2] Added random animation chance toggle and always on top toggle --- Clippy macOS/AgentController.swift | 52 +++++++++++++++ Clippy macOS/AgentViewController.swift | 82 +++++++++++++++++++++--- Clippy macOS/AgentWindow.swift | 8 ++- Clippy macOS/AppDelegate.swift | 87 +++++++++++++++++++++++--- Clippy.xcodeproj/project.pbxproj | 4 +- 5 files changed, 212 insertions(+), 21 deletions(-) diff --git a/Clippy macOS/AgentController.swift b/Clippy macOS/AgentController.swift index d8d0951..a667533 100644 --- a/Clippy macOS/AgentController.swift +++ b/Clippy macOS/AgentController.swift @@ -3,13 +3,16 @@ // Clippy macOS // // Created by Devran on 07.09.19. +// Updated by JordanE on 01.07.26. // Copyright © 2019 Devran. All rights reserved. // + import Cocoa import AVKit import SpriteKit + class AgentController { var isMuted = false var player: AVPlayer = { @@ -22,6 +25,17 @@ class AgentController { var delegate: AgentControllerDelegate? var isHidden = true + var randomAnimationsEnabled = false { + didSet { + if randomAnimationsEnabled { + scheduleNextRandomAnimation() + } else { + cancelRandomAnimations() + } + } + } + private var randomAnimationTimer: Timer? + init() { } @@ -37,6 +51,10 @@ class AgentController { self.agent = agent showInitialFrame() delegate?.didLoadAgent(agent: agent) + + if randomAnimationsEnabled { + scheduleNextRandomAnimation() + } } func audioActionForFrame(frame: AgentFrame) -> SKAction? { @@ -96,4 +114,38 @@ class AgentController { func show() { delegate?.handleShow() } + + private func scheduleNextRandomAnimation() { + cancelRandomAnimations() + + let randomInterval = TimeInterval.random(in: 30...300) + + randomAnimationTimer = Timer.scheduledTimer(withTimeInterval: randomInterval, repeats: false) { [weak self] _ in + self?.performRandomAnimation() + } + } + + private func performRandomAnimation() { + guard randomAnimationsEnabled, !isHidden else { + if randomAnimationsEnabled { + scheduleNextRandomAnimation() + } + return + } + + animate() + + if randomAnimationsEnabled { + scheduleNextRandomAnimation() + } + } + + private func cancelRandomAnimations() { + randomAnimationTimer?.invalidate() + randomAnimationTimer = nil + } + + deinit { + cancelRandomAnimations() + } } diff --git a/Clippy macOS/AgentViewController.swift b/Clippy macOS/AgentViewController.swift index 5356e03..b9b96b0 100644 --- a/Clippy macOS/AgentViewController.swift +++ b/Clippy macOS/AgentViewController.swift @@ -8,13 +8,16 @@ // + import AppKit + class AgentViewController: NSViewController { var agentController: AgentController var agentView: AgentView + init() { agentView = AgentView() agentController = AgentController(agentView: agentView) @@ -23,10 +26,12 @@ class AgentViewController: NSViewController { agentController.delegate = self } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + override func loadView() { let size = CGSize(width: 100, height: 200) view = NSView(frame: CGRect(origin: CGPoint.zero, size: size)) @@ -34,6 +39,7 @@ class AgentViewController: NSViewController { view.layer?.backgroundColor = .clear } + override func viewDidLoad() { super.viewDidLoad() view.addSubview(agentView) @@ -42,18 +48,25 @@ class AgentViewController: NSViewController { } + override func viewDidAppear() { super.viewDidAppear() view.window?.makeFirstResponder(self) + let lastUsedName = (NSApplication.shared.delegate as? AppDelegate)?.lastUsedAgent let name = lastUsedName ?? Agent.randomAgentName() if let name = name { try? agentController.load(name: name) agentController.show() } + + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + agentController.randomAnimationsEnabled = appDelegate.isRandomAnimationsEnabled + } } + func setupConstraints() { agentView.translatesAutoresizingMaskIntoConstraints = false agentView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true @@ -62,6 +75,7 @@ class AgentViewController: NSViewController { view.bottomAnchor.constraint(equalTo: agentView.bottomAnchor).isActive = true } + func setupTrackingArea() { let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .inVisibleRect, .activeAlways] let trackingArea = NSTrackingArea(rect: view.frame, options: options, owner: self, userInfo: nil) @@ -70,13 +84,14 @@ class AgentViewController: NSViewController { } + extension AgentViewController { override func mouseEntered(with event: NSEvent) { self.view.superview?.window?.alphaValue = 1.0 } + override func mouseExited(with event: NSEvent) { - // Check if transparency is enabled in AppDelegate if let appDelegate = NSApplication.shared.delegate as? AppDelegate { if appDelegate.isTransparencyEnabled { self.view.superview?.window?.alphaValue = 0.5 @@ -84,45 +99,50 @@ extension AgentViewController { } } + @objc func animateAction() { agentController.animate() } + @objc func chooseAssistantAction() { guard let name = Agent.randomAgentName() else { return } try? agentController.load(name: name) } + override var acceptsFirstResponder: Bool { return true } + override func becomeFirstResponder() -> Bool { return true } + override func keyDown(with event: NSEvent) { guard let agent = agentController.agent else { super.keyDown(with: event) return } switch Int(event.keyCode) { - case 49: // Spacebar + case 49: agentController.animate() - case 36: // Return + case 36: guard let name = Agent.randomAgentName() else { return } try? agentController.load(name: name) agentController.show() - case 124: // Arrow Right Key + case 124: guard let animation = agent.findAnimation("LookLeft") else { break } agentController.play(animation: animation) - case 123: // Arrow Left Key + case 123: guard let animation = agent.findAnimation("LookRight") else { break } agentController.play(animation: animation) - case 126: // Arrow Up Key + case 126: guard let animation = agent.findAnimation("LookUp") else { break } agentController.play(animation: animation) - case 125: // Arrow Down Key + case 125: guard let animation = agent.findAnimation("LookDown") else { break } agentController.play(animation: animation) default: @@ -130,41 +150,59 @@ extension AgentViewController { } } + override func mouseDown(with event: NSEvent) { if event.clickCount == 2 { agentController.animate() } } + func createOptionsSubmenu() -> NSMenu { let optionsMenu = NSMenu(title: "Options") - // Mute toggle + let muteItem = NSMenuItem(title: "Mute", action: #selector(toggleMuteAction(sender:)), keyEquivalent: "") if let isMuted = AppDelegate.agentController?.isMuted { muteItem.state = isMuted ? .on : .off } optionsMenu.addItem(muteItem) - // Transparency toggle + let transparencyItem = NSMenuItem(title: "Transparency When Inactive", action: #selector(toggleTransparencyAction(sender:)), keyEquivalent: "") if let appDelegate = NSApplication.shared.delegate as? AppDelegate { transparencyItem.state = appDelegate.isTransparencyEnabled ? .on : .off } optionsMenu.addItem(transparencyItem) + + let randomAnimationsItem = NSMenuItem(title: "Random Animations", action: #selector(toggleRandomAnimationsAction(sender:)), keyEquivalent: "") + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + randomAnimationsItem.state = appDelegate.isRandomAnimationsEnabled ? .on : .off + } + optionsMenu.addItem(randomAnimationsItem) + + let alwaysOnTopItem = NSMenuItem(title: "Always On Top", action: #selector(toggleAlwaysOnTopAction(sender:)), keyEquivalent: "") + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + alwaysOnTopItem.state = appDelegate.isAlwaysOnTop ? .on : .off + } + optionsMenu.addItem(alwaysOnTopItem) + return optionsMenu } + override func rightMouseDown(with event: NSEvent) { guard let _ = agentController.agent else { return } + let menu = NSMenu(title: "Agent") - // Create Options menu + let optionsMenuItem = NSMenuItem(title: "Options", action: nil, keyEquivalent: "") optionsMenuItem.submenu = createOptionsSubmenu() + let menuItems = [ optionsMenuItem, NSMenuItem.separator(), @@ -180,16 +218,19 @@ extension AgentViewController { keyEquivalent: "") ] + for (index, menuItem) in menuItems.enumerated() { menu.insertItem(menuItem, at: index) } NSMenu.popUpContextMenu(menu, with: event, for: agentView) } + @objc func hideAction(sender: AnyObject) { agentController.hide() } + @objc func toggleMuteAction(sender: AnyObject) { guard let menuItem = sender as? NSMenuItem else { return } guard let isMuted = AppDelegate.agentController?.isMuted else { return } @@ -197,19 +238,40 @@ extension AgentViewController { AppDelegate.agentController?.isMuted = newValue menuItem.state = newValue ? .on : .off + NotificationCenter.default.post(name: .muteDidChange, object: nil) } + @objc func toggleTransparencyAction(sender: AnyObject) { guard let menuItem = sender as? NSMenuItem else { return } guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } + appDelegate.isTransparencyEnabled = !appDelegate.isTransparencyEnabled menuItem.state = appDelegate.isTransparencyEnabled ? .on : .off + if let window = view.window, !window.isKeyWindow { window.alphaValue = appDelegate.isTransparencyEnabled ? 0.5 : 1.0 } + } + + @objc func toggleRandomAnimationsAction(sender: AnyObject) { + guard let menuItem = sender as? NSMenuItem else { return } + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } + + + appDelegate.isRandomAnimationsEnabled = !appDelegate.isRandomAnimationsEnabled + menuItem.state = appDelegate.isRandomAnimationsEnabled ? .on : .off + } + + @objc func toggleAlwaysOnTopAction(sender: AnyObject) { + guard let menuItem = sender as? NSMenuItem else { return } + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } + + appDelegate.isAlwaysOnTop = !appDelegate.isAlwaysOnTop + menuItem.state = appDelegate.isAlwaysOnTop ? .on : .off } } diff --git a/Clippy macOS/AgentWindow.swift b/Clippy macOS/AgentWindow.swift index e7e989b..7b299a3 100644 --- a/Clippy macOS/AgentWindow.swift +++ b/Clippy macOS/AgentWindow.swift @@ -7,9 +7,11 @@ // Copyright © 2019 Devran. All rights reserved. // + import Cocoa + class AgentWindow: NSWindow { override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) @@ -26,21 +28,22 @@ class AgentWindow: NSWindow { backgroundColor = .clear } - /// Fixes glitches + hasShadow = false isOpaque = true delegate = self } + override var canBecomeKey: Bool { return true } } + extension AgentWindow: NSWindowDelegate { func windowDidResignKey(_ notification: Notification) { - // Check if transparency is enabled in AppDelegate if let appDelegate = NSApplication.shared.delegate as? AppDelegate { if appDelegate.isTransparencyEnabled { alphaValue = 0.5 @@ -48,6 +51,7 @@ extension AgentWindow: NSWindowDelegate { } } + func windowDidBecomeKey(_ notification: Notification) { alphaValue = 1.0 } diff --git a/Clippy macOS/AppDelegate.swift b/Clippy macOS/AppDelegate.swift index 431bd97..a30daed 100644 --- a/Clippy macOS/AppDelegate.swift +++ b/Clippy macOS/AppDelegate.swift @@ -7,9 +7,11 @@ // Copyright © 2019 Devran. All rights reserved. // + import Cocoa + class AppDelegate: NSObject, NSApplicationDelegate { let applicationName = "Clippy" var window: NSWindow? @@ -18,16 +20,36 @@ class AppDelegate: NSObject, NSApplicationDelegate { static var agentController: AgentController? var lastUsedAgent: String? - // Track transparency state + var isTransparencyEnabled: Bool = true { didSet { UserDefaults.standard.set(isTransparencyEnabled, forKey: "transparencyEnabled") NotificationCenter.default.post(name: .transparencyDidChange, object: nil) } } + + var isRandomAnimationsEnabled: Bool = false { + didSet { + UserDefaults.standard.set(isRandomAnimationsEnabled, forKey: "randomAnimationsEnabled") + AppDelegate.agentController?.randomAnimationsEnabled = isRandomAnimationsEnabled + NotificationCenter.default.post(name: .randomAnimationsDidChange, object: nil) + } + } + + var isAlwaysOnTop: Bool = true { + didSet { + UserDefaults.standard.set(isAlwaysOnTop, forKey: "alwaysOnTop") + updateWindowLevel() + NotificationCenter.default.post(name: .alwaysOnTopDidChange, object: nil) + } + } + func applicationDidFinishLaunching(_ aNotification: Notification) { isTransparencyEnabled = UserDefaults.standard.object(forKey: "transparencyEnabled") as? Bool ?? true + isRandomAnimationsEnabled = UserDefaults.standard.object(forKey: "randomAnimationsEnabled") as? Bool ?? false + isAlwaysOnTop = UserDefaults.standard.object(forKey: "alwaysOnTop") as? Bool ?? true + window = AgentWindow(contentRect: CGRect.zero, styleMask: [], backing: .buffered, defer: true) window?.title = applicationName @@ -36,18 +58,21 @@ class AppDelegate: NSObject, NSApplicationDelegate { window?.makeKeyAndOrderFront(self) } window?.center() - + + updateWindowLevel() setupStatusBar() } + func applicationWillTerminate(_ aNotification: Notification) { - // Insert code here to tear down your application } + func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + func setupStatusBar() { let statusBar = NSStatusBar.system statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength) @@ -55,13 +80,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { button.title = "📎" } + setupStatusBarMenu() } + func createAgentsMenu() -> NSMenu { let agentsMenu = NSMenu(title: "Agents") let agentNames = Agent.agentNames() + if agentNames.isEmpty { agentsMenu.addItem(withTitle: "No Agents found.", action: nil, @@ -83,20 +111,30 @@ class AppDelegate: NSObject, NSApplicationDelegate { return agentsMenu } + func setupStatusBarMenu() { - // Status bar menu let statusBarMenu = NSMenu(title: "Clippy") statusBarMenu.delegate = self agentsMenuItem = NSMenuItem(title: "Agents", action: nil, keyEquivalent: "") + statusBarMenu.addItem(withTitle: "Show", action: #selector(showAction(sender:)), keyEquivalent: "") statusBarMenu.addItem(withTitle: "Hide", action: #selector(hideAction(sender:)), keyEquivalent: "") statusBarMenu.addItem(withTitle: "Mute", action: #selector(toggleMuteAction(sender:)), keyEquivalent: "") - // Add transparency toggle menu item + let transparencyItem = NSMenuItem(title: "Transparency When Inactive", action: #selector(toggleTransparencyAction(sender:)), keyEquivalent: "") transparencyItem.state = isTransparencyEnabled ? .on : .off statusBarMenu.addItem(transparencyItem) + + let randomAnimationsItem = NSMenuItem(title: "Random Animations", action: #selector(toggleRandomAnimationsAction(sender:)), keyEquivalent: "") + randomAnimationsItem.state = isRandomAnimationsEnabled ? .on : .off + statusBarMenu.addItem(randomAnimationsItem) + + let alwaysOnTopItem = NSMenuItem(title: "Always On Top", action: #selector(toggleAlwaysOnTopAction(sender:)), keyEquivalent: "") + alwaysOnTopItem.state = isAlwaysOnTop ? .on : .off + statusBarMenu.addItem(alwaysOnTopItem) + statusBarMenu.addItem(NSMenuItem.separator()) guard let menuItem = agentsMenuItem else { return } @@ -107,32 +145,39 @@ class AppDelegate: NSObject, NSApplicationDelegate { statusBarMenu.addItem(NSMenuItem.separator()) statusBarMenu.addItem(withTitle: "Quit \(applicationName)", action: #selector(quitAction(sender:)), keyEquivalent: "") - // Agents menu + statusBarMenu.setSubmenu(createAgentsMenu(), for: menuItem) + statusItem?.menu = statusBarMenu } + @objc func quitAction(sender: AnyObject) { NSApplication.shared.terminate(self) } + @objc func reloadAction(sender: AnyObject) { agentsMenuItem?.submenu = createAgentsMenu() } + @objc func openFolderAction(sender: AnyObject) { NSWorkspace.shared.open(Agent.agentsURL()) } + @objc func hideAction(sender: AnyObject) { AppDelegate.agentController?.hide() } + @objc func showAction(sender: AnyObject) { window?.makeKeyAndOrderFront(self) } + @objc func toggleMuteAction(sender: AnyObject) { guard let menuItem = sender as? NSMenuItem else { return } guard let isMuted = AppDelegate.agentController?.isMuted else { return } @@ -141,20 +186,40 @@ class AppDelegate: NSObject, NSApplicationDelegate { menuItem.state = newValue ? .on : .off } + @objc func toggleTransparencyAction(sender: AnyObject) { guard let menuItem = sender as? NSMenuItem else { return } isTransparencyEnabled = !isTransparencyEnabled menuItem.state = isTransparencyEnabled ? .on : .off + if let agentWindow = window, !agentWindow.isKeyWindow { agentWindow.alphaValue = isTransparencyEnabled ? 0.5 : 1.0 } } + + @objc func toggleRandomAnimationsAction(sender: AnyObject) { + guard let menuItem = sender as? NSMenuItem else { return } + isRandomAnimationsEnabled = !isRandomAnimationsEnabled + menuItem.state = isRandomAnimationsEnabled ? .on : .off + } + + @objc func toggleAlwaysOnTopAction(sender: AnyObject) { + guard let menuItem = sender as? NSMenuItem else { return } + isAlwaysOnTop = !isAlwaysOnTop + menuItem.state = isAlwaysOnTop ? .on : .off + } + + func updateWindowLevel() { + window?.level = isAlwaysOnTop ? .floating : .normal + } + @objc func selectAgent(sender: AnyObject) { guard let menuItem = sender as? NSMenuItem else { return } let name = menuItem.title.lowercased() + if let isVisible = window?.isVisible, isVisible == true { try? AppDelegate.agentController?.load(name: name) if let animation = AppDelegate.agentController?.agent?.findAnimation("Show") { @@ -165,13 +230,14 @@ class AppDelegate: NSObject, NSApplicationDelegate { window?.makeKeyAndOrderFront(self) } + agentsMenuItem?.submenu = createAgentsMenu() } } + extension AppDelegate: NSMenuDelegate { func menuNeedsUpdate(_ menu: NSMenu) { - // Update menu item states when menu is about to open for item in menu.items { if item.title == "Mute" { if let isMuted = AppDelegate.agentController?.isMuted { @@ -179,12 +245,19 @@ extension AppDelegate: NSMenuDelegate { } } else if item.title == "Transparency When Inactive" { item.state = isTransparencyEnabled ? .on : .off + } else if item.title == "Random Animations" { + item.state = isRandomAnimationsEnabled ? .on : .off + } else if item.title == "Always On Top" { + item.state = isAlwaysOnTop ? .on : .off } } } } + extension Notification.Name { static let transparencyDidChange = Notification.Name("transparencyDidChange") static let muteDidChange = Notification.Name("muteDidChange") + static let randomAnimationsDidChange = Notification.Name("randomAnimationsDidChange") + static let alwaysOnTopDidChange = Notification.Name("alwaysOnTopDidChange") } diff --git a/Clippy.xcodeproj/project.pbxproj b/Clippy.xcodeproj/project.pbxproj index be17efb..7100eae 100644 --- a/Clippy.xcodeproj/project.pbxproj +++ b/Clippy.xcodeproj/project.pbxproj @@ -522,7 +522,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 2; - DEVELOPMENT_TEAM = YY62Y6T8GK; + DEVELOPMENT_TEAM = Z3ZG8RPGHF; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Clippy macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( @@ -546,7 +546,7 @@ CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 2; - DEVELOPMENT_TEAM = YY62Y6T8GK; + DEVELOPMENT_TEAM = Z3ZG8RPGHF; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Clippy macOS/Info.plist"; LD_RUNPATH_SEARCH_PATHS = (