From f6f877a38d5eb4c56779bff7e886353314607604 Mon Sep 17 00:00:00 2001 From: gobbledygoober Date: Sun, 15 Mar 2026 17:01:16 -0400 Subject: [PATCH 1/3] Add submenu to select source resolution --- Quick Camera/QCAppDelegate.swift | 92 ++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/Quick Camera/QCAppDelegate.swift b/Quick Camera/QCAppDelegate.swift index 7730ac3..a90ee41 100644 --- a/Quick Camera/QCAppDelegate.swift +++ b/Quick Camera/QCAppDelegate.swift @@ -112,6 +112,10 @@ class QCAppDelegate: NSObject, NSApplicationDelegate, QCUsbWatcherDelegate { if deviceIndex < 9 { deviceMenuItem.keyEquivalent = String(deviceIndex + 1) } + + // Add resolution change menu item + addResolutionsTo(menuItem: deviceMenuItem, forDevice: device) + deviceMenu.addItem(deviceMenuItem) deviceIndex += 1 } @@ -472,6 +476,94 @@ class QCAppDelegate: NSObject, NSApplicationDelegate, QCUsbWatcherDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } + + // MARK: Resolutions + + var selectedSourceResolution: Int = -1 + + @objc func sourceResolutionsMenuChanged(_ sender: NSMenuItem) { + for menuItem: NSMenuItem in sender.parent!.submenu!.items { + menuItem.state = NSControl.StateValue.off; + } + sender.state = NSControl.StateValue.on; + + /// should have a pair of device to res + self.selectedSourceResolution = sender.representedObject as! Int + + let deviceMenuItem = sender.parent! + for menuItem: NSMenuItem in deviceMenuItem.parent!.submenu!.items { + menuItem.state = NSControl.StateValue.off; + } + deviceMenuItem.state = NSControl.StateValue.on + + self.selectedDeviceIndex = deviceMenuItem.representedObject as! Int + + /// then select the device and resolution + self.startCaptureWithVideoDevice(defaultDevice: self.selectedDeviceIndex) + self.applyResolutionToDevice() + } + + func applyResolutionToDevice() + { + let device = (self.captureSession.inputs[0] as! AVCaptureDeviceInput).device + try! device.lockForConfiguration() + + let format = device.formats[selectedSourceResolution] + device.activeFormat = format + + let maxFrameRateDuration = format.videoSupportedFrameRateRanges.reduce(CMTime.positiveInfinity) { (res, e) -> CMTime in + CMTimeMinimum(res, e.minFrameDuration) + } + device.activeVideoMinFrameDuration = maxFrameRateDuration + device.unlockForConfiguration() + } + + // MARK - source resolutions + func addResolutionsTo(menuItem: NSMenuItem, forDevice: AVCaptureDevice) { + + let selectedDevice = forDevice + let resolutionsMenu = NSMenu(); + + var resIndex = 0; + + + for res in selectedDevice.formats { + + let menuTitle = descriptionFor(resolution: res) + + //if not already in the menu + if(resolutionsMenu.indexOfItem(withTitle: menuTitle) == -1) { + + let deviceMenuItem = NSMenuItem(title: menuTitle, action: #selector(sourceResolutionsMenuChanged), keyEquivalent: "") + deviceMenuItem.target = self; + deviceMenuItem.representedObject = resIndex; + + if(selectedDevice.activeFormat == res) { + deviceMenuItem.state = NSControl.StateValue.on; + self.selectedSourceResolution = resIndex + } + + resolutionsMenu.addItem(deviceMenuItem); + + menuItem.submenu = resolutionsMenu + } + resIndex += 1; + + + } + } + + private func descriptionFor(resolution: AVCaptureDevice.Format) -> String { + var result = "" + if #available(OSX 10.15, *) { + result = "\(resolution.formatDescription.dimensions.width)x\(resolution.formatDescription.dimensions.height)" + } else { + result = "\(resolution)" // this looks fairly bad on mac os pre 10.15 + } + return result + } + + } // MARK: - Helper Functions From 5da48892215fcdd345befbc1a81477ffeeb91c81 Mon Sep 17 00:00:00 2001 From: gobbledygoober Date: Sun, 15 Mar 2026 20:02:43 -0400 Subject: [PATCH 2/3] Add preference pane with option to enable showing camera resolutions Resolution submenus could clutter the source picker for users who never need to change resolution. Gate them behind a new Preferences setting so the menu stays clean by default and does not confuse users. - Wire "Preferences..." menu item to the bottom of the source select menu - Resolution submenus are hidden unless enabled in Preferences - Changes take effect immediately without a restart AI-assisted: preference window scaffolding and UserDefaults wiring --- Quick Camera/QCAppDelegate.swift | 140 ++++++++++++++++++++++++------- 1 file changed, 112 insertions(+), 28 deletions(-) diff --git a/Quick Camera/QCAppDelegate.swift b/Quick Camera/QCAppDelegate.swift index a90ee41..8cdcfdb 100644 --- a/Quick Camera/QCAppDelegate.swift +++ b/Quick Camera/QCAppDelegate.swift @@ -2,6 +2,62 @@ import AVFoundation import AVKit import Cocoa +private let kShowResolutionOptions = "showCameraResolutionOptions" + +// MARK: - Preferences + +class QCPreferencesViewController: NSViewController { + + var onChange: (() -> Void)? + + private let checkbox = NSButton( + checkboxWithTitle: "Show camera resolution options", + target: nil, + action: nil + ) + + override func loadView() { + let container = NSView(frame: NSRect(x: 0, y: 0, width: 340, height: 80)) + checkbox.translatesAutoresizingMaskIntoConstraints = false + checkbox.target = self + checkbox.action = #selector(checkboxChanged(_:)) + checkbox.state = UserDefaults.standard.bool(forKey: kShowResolutionOptions) ? .on : .off + container.addSubview(checkbox) + NSLayoutConstraint.activate([ + checkbox.centerXAnchor.constraint(equalTo: container.centerXAnchor), + checkbox.centerYAnchor.constraint(equalTo: container.centerYAnchor), + ]) + self.view = container + } + + @objc private func checkboxChanged(_ sender: NSButton) { + UserDefaults.standard.set(sender.state == .on, forKey: kShowResolutionOptions) + onChange?() + } +} + +class QCPreferencesWindowController: NSWindowController { + + convenience init() { + let vc = QCPreferencesViewController() + let window = NSWindow(contentViewController: vc) + window.title = "Preferences" + window.styleMask = [.titled, .closable] + window.setContentSize(NSSize(width: 340, height: 80)) + window.center() + self.init(window: window) + } + + var preferencesViewController: QCPreferencesViewController? { + return contentViewController as? QCPreferencesViewController + } + + override func showWindow(_ sender: Any?) { + window?.center() + super.showWindow(sender) + } +} + // MARK: - QCAppDelegate Class @NSApplicationMain class QCAppDelegate: NSObject, NSApplicationDelegate, QCUsbWatcherDelegate { @@ -113,12 +169,25 @@ class QCAppDelegate: NSObject, NSApplicationDelegate, QCUsbWatcherDelegate { deviceMenuItem.keyEquivalent = String(deviceIndex + 1) } - // Add resolution change menu item - addResolutionsTo(menuItem: deviceMenuItem, forDevice: device) + // Add resolution change submenu only if enabled in Preferences + if UserDefaults.standard.bool(forKey: kShowResolutionOptions) { + addResolutionsTo(menuItem: deviceMenuItem, forDevice: device) + } deviceMenu.addItem(deviceMenuItem) deviceIndex += 1 } + + // Preferences item at the bottom of the source menu, separated + deviceMenu.addItem(NSMenuItem.separator()) + let prefsItem = NSMenuItem( + title: "Preferences...", + action: #selector(openPreferences), + keyEquivalent: "," + ) + prefsItem.target = self + deviceMenu.addItem(prefsItem) + selectSourceMenu.submenu = deviceMenu } @@ -476,83 +545,98 @@ class QCAppDelegate: NSObject, NSApplicationDelegate, QCUsbWatcherDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true } - + // MARK: Resolutions - + var selectedSourceResolution: Int = -1 - + var preferencesWindowController: QCPreferencesWindowController? + + // MARK: Preferences + + @objc func openPreferences() { + if preferencesWindowController == nil { + preferencesWindowController = QCPreferencesWindowController() + // Rebuild the device menu immediately whenever the checkbox is toggled + preferencesWindowController?.preferencesViewController?.onChange = { [weak self] in + self?.detectVideoDevices() + } + } + preferencesWindowController?.showWindow(self) + NSApp.activate(ignoringOtherApps: true) + } + @objc func sourceResolutionsMenuChanged(_ sender: NSMenuItem) { for menuItem: NSMenuItem in sender.parent!.submenu!.items { menuItem.state = NSControl.StateValue.off; } sender.state = NSControl.StateValue.on; - + /// should have a pair of device to res self.selectedSourceResolution = sender.representedObject as! Int - + let deviceMenuItem = sender.parent! for menuItem: NSMenuItem in deviceMenuItem.parent!.submenu!.items { menuItem.state = NSControl.StateValue.off; } deviceMenuItem.state = NSControl.StateValue.on - + self.selectedDeviceIndex = deviceMenuItem.representedObject as! Int - + /// then select the device and resolution self.startCaptureWithVideoDevice(defaultDevice: self.selectedDeviceIndex) self.applyResolutionToDevice() } - + func applyResolutionToDevice() { let device = (self.captureSession.inputs[0] as! AVCaptureDeviceInput).device try! device.lockForConfiguration() - + let format = device.formats[selectedSourceResolution] device.activeFormat = format - + let maxFrameRateDuration = format.videoSupportedFrameRateRanges.reduce(CMTime.positiveInfinity) { (res, e) -> CMTime in CMTimeMinimum(res, e.minFrameDuration) } device.activeVideoMinFrameDuration = maxFrameRateDuration device.unlockForConfiguration() } - + // MARK - source resolutions func addResolutionsTo(menuItem: NSMenuItem, forDevice: AVCaptureDevice) { - + let selectedDevice = forDevice let resolutionsMenu = NSMenu(); - + var resIndex = 0; - - + + for res in selectedDevice.formats { - + let menuTitle = descriptionFor(resolution: res) - + //if not already in the menu if(resolutionsMenu.indexOfItem(withTitle: menuTitle) == -1) { - + let deviceMenuItem = NSMenuItem(title: menuTitle, action: #selector(sourceResolutionsMenuChanged), keyEquivalent: "") deviceMenuItem.target = self; deviceMenuItem.representedObject = resIndex; - + if(selectedDevice.activeFormat == res) { deviceMenuItem.state = NSControl.StateValue.on; self.selectedSourceResolution = resIndex } - + resolutionsMenu.addItem(deviceMenuItem); - + menuItem.submenu = resolutionsMenu } resIndex += 1; - - + + } } - + private func descriptionFor(resolution: AVCaptureDevice.Format) -> String { var result = "" if #available(OSX 10.15, *) { @@ -562,8 +646,8 @@ class QCAppDelegate: NSObject, NSApplicationDelegate, QCUsbWatcherDelegate { } return result } - - + + } // MARK: - Helper Functions From 59159cd0fdf6c6a428fa79046bdbab787df2c4b1 Mon Sep 17 00:00:00 2001 From: gobbledygoober Date: Sat, 21 Mar 2026 15:46:08 -0400 Subject: [PATCH 3/3] Relocate Preferences from Select Source to main Quick Camera menu. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The existing Preferences… menu item (⌘,) in MainMenu.xib had no action connected. Wire it to openPreferences: on QCAppDelegate so the preferences window can be opened from the standard app menu location without requiring a restart to apply changes. AI-Assisted: XIB action connection wiring for Preferences… menu item --- Quick Camera/Base.lproj/MainMenu.xib | 6 +++++- Quick Camera/QCAppDelegate.swift | 21 +++------------------ 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/Quick Camera/Base.lproj/MainMenu.xib b/Quick Camera/Base.lproj/MainMenu.xib index 62b7886..0536a79 100644 --- a/Quick Camera/Base.lproj/MainMenu.xib +++ b/Quick Camera/Base.lproj/MainMenu.xib @@ -27,7 +27,11 @@ - + + + + + diff --git a/Quick Camera/QCAppDelegate.swift b/Quick Camera/QCAppDelegate.swift index 8cdcfdb..2743b63 100644 --- a/Quick Camera/QCAppDelegate.swift +++ b/Quick Camera/QCAppDelegate.swift @@ -178,16 +178,6 @@ class QCAppDelegate: NSObject, NSApplicationDelegate, QCUsbWatcherDelegate { deviceIndex += 1 } - // Preferences item at the bottom of the source menu, separated - deviceMenu.addItem(NSMenuItem.separator()) - let prefsItem = NSMenuItem( - title: "Preferences...", - action: #selector(openPreferences), - keyEquivalent: "," - ) - prefsItem.target = self - deviceMenu.addItem(prefsItem) - selectSourceMenu.submenu = deviceMenu } @@ -553,7 +543,7 @@ class QCAppDelegate: NSObject, NSApplicationDelegate, QCUsbWatcherDelegate { // MARK: Preferences - @objc func openPreferences() { + @IBAction func openPreferences(_ sender: Any?) { if preferencesWindowController == nil { preferencesWindowController = QCPreferencesWindowController() // Rebuild the device menu immediately whenever the checkbox is toggled @@ -638,13 +628,8 @@ class QCAppDelegate: NSObject, NSApplicationDelegate, QCUsbWatcherDelegate { } private func descriptionFor(resolution: AVCaptureDevice.Format) -> String { - var result = "" - if #available(OSX 10.15, *) { - result = "\(resolution.formatDescription.dimensions.width)x\(resolution.formatDescription.dimensions.height)" - } else { - result = "\(resolution)" // this looks fairly bad on mac os pre 10.15 - } - return result + let dims = CMVideoFormatDescriptionGetDimensions(resolution.formatDescription) + return "\(dims.width)x\(dims.height)" }