🚀 First official release of ShotScreen with complete feature set: ✨ Core Features: - Advanced screenshot capture system - Multi-monitor support - Professional UI/UX design - Automated update system with Sparkle - Apple notarized & code signed 🛠 Technical Excellence: - Native Swift macOS application - Professional build & deployment pipeline - Comprehensive error handling - Memory optimized performance 📦 Distribution Ready: - Professional DMG packaging - Apple notarization complete - No security warnings for users - Ready for public distribution This is the foundation release that establishes ShotScreen as a premium screenshot tool for macOS.
783 lines
33 KiB
Swift
783 lines
33 KiB
Swift
import AppKit
|
||
|
||
// MARK: - Menu Management
|
||
class MenuManager {
|
||
weak var delegate: MenuManagerDelegate?
|
||
private var statusItem: NSStatusItem?
|
||
|
||
init(delegate: MenuManagerDelegate) {
|
||
self.delegate = delegate
|
||
|
||
// Add observer for shortcut changes to update menu
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(updateShortcuts),
|
||
name: .shortcutSettingChanged,
|
||
object: nil
|
||
)
|
||
|
||
// Add observer for desktop icons setting changes
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(handleDesktopIconsSettingChanged),
|
||
name: .hideDesktopIconsSettingChanged,
|
||
object: nil
|
||
)
|
||
|
||
// Add observer for desktop widgets setting changes
|
||
NotificationCenter.default.addObserver(
|
||
self,
|
||
selector: #selector(handleDesktopWidgetsSettingChanged),
|
||
name: .hideDesktopWidgetsSettingChanged,
|
||
object: nil
|
||
)
|
||
}
|
||
|
||
func setupMenu() {
|
||
setupMainMenu()
|
||
setupStatusBarMenu()
|
||
|
||
// Set initial shortcuts
|
||
updateShortcuts()
|
||
}
|
||
|
||
private func setupMainMenu() {
|
||
// Create main menu
|
||
let mainMenu = NSMenu()
|
||
|
||
// Application menu (first menu)
|
||
let appMenu = NSMenu()
|
||
let appName = "ShotScreen"
|
||
|
||
// About item with info icon
|
||
let aboutItem = NSMenuItem(title: "About \(appName)", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "")
|
||
if let infoIcon = NSImage(systemSymbolName: "info.circle", accessibilityDescription: "About") {
|
||
infoIcon.size = NSSize(width: 16, height: 16)
|
||
aboutItem.image = infoIcon
|
||
}
|
||
appMenu.addItem(aboutItem)
|
||
appMenu.addItem(NSMenuItem.separator())
|
||
|
||
// Settings item with gear icon
|
||
let settingsItem = NSMenuItem(title: "Settings", action: #selector(MenuManager.openSettings), keyEquivalent: ",")
|
||
settingsItem.target = self
|
||
if let gearIcon = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings") {
|
||
gearIcon.size = NSSize(width: 16, height: 16)
|
||
settingsItem.image = gearIcon
|
||
}
|
||
appMenu.addItem(settingsItem)
|
||
|
||
// Show Stash item with folder icon
|
||
let showStashItem = NSMenuItem(title: "Show Stash", action: #selector(MenuManager.showStash), keyEquivalent: "s")
|
||
showStashItem.target = self
|
||
if let folderIcon = NSImage(systemSymbolName: "folder", accessibilityDescription: "Folder") {
|
||
folderIcon.size = NSSize(width: 16, height: 16)
|
||
showStashItem.image = folderIcon
|
||
}
|
||
appMenu.addItem(showStashItem)
|
||
|
||
// NIEUW: Reset First Launch Wizard (for testing)
|
||
appMenu.addItem(NSMenuItem.separator())
|
||
let resetWizardItem = NSMenuItem(title: "Reset First Launch Wizard", action: #selector(MenuManager.resetFirstLaunchWizard), keyEquivalent: "")
|
||
resetWizardItem.target = self
|
||
if let resetIcon = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: "Reset") {
|
||
resetIcon.size = NSSize(width: 16, height: 16)
|
||
resetWizardItem.image = resetIcon
|
||
}
|
||
appMenu.addItem(resetWizardItem)
|
||
|
||
appMenu.addItem(NSMenuItem.separator())
|
||
|
||
// Quit item with power icon
|
||
let quitItem = NSMenuItem(title: "Quit \(appName)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
|
||
if let powerIcon = NSImage(systemSymbolName: "power", accessibilityDescription: "Quit") {
|
||
powerIcon.size = NSSize(width: 16, height: 16)
|
||
quitItem.image = powerIcon
|
||
}
|
||
appMenu.addItem(quitItem)
|
||
|
||
// Add app menu to main menu
|
||
let appMenuItem = NSMenuItem(title: appName, action: nil, keyEquivalent: "")
|
||
appMenuItem.submenu = appMenu
|
||
mainMenu.addItem(appMenuItem)
|
||
|
||
// Set the menu
|
||
NSApplication.shared.mainMenu = mainMenu
|
||
}
|
||
|
||
private func setupStatusBarMenu() {
|
||
// Create status item in menu bar
|
||
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||
if let button = statusItem?.button {
|
||
// Use MenuIcon for status bar instead of AppIcon
|
||
if let bundle = Bundle.main.url(forResource: "MenuIcon", withExtension: "png", subdirectory: "images"),
|
||
let menuIcon = NSImage(contentsOf: bundle) {
|
||
menuIcon.size = NSSize(width: 16, height: 16)
|
||
button.image = menuIcon
|
||
} else if let menuIcon = NSImage(named: "MenuIcon") {
|
||
menuIcon.size = NSSize(width: 16, height: 16)
|
||
button.image = menuIcon
|
||
} else if let screenshotIcon = NSImage(systemSymbolName: "camera.aperture", accessibilityDescription: "ShotScreen") {
|
||
// Fallback to aperture icon if menu icon not available
|
||
screenshotIcon.size = NSSize(width: 16, height: 16)
|
||
button.image = screenshotIcon
|
||
} else {
|
||
// Final fallback to green dot
|
||
button.image = NSImage(named: "NSStatusAvailable")
|
||
}
|
||
|
||
// Debug: Print all status bar items
|
||
debugPrintStatusBarItems()
|
||
|
||
// Create a menu for the status item
|
||
let statusMenu = NSMenu()
|
||
|
||
// Take Screenshot menu item with camera icon
|
||
let screenshotItem = NSMenuItem(title: "Take Screenshot", action: #selector(MenuManager.triggerScreenshot), keyEquivalent: "")
|
||
screenshotItem.target = self
|
||
if let cameraIcon = NSImage(systemSymbolName: "camera", accessibilityDescription: "Camera") {
|
||
cameraIcon.size = NSSize(width: 16, height: 16)
|
||
screenshotItem.image = cameraIcon
|
||
}
|
||
updateScreenshotShortcut(screenshotItem) // Set initial shortcut
|
||
statusMenu.addItem(screenshotItem)
|
||
|
||
// NIEUW: Capture Whole Screen (Double Hotkey) menu item with monitor icon
|
||
let wholeScreenItem = NSMenuItem(title: "Capture This Screen", action: #selector(MenuManager.triggerWholeScreenCapture), keyEquivalent: "")
|
||
wholeScreenItem.target = self
|
||
if let monitorIcon = NSImage(systemSymbolName: "display", accessibilityDescription: "Monitor") {
|
||
monitorIcon.size = NSSize(width: 16, height: 16)
|
||
wholeScreenItem.image = monitorIcon
|
||
}
|
||
updateWholeScreenCaptureShortcut(wholeScreenItem) // Set initial shortcut
|
||
statusMenu.addItem(wholeScreenItem)
|
||
|
||
// NIEUW: Capture All Screens menu item
|
||
let allScreensItem = NSMenuItem(title: "Capture All Screens", action: #selector(MenuManager.triggerAllScreensCapture), keyEquivalent: "")
|
||
allScreensItem.target = self
|
||
if let screensIcon = NSImage(systemSymbolName: "rectangle.3.group", accessibilityDescription: "All Screens") {
|
||
screensIcon.size = NSSize(width: 16, height: 16)
|
||
allScreensItem.image = screensIcon
|
||
}
|
||
updateAllScreensCaptureShortcut(allScreensItem) // Set initial shortcut
|
||
statusMenu.addItem(allScreensItem)
|
||
|
||
// NIEUW: Simple Window Capture menu item (native macOS)
|
||
let windowCaptureItem = NSMenuItem(title: "Capture Window", action: #selector(MenuManager.triggerNativeWindowCapture), keyEquivalent: "")
|
||
windowCaptureItem.target = self
|
||
if let windowIcon = NSImage(systemSymbolName: "macwindow", accessibilityDescription: "Window") {
|
||
windowIcon.size = NSSize(width: 16, height: 16)
|
||
windowCaptureItem.image = windowIcon
|
||
}
|
||
updateWindowCaptureShortcut(windowCaptureItem) // Set initial shortcut display
|
||
statusMenu.addItem(windowCaptureItem)
|
||
|
||
// Add separator between screenshot actions and other options
|
||
statusMenu.addItem(NSMenuItem.separator())
|
||
|
||
// NIEUW: Toggle Desktop Icons menu item
|
||
let hideIconsItem = NSMenuItem(title: "", action: #selector(MenuManager.toggleDesktopIcons), keyEquivalent: "")
|
||
hideIconsItem.target = self
|
||
updateDesktopIconsMenuItem(hideIconsItem) // Set initial title and icon
|
||
statusMenu.addItem(hideIconsItem)
|
||
print("➕ Desktop Icons menu item added with initial state")
|
||
|
||
// NIEUW: Toggle Desktop Widgets menu item
|
||
let hideWidgetsItem = NSMenuItem(title: "", action: #selector(MenuManager.toggleDesktopWidgets), keyEquivalent: "")
|
||
hideWidgetsItem.target = self
|
||
updateDesktopWidgetsMenuItem(hideWidgetsItem) // Set initial title and icon
|
||
statusMenu.addItem(hideWidgetsItem)
|
||
print("➕ Desktop Widgets menu item added with initial state")
|
||
|
||
// Add separator between screenshot actions and other options
|
||
statusMenu.addItem(NSMenuItem.separator())
|
||
|
||
// Show Stash menu item with folder icon
|
||
let stashItem = NSMenuItem(title: "Show Stash", action: #selector(MenuManager.showStash), keyEquivalent: "")
|
||
stashItem.target = self
|
||
if let folderIcon = NSImage(systemSymbolName: "folder", accessibilityDescription: "Folder") {
|
||
folderIcon.size = NSSize(width: 16, height: 16)
|
||
stashItem.image = folderIcon
|
||
}
|
||
statusMenu.addItem(stashItem)
|
||
|
||
|
||
|
||
// 🔄 UPDATE: Check for Updates menu item with arrow icon
|
||
let updatesItem = NSMenuItem(title: "Check for Updates", action: #selector(MenuManager.checkForUpdates), keyEquivalent: "")
|
||
updatesItem.target = self
|
||
updatesItem.isEnabled = UpdateManager.shared.isUpdaterAvailable
|
||
if let updateIcon = NSImage(systemSymbolName: "arrow.clockwise.circle", accessibilityDescription: "Check for Updates") {
|
||
updateIcon.size = NSSize(width: 16, height: 16)
|
||
updatesItem.image = updateIcon
|
||
}
|
||
|
||
|
||
statusMenu.addItem(NSMenuItem.separator())
|
||
|
||
// NIEUW: Reset First Launch Wizard (for testing) - also in status bar menu
|
||
let resetWizardItem = NSMenuItem(title: "Reset First Launch Wizard", action: #selector(MenuManager.resetFirstLaunchWizard), keyEquivalent: "")
|
||
resetWizardItem.target = self
|
||
if let resetIcon = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: "Reset") {
|
||
resetIcon.size = NSSize(width: 16, height: 16)
|
||
resetWizardItem.image = resetIcon
|
||
}
|
||
statusMenu.addItem(resetWizardItem)
|
||
|
||
statusMenu.addItem(NSMenuItem.separator())
|
||
// Settings menu item with gear icon
|
||
let settingsItem = NSMenuItem(title: "Settings", action: #selector(MenuManager.openSettings), keyEquivalent: "")
|
||
settingsItem.target = self
|
||
if let gearIcon = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings") {
|
||
gearIcon.size = NSSize(width: 16, height: 16)
|
||
settingsItem.image = gearIcon
|
||
}
|
||
statusMenu.addItem(settingsItem)
|
||
// Quit menu item with power icon
|
||
let quitItem = NSMenuItem(title: "Quit ShotScreen", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "")
|
||
if let powerIcon = NSImage(systemSymbolName: "power", accessibilityDescription: "Quit") {
|
||
powerIcon.size = NSSize(width: 16, height: 16)
|
||
quitItem.image = powerIcon
|
||
}
|
||
statusMenu.addItem(quitItem)
|
||
|
||
statusItem?.menu = statusMenu
|
||
}
|
||
}
|
||
|
||
// MARK: - Menu Actions
|
||
@objc func triggerScreenshot() {
|
||
delegate?.triggerScreenshot()
|
||
}
|
||
|
||
@objc func triggerWholeScreenCapture() {
|
||
delegate?.triggerWholeScreenCapture()
|
||
}
|
||
|
||
@objc func triggerNativeWindowCapture() {
|
||
delegate?.triggerNativeWindowCapture()
|
||
}
|
||
|
||
@objc func triggerAllScreensCapture() {
|
||
delegate?.triggerAllScreensCapture()
|
||
}
|
||
|
||
@objc func openSettings() {
|
||
delegate?.openSettings()
|
||
}
|
||
|
||
@objc func showStash() {
|
||
delegate?.showStash()
|
||
}
|
||
|
||
@objc func resetFirstLaunchWizard() {
|
||
delegate?.resetFirstLaunchWizard()
|
||
}
|
||
|
||
@objc func toggleDesktopIcons() {
|
||
delegate?.toggleDesktopIcons()
|
||
}
|
||
|
||
@objc func toggleDesktopWidgets() {
|
||
delegate?.toggleDesktopWidgets()
|
||
}
|
||
|
||
@objc func checkForUpdates() {
|
||
delegate?.checkForUpdates()
|
||
}
|
||
|
||
// MARK: - Dynamic Version Helper
|
||
private func getDynamicAboutTitle() -> String {
|
||
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
|
||
return "ShotScreen v\(version)"
|
||
}
|
||
|
||
@objc func showAbout() {
|
||
delegate?.showAbout()
|
||
}
|
||
|
||
@objc func exitApp() {
|
||
delegate?.shouldTerminate = true
|
||
NSApplication.shared.terminate(self)
|
||
}
|
||
|
||
// MARK: - Menu Item Updates
|
||
private func updateDesktopIconsMenuItem(_ menuItem: NSMenuItem) {
|
||
let isHidden = SettingsManager.shared.hideDesktopIconsDuringScreenshot
|
||
|
||
if isHidden {
|
||
menuItem.title = "Show Desktop Icons"
|
||
// Try specific icon first, fallback to generic folder icon
|
||
if let icon = NSImage(systemSymbolName: "folder.badge.minus", accessibilityDescription: "Desktop Icons Hidden") {
|
||
icon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = icon
|
||
} else if let fallbackIcon = NSImage(systemSymbolName: "folder", accessibilityDescription: "Folder") {
|
||
fallbackIcon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = fallbackIcon
|
||
} else {
|
||
menuItem.image = nil
|
||
}
|
||
} else {
|
||
menuItem.title = "Hide Desktop Icons"
|
||
// Try specific icon first, fallback to generic folder icon
|
||
if let icon = NSImage(systemSymbolName: "folder.badge.plus", accessibilityDescription: "Desktop Icons Visible") {
|
||
icon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = icon
|
||
} else if let fallbackIcon = NSImage(systemSymbolName: "folder", accessibilityDescription: "Folder") {
|
||
fallbackIcon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = fallbackIcon
|
||
} else {
|
||
menuItem.image = nil
|
||
}
|
||
}
|
||
|
||
print("🖥️ Desktop Icons menu item updated: '\(menuItem.title)' (Hidden: \(isHidden))")
|
||
}
|
||
|
||
private func updateDesktopWidgetsMenuItem(_ menuItem: NSMenuItem) {
|
||
let isHidden = SettingsManager.shared.hideDesktopWidgetsDuringScreenshot
|
||
|
||
if isHidden {
|
||
menuItem.title = "Show Desktop Widgets"
|
||
// Try specific widget icons first, fallback to simpler rectangle icon
|
||
if let icon = NSImage(systemSymbolName: "rectangle.3.group.bubble", accessibilityDescription: "Desktop Widgets Hidden") {
|
||
icon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = icon
|
||
} else if let fallbackIcon = NSImage(systemSymbolName: "rectangle.3.group", accessibilityDescription: "Widgets") {
|
||
fallbackIcon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = fallbackIcon
|
||
} else if let simpleIcon = NSImage(systemSymbolName: "rectangle.grid.2x2", accessibilityDescription: "Grid") {
|
||
simpleIcon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = simpleIcon
|
||
} else {
|
||
menuItem.image = nil
|
||
}
|
||
} else {
|
||
menuItem.title = "Hide Desktop Widgets"
|
||
// Try specific widget icons first, fallback to simpler rectangle icon
|
||
if let icon = NSImage(systemSymbolName: "rectangle.3.group.bubble.fill", accessibilityDescription: "Desktop Widgets Visible") {
|
||
icon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = icon
|
||
} else if let fallbackIcon = NSImage(systemSymbolName: "rectangle.3.group.fill", accessibilityDescription: "Widgets") {
|
||
fallbackIcon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = fallbackIcon
|
||
} else if let simpleIcon = NSImage(systemSymbolName: "rectangle.grid.2x2.fill", accessibilityDescription: "Grid") {
|
||
simpleIcon.size = NSSize(width: 16, height: 16)
|
||
menuItem.image = simpleIcon
|
||
} else {
|
||
menuItem.image = nil
|
||
}
|
||
}
|
||
|
||
print("📱 Desktop Widgets menu item updated: '\(menuItem.title)' (Hidden: \(isHidden))")
|
||
}
|
||
|
||
// Public method to refresh menu when settings change
|
||
func refreshDesktopIconsMenuItem() {
|
||
print("🔄 Refreshing desktop icons menu item...")
|
||
if let menu = statusItem?.menu {
|
||
// Find the desktop icons menu item (it should be the one with our action)
|
||
var foundItem = false
|
||
for item in menu.items {
|
||
if item.action == #selector(MenuManager.toggleDesktopIcons) {
|
||
updateDesktopIconsMenuItem(item)
|
||
foundItem = true
|
||
break
|
||
}
|
||
}
|
||
if !foundItem {
|
||
print("⚠️ Desktop icons menu item not found!")
|
||
}
|
||
} else {
|
||
print("⚠️ Status item menu not available!")
|
||
}
|
||
}
|
||
|
||
// Public method to refresh widgets menu when settings change
|
||
func refreshDesktopWidgetsMenuItem() {
|
||
print("🔄 Refreshing desktop widgets menu item...")
|
||
if let menu = statusItem?.menu {
|
||
// Find the desktop widgets menu item (it should be the one with our action)
|
||
var foundItem = false
|
||
for item in menu.items {
|
||
if item.action == #selector(MenuManager.toggleDesktopWidgets) {
|
||
updateDesktopWidgetsMenuItem(item)
|
||
foundItem = true
|
||
break
|
||
}
|
||
}
|
||
if !foundItem {
|
||
print("⚠️ Desktop widgets menu item not found!")
|
||
}
|
||
} else {
|
||
print("⚠️ Status item menu not available!")
|
||
}
|
||
}
|
||
|
||
// MARK: - Settings Change Handlers
|
||
@objc private func handleDesktopIconsSettingChanged() {
|
||
print("📢 Notification received: Desktop Icons setting changed")
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.refreshDesktopIconsMenuItem()
|
||
}
|
||
}
|
||
|
||
@objc private func handleDesktopWidgetsSettingChanged() {
|
||
print("📢 Notification received: Desktop Widgets setting changed")
|
||
DispatchQueue.main.async { [weak self] in
|
||
self?.refreshDesktopWidgetsMenuItem()
|
||
}
|
||
}
|
||
|
||
// MARK: - Shortcut Management
|
||
@objc func updateShortcuts() {
|
||
// Update shortcuts in both main menu and status bar menu
|
||
updateMenuShortcuts()
|
||
}
|
||
|
||
private func updateMenuShortcuts() {
|
||
// Update status bar menu shortcuts
|
||
if let menu = statusItem?.menu {
|
||
for item in menu.items {
|
||
if item.action == #selector(MenuManager.triggerScreenshot) {
|
||
updateScreenshotShortcut(item)
|
||
} else if item.action == #selector(MenuManager.triggerWholeScreenCapture) {
|
||
updateWholeScreenCaptureShortcut(item)
|
||
} else if item.action == #selector(MenuManager.triggerNativeWindowCapture) {
|
||
updateWindowCaptureShortcut(item)
|
||
} else if item.action == #selector(MenuManager.triggerAllScreensCapture) {
|
||
updateAllScreensCaptureShortcut(item)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Update main menu shortcuts if needed
|
||
updateMainMenuShortcuts()
|
||
}
|
||
|
||
private func updateMainMenuShortcuts() {
|
||
// Update shortcuts in the main application menu
|
||
if let mainMenu = NSApplication.shared.mainMenu {
|
||
// Find the application menu (first submenu)
|
||
if let appMenuItem = mainMenu.items.first,
|
||
let appSubmenu = appMenuItem.submenu {
|
||
for item in appSubmenu.items {
|
||
if item.action == #selector(MenuManager.openSettings) {
|
||
// Settings already has "," shortcut, keep it
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private func updateScreenshotShortcut(_ menuItem: NSMenuItem) {
|
||
let settings = SettingsManager.shared
|
||
|
||
if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 {
|
||
// Use custom shortcut - display only, same as others
|
||
let shortcutText = getShortcutDisplay(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey)
|
||
setMenuItemWithRightAlignedShortcut(menuItem, title: "Take Area Screenshot", shortcut: shortcutText)
|
||
} else {
|
||
// Use default Cmd+Shift+4 - display only, positioned slightly to the left
|
||
setMenuItemWithRightAlignedShortcut(menuItem, title: "Take Area Screenshot", shortcut: "⌘⇧4")
|
||
}
|
||
|
||
// Clear actual key equivalents since these are display-only
|
||
menuItem.keyEquivalent = ""
|
||
menuItem.keyEquivalentModifierMask = []
|
||
}
|
||
|
||
private func updateWholeScreenCaptureShortcut(_ menuItem: NSMenuItem) {
|
||
let settings = SettingsManager.shared
|
||
let baseShortcut: String
|
||
|
||
if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 {
|
||
// Use custom shortcut
|
||
baseShortcut = getShortcutDisplay(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey)
|
||
} else {
|
||
// Use default shortcut
|
||
baseShortcut = "⌘⇧4"
|
||
}
|
||
|
||
// Display shortcut as: 2 x [shortcut] (double hotkey for whole screen)
|
||
setMenuItemWithRightAlignedShortcut(menuItem, title: "Capture Screen", shortcut: "2 x \(baseShortcut)")
|
||
|
||
// Clear actual key equivalents since these are display-only
|
||
menuItem.keyEquivalent = ""
|
||
menuItem.keyEquivalentModifierMask = []
|
||
}
|
||
|
||
private func updateWindowCaptureShortcut(_ menuItem: NSMenuItem) {
|
||
let settings = SettingsManager.shared
|
||
let baseShortcut: String
|
||
|
||
if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 {
|
||
// Use custom shortcut
|
||
baseShortcut = getShortcutDisplay(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey)
|
||
} else {
|
||
// Use default shortcut
|
||
baseShortcut = "⌘⇧4"
|
||
}
|
||
|
||
// Display shortcut as: [shortcut] → ␣ (indicating spacebar during selection)
|
||
setMenuItemWithRightAlignedShortcut(menuItem, title: "Capture Window", shortcut: "\(baseShortcut) → ␣")
|
||
|
||
// Clear actual key equivalents since these are display-only
|
||
menuItem.keyEquivalent = ""
|
||
menuItem.keyEquivalentModifierMask = []
|
||
}
|
||
|
||
private func updateAllScreensCaptureShortcut(_ menuItem: NSMenuItem) {
|
||
let settings = SettingsManager.shared
|
||
let baseShortcut: String
|
||
|
||
if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 {
|
||
// Use custom shortcut
|
||
baseShortcut = getShortcutDisplay(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey)
|
||
} else {
|
||
// Use default shortcut
|
||
baseShortcut = "⌘⇧4"
|
||
}
|
||
|
||
// Triple hotkey for all screens capture
|
||
setMenuItemWithRightAlignedShortcut(menuItem, title: "Capture All Screens", shortcut: "3 x \(baseShortcut)")
|
||
|
||
// Clear actual key equivalents since these are display-only
|
||
menuItem.keyEquivalent = ""
|
||
menuItem.keyEquivalentModifierMask = []
|
||
}
|
||
|
||
private func setMenuItemWithRightAlignedShortcut(_ menuItem: NSMenuItem, title: String, shortcut: String) {
|
||
// Create attributed string with proper tab stop for right alignment
|
||
let attributedTitle = NSMutableAttributedString()
|
||
|
||
// Create paragraph style with right-aligned tab stop
|
||
let paragraphStyle = NSMutableParagraphStyle()
|
||
|
||
// Calculate the right tab stop position (approximately 250 points from left margin)
|
||
// This gives us consistent right alignment for all shortcuts
|
||
let tabStopPosition: CGFloat = 250.0
|
||
|
||
// Create a right-aligned tab stop
|
||
let tabStop = NSTextTab(textAlignment: .right, location: tabStopPosition, options: [:])
|
||
paragraphStyle.tabStops = [tabStop]
|
||
|
||
// Add the main title with normal styling
|
||
let titleString = NSAttributedString(string: title, attributes: [
|
||
.font: NSFont.menuFont(ofSize: 0),
|
||
.paragraphStyle: paragraphStyle
|
||
])
|
||
attributedTitle.append(titleString)
|
||
|
||
// Add tab character to move to the tab stop
|
||
let tabString = NSAttributedString(string: "\t", attributes: [
|
||
.paragraphStyle: paragraphStyle
|
||
])
|
||
attributedTitle.append(tabString)
|
||
|
||
// Add the shortcut with different styling at the tab stop (right-aligned)
|
||
let shortcutString = NSAttributedString(string: shortcut, attributes: [
|
||
.foregroundColor: NSColor.secondaryLabelColor,
|
||
.font: NSFont.menuFont(ofSize: 0),
|
||
.paragraphStyle: paragraphStyle
|
||
])
|
||
attributedTitle.append(shortcutString)
|
||
|
||
menuItem.attributedTitle = attributedTitle
|
||
}
|
||
|
||
private func getShortcutDisplay(modifiers: UInt, keyCode: UInt16) -> String {
|
||
var result = ""
|
||
|
||
// Add modifier symbols in correct order: Control, Option, Shift, Command
|
||
if modifiers & (1 << 3) != 0 { result += "⌃" } // Control
|
||
if modifiers & (1 << 2) != 0 { result += "⌥" } // Option
|
||
if modifiers & (1 << 1) != 0 { result += "⇧" } // Shift
|
||
if modifiers & (1 << 0) != 0 { result += "⌘" } // Command
|
||
|
||
let keyString = keyCodeToDisplayString(keyCode)
|
||
result += keyString
|
||
|
||
return result
|
||
}
|
||
|
||
private func keyCodeToDisplayString(_ keyCode: UInt16) -> String {
|
||
// Convert keyCode to display string (uppercase for better readability)
|
||
switch keyCode {
|
||
case 0: return "A"
|
||
case 1: return "S"
|
||
case 2: return "D"
|
||
case 3: return "F"
|
||
case 4: return "H"
|
||
case 5: return "G"
|
||
case 6: return "Z"
|
||
case 7: return "X"
|
||
case 8: return "C"
|
||
case 9: return "V"
|
||
case 11: return "B"
|
||
case 12: return "Q"
|
||
case 13: return "W"
|
||
case 14: return "E"
|
||
case 15: return "R"
|
||
case 16: return "Y"
|
||
case 17: return "T"
|
||
case 18: return "1"
|
||
case 19: return "2"
|
||
case 20: return "3"
|
||
case 21: return "4"
|
||
case 22: return "6"
|
||
case 23: return "5"
|
||
case 24: return "="
|
||
case 25: return "9"
|
||
case 26: return "7"
|
||
case 27: return "-"
|
||
case 28: return "8"
|
||
case 29: return "0"
|
||
case 30: return "]"
|
||
case 31: return "O"
|
||
case 32: return "U"
|
||
case 33: return "["
|
||
case 34: return "I"
|
||
case 35: return "P"
|
||
case 37: return "L"
|
||
case 38: return "J"
|
||
case 39: return "'"
|
||
case 40: return "K"
|
||
case 41: return ";"
|
||
case 42: return "\\"
|
||
case 43: return ","
|
||
case 44: return "/"
|
||
case 45: return "N"
|
||
case 46: return "M"
|
||
case 47: return "."
|
||
case 50: return "`"
|
||
default: return ""
|
||
}
|
||
}
|
||
|
||
private func getModifierSymbols(_ modifier: UInt) -> String {
|
||
var symbols: [String] = []
|
||
|
||
if modifier & (1 << 3) != 0 { symbols.append("⌃") } // Control
|
||
if modifier & (1 << 2) != 0 { symbols.append("␣") } // Option (now Space symbol)
|
||
if modifier & (1 << 1) != 0 { symbols.append("⇧") } // Shift
|
||
if modifier & (1 << 0) != 0 { symbols.append("⌘") } // Command
|
||
|
||
return symbols.joined()
|
||
}
|
||
|
||
// MARK: - License Management
|
||
func setAppEnabled(_ enabled: Bool) {
|
||
print("🔐 LICENSE: Setting app menu enabled state to: \(enabled)")
|
||
|
||
guard let menu = statusItem?.menu else {
|
||
print("⚠️ LICENSE: Status item menu not available!")
|
||
return
|
||
}
|
||
|
||
// Disable/enable screenshot-related menu items
|
||
for item in menu.items {
|
||
if item.action == #selector(MenuManager.triggerScreenshot) ||
|
||
item.action == #selector(MenuManager.triggerWholeScreenCapture) ||
|
||
item.action == #selector(MenuManager.triggerNativeWindowCapture) ||
|
||
item.action == #selector(MenuManager.triggerAllScreensCapture) ||
|
||
item.action == #selector(MenuManager.showStash) ||
|
||
item.action == #selector(MenuManager.toggleDesktopIcons) ||
|
||
item.action == #selector(MenuManager.toggleDesktopWidgets) {
|
||
item.isEnabled = enabled
|
||
}
|
||
// Keep Settings, Updates, About, and Quit always enabled
|
||
}
|
||
|
||
// Update menu icon to indicate license status
|
||
if !enabled {
|
||
// Show different icon for trial/expired - use lock icon
|
||
if let lockIcon = NSImage(systemSymbolName: "lock.fill", accessibilityDescription: "License Required") {
|
||
lockIcon.size = NSSize(width: 16, height: 16)
|
||
statusItem?.button?.image = lockIcon
|
||
}
|
||
statusItem?.button?.toolTip = "ShotScreen - License Required"
|
||
} else {
|
||
// Restore normal menu icon for licensed
|
||
if let bundle = Bundle.main.url(forResource: "MenuIcon", withExtension: "png", subdirectory: "images"),
|
||
let menuIcon = NSImage(contentsOf: bundle) {
|
||
menuIcon.size = NSSize(width: 16, height: 16)
|
||
statusItem?.button?.image = menuIcon
|
||
} else if let menuIcon = NSImage(named: "MenuIcon") {
|
||
menuIcon.size = NSSize(width: 16, height: 16)
|
||
statusItem?.button?.image = menuIcon
|
||
} else if let screenshotIcon = NSImage(systemSymbolName: "camera.aperture", accessibilityDescription: "ShotScreen") {
|
||
screenshotIcon.size = NSSize(width: 16, height: 16)
|
||
statusItem?.button?.image = screenshotIcon
|
||
}
|
||
statusItem?.button?.toolTip = "ShotScreen"
|
||
}
|
||
|
||
// Clear any title to prevent duplicate icons
|
||
statusItem?.button?.title = ""
|
||
}
|
||
|
||
// MARK: - Cleanup
|
||
func cleanup() {
|
||
print("🧹 MenuManager: Cleaning up...")
|
||
NotificationCenter.default.removeObserver(self)
|
||
|
||
if let item = statusItem {
|
||
NSStatusBar.system.removeStatusItem(item)
|
||
print("🧹 STATUS: Removed status bar item successfully")
|
||
}
|
||
statusItem = nil
|
||
}
|
||
|
||
deinit {
|
||
print("🗑️ MenuManager: Deinitializing and cleaning up")
|
||
cleanup()
|
||
}
|
||
|
||
private func debugPrintStatusBarItems() {
|
||
print("🔍 DEBUG: Checking all status bar items...")
|
||
|
||
// Try to get information about other status bar items
|
||
let statusBar = NSStatusBar.system
|
||
print("🔍 DEBUG: System status bar available: \(statusBar)")
|
||
|
||
// Check if there might be multiple ShotScreen processes running
|
||
let runningApps = NSWorkspace.shared.runningApplications
|
||
let shotScreenApps = runningApps.filter { $0.bundleIdentifier?.contains("ShotScreen") == true || $0.localizedName?.contains("ShotScreen") == true }
|
||
|
||
print("🔍 DEBUG: Found \(shotScreenApps.count) ShotScreen-related processes:")
|
||
for app in shotScreenApps {
|
||
print(" - \(app.localizedName ?? "Unknown") (PID: \(app.processIdentifier))")
|
||
}
|
||
|
||
// Check for screenshot-related apps
|
||
let screenshotApps = runningApps.filter {
|
||
let name = $0.localizedName?.lowercased() ?? ""
|
||
let bundle = $0.bundleIdentifier?.lowercased() ?? ""
|
||
return name.contains("screenshot") || name.contains("capture") || name.contains("screen") ||
|
||
bundle.contains("screenshot") || bundle.contains("capture") || bundle.contains("screen")
|
||
}
|
||
|
||
print("🔍 DEBUG: Found \(screenshotApps.count) screenshot/capture-related apps:")
|
||
for app in screenshotApps {
|
||
print(" - \(app.localizedName ?? "Unknown"): \(app.bundleIdentifier ?? "No Bundle ID")")
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - MenuManager Delegate Protocol
|
||
protocol MenuManagerDelegate: AnyObject {
|
||
func triggerScreenshot()
|
||
func triggerWholeScreenCapture()
|
||
func triggerNativeWindowCapture()
|
||
func triggerAllScreensCapture()
|
||
func openSettings()
|
||
func showStash()
|
||
func resetFirstLaunchWizard()
|
||
func toggleDesktopIcons()
|
||
func toggleDesktopWidgets()
|
||
func checkForUpdates()
|
||
func showAbout()
|
||
var shouldTerminate: Bool { get set }
|
||
} |