From a4d0f7ba3aaa73e0b59c900770be2555e242893b Mon Sep 17 00:00:00 2001 From: juniq Date: Sun, 10 May 2026 18:35:31 +0900 Subject: [PATCH 1/4] Add Apple Silicon build support and volume label - Remove EXCLUDED_ARCHS = arm64; bump deployment target 10.10 -> 13.0 - Drop compile-time link to OSD.framework; load via dlopen at runtime - Add volume percent label to menu popup slider - Add temporary NSLog for debugging --- MultiSoundChanger.xcodeproj/project.pbxproj | 16 ++++----- .../Other/MultiSoundChanger-Bridging-Header.h | 1 - .../Classes/ApplicationController.swift | 2 ++ .../Sources/Classes/MediaManager.swift | 36 +++++++++++-------- .../Sources/Stories/Volume/Volume.storyboard | 16 +++++++-- .../Stories/Volume/VolumeViewController.swift | 6 +++- 6 files changed, 48 insertions(+), 29 deletions(-) diff --git a/MultiSoundChanger.xcodeproj/project.pbxproj b/MultiSoundChanger.xcodeproj/project.pbxproj index 75c9711..412ae8f 100644 --- a/MultiSoundChanger.xcodeproj/project.pbxproj +++ b/MultiSoundChanger.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 4743EFAB1E91493B0032F5AA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4743EFAA1E91493B0032F5AA /* AppDelegate.swift */; }; - 6985C6FE251951F8003C2FDB /* OSD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6985C6FD251951F8003C2FDB /* OSD.framework */; }; E4FFDC0757FD125F92CC0F62 /* Pods_MultiSoundChanger.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C34C05E9BD81D579A0C4957 /* Pods_MultiSoundChanger.framework */; }; F312C54E25B3741C00205846 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F373D8C02561D24600642274 /* Main.storyboard */; }; F312C55025B3742200205846 /* Volume.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F373D8BC2561D22000642274 /* Volume.storyboard */; }; @@ -33,7 +32,6 @@ 4743EFA71E91493B0032F5AA /* MultiSoundChanger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MultiSoundChanger.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4743EFAA1E91493B0032F5AA /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 4C34C05E9BD81D579A0C4957 /* Pods_MultiSoundChanger.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MultiSoundChanger.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 6985C6FD251951F8003C2FDB /* OSD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OSD.framework; sourceTree = ""; }; 6FD0ED04AFD1CC1242C9B3B3 /* Pods-MultiSoundChanger.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultiSoundChanger.debug.xcconfig"; path = "Target Support Files/Pods-MultiSoundChanger/Pods-MultiSoundChanger.debug.xcconfig"; sourceTree = ""; }; D184B2CD842B856AFFE7DF7E /* Pods-MultiSoundChanger.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MultiSoundChanger.release.xcconfig"; path = "Target Support Files/Pods-MultiSoundChanger/Pods-MultiSoundChanger.release.xcconfig"; sourceTree = ""; }; F3433FCA25B36E16009AAE86 /* Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; }; @@ -62,7 +60,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6985C6FE251951F8003C2FDB /* OSD.framework in Frameworks */, E4FFDC0757FD125F92CC0F62 /* Pods_MultiSoundChanger.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -127,7 +124,6 @@ 83889335DD9089B748A33010 /* Frameworks */ = { isa = PBXGroup; children = ( - 6985C6FD251951F8003C2FDB /* OSD.framework */, 4C34C05E9BD81D579A0C4957 /* Pods_MultiSoundChanger.framework */, ); name = Frameworks; @@ -442,7 +438,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -497,7 +493,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; @@ -514,14 +510,14 @@ CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; - EXCLUDED_ARCHS = arm64; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", ); INFOPLIST_FILE = MultiSoundChanger/Other/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.rlxone.multisoundchanger; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -540,14 +536,14 @@ CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; DEVELOPMENT_TEAM = ""; - EXCLUDED_ARCHS = arm64; + EXCLUDED_ARCHS = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", ); INFOPLIST_FILE = MultiSoundChanger/Other/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = com.rlxone.multisoundchanger; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/MultiSoundChanger/Other/MultiSoundChanger-Bridging-Header.h b/MultiSoundChanger/Other/MultiSoundChanger-Bridging-Header.h index 6b74749..a31bfe4 100644 --- a/MultiSoundChanger/Other/MultiSoundChanger-Bridging-Header.h +++ b/MultiSoundChanger/Other/MultiSoundChanger-Bridging-Header.h @@ -8,6 +8,5 @@ #define MultiSoundChanger_Bridging_Header_h #import -#import #endif /* MultiSoundChanger_Bridging_Header_h */ diff --git a/MultiSoundChanger/Sources/Classes/ApplicationController.swift b/MultiSoundChanger/Sources/Classes/ApplicationController.swift index 6498952..be9666c 100644 --- a/MultiSoundChanger/Sources/Classes/ApplicationController.swift +++ b/MultiSoundChanger/Sources/Classes/ApplicationController.swift @@ -32,7 +32,9 @@ final class ApplicationControllerImp: ApplicationController { extension ApplicationControllerImp: MediaManagerDelegate { func onMediaKeyTap(mediaKey: MediaKey) { + NSLog("MSC: onMediaKeyTap called, key=\(mediaKey)") guard let selectedDeviceVolume = audioManager.getSelectedDeviceVolume() else { + NSLog("MSC: getSelectedDeviceVolume returned nil") return } diff --git a/MultiSoundChanger/Sources/Classes/MediaManager.swift b/MultiSoundChanger/Sources/Classes/MediaManager.swift index b3a4799..3c62c5f 100644 --- a/MultiSoundChanger/Sources/Classes/MediaManager.swift +++ b/MultiSoundChanger/Sources/Classes/MediaManager.swift @@ -43,31 +43,37 @@ final class MediaManagerImpl: MediaManager { } func showOSD(volume: Float, chicletsCount: Int = 16) { - guard let manager = OSDManager.sharedManager() as? OSDManager else { + NSLog("MSC: showOSD called, volume=\(volume)") + guard let osdBundle = Bundle(path: "/System/Library/PrivateFrameworks/OSD.framework"), + osdBundle.load(), + let osdClass = NSClassFromString("OSDManager") as? NSObject.Type else { return } - + + guard let manager = osdClass.perform(NSSelectorFromString("sharedManager"))?.takeUnretainedValue() as? NSObject else { + return + } + let mouseloc: NSPoint = NSEvent.mouseLocation var displayForPoint: CGDirectDisplayID = 0 var count: UInt32 = 0 - + if CGGetDisplaysWithPoint(mouseloc, 1, &displayForPoint, &count) != .success { Logger.warning(Constants.InnerMessages.getDisplayError) displayForPoint = CGMainDisplayID() } - - let image = (volume == 0) ? OSDGraphicSpeakerMuted.rawValue : OSDGraphicSpeaker.rawValue + + let image: Int64 = (volume == 0) ? 4 : 3 // OSDGraphicSpeakerMuted / OSDGraphicSpeaker let volumeStep: Float = 100 / Float(chicletsCount) - - manager.showImage( - Int64(image), - onDisplayID: displayForPoint, - priority: 0x1F4, - msecUntilFade: 1_000, - filledChiclets: UInt32(volume / volumeStep), - totalChiclets: UInt32(100.0 / volumeStep), - locked: false - ) + + let sel = NSSelectorFromString("showImage:onDisplayID:priority:msecUntilFade:filledChiclets:totalChiclets:locked:") + NSLog("MSC OSD: responds=\(manager.responds(to: sel)), volume=\(volume), filled=\(UInt32(volume / volumeStep)), total=\(UInt32(100.0 / volumeStep))") + if manager.responds(to: sel) { + let imp = manager.method(for: sel) + typealias ShowImageFunc = @convention(c) (NSObject, Selector, Int64, UInt32, UInt32, UInt32, UInt32, UInt32, Bool) -> Void + let f = unsafeBitCast(imp, to: ShowImageFunc.self) + f(manager, sel, image, displayForPoint, 0x1F4, 1_000, UInt32(volume / volumeStep), UInt32(100.0 / volumeStep), false) + } } // MARK: Private diff --git a/MultiSoundChanger/Sources/Stories/Volume/Volume.storyboard b/MultiSoundChanger/Sources/Stories/Volume/Volume.storyboard index be6283b..6537eeb 100644 --- a/MultiSoundChanger/Sources/Stories/Volume/Volume.storyboard +++ b/MultiSoundChanger/Sources/Stories/Volume/Volume.storyboard @@ -15,22 +15,34 @@ - + + + + + + + + + - + + + + + diff --git a/MultiSoundChanger/Sources/Stories/Volume/VolumeViewController.swift b/MultiSoundChanger/Sources/Stories/Volume/VolumeViewController.swift index e1861c7..fcf980d 100644 --- a/MultiSoundChanger/Sources/Stories/Volume/VolumeViewController.swift +++ b/MultiSoundChanger/Sources/Stories/Volume/VolumeViewController.swift @@ -12,6 +12,7 @@ import MediaKeyTap final class VolumeViewController: NSViewController { @IBOutlet weak var volumeSlider: NSSlider! + @IBOutlet weak var volumeLabel: NSTextField! private var muted: Bool = false weak var statusBarController: StatusBarController? @@ -22,11 +23,14 @@ final class VolumeViewController: NSViewController { } func updateSliderVolume(volume: Float) { - volumeSlider.floatValue = volume.clamped(to: 0...100) + let clamped = volume.clamped(to: 0...100) + volumeSlider.floatValue = clamped + volumeLabel?.stringValue = "\(Int(clamped))%" } @IBAction func volumeSliderAction(_ sender: Any) { changeDeviceVolume(value: volumeSlider.floatValue / 100) statusBarController?.changeStatusItemImage(value: volumeSlider.floatValue) + volumeLabel?.stringValue = "\(Int(volumeSlider.floatValue))%" } } From 5a6c3256f8c840f0f5853b29801e480897f3b868 Mon Sep 17 00:00:00 2001 From: juniq Date: Sun, 10 May 2026 19:34:09 +0900 Subject: [PATCH 2/4] Add custom volume HUD overlay - Replace broken OSD private API call with self-drawn NSWindow overlay - Show speaker icon, chiclet bar, and volume percentage on key press - Auto-fade after 1.2s, positioned at bottom-center of active screen - Remove debug NSLog statements --- MultiSoundChanger.xcodeproj/project.pbxproj | 4 + .../Classes/ApplicationController.swift | 2 - .../Sources/Classes/MediaManager.swift | 32 +--- .../Sources/Classes/VolumeHUD.swift | 170 ++++++++++++++++++ 4 files changed, 175 insertions(+), 33 deletions(-) create mode 100644 MultiSoundChanger/Sources/Classes/VolumeHUD.swift diff --git a/MultiSoundChanger.xcodeproj/project.pbxproj b/MultiSoundChanger.xcodeproj/project.pbxproj index 412ae8f..8fa21bf 100644 --- a/MultiSoundChanger.xcodeproj/project.pbxproj +++ b/MultiSoundChanger.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ F373D8B32561D1A600642274 /* StatusBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8B02561D1A600642274 /* StatusBarController.swift */; }; F373D8B42561D1A600642274 /* AudioManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8B12561D1A600642274 /* AudioManager.swift */; }; F373D8B52561D1A600642274 /* MediaManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8B22561D1A600642274 /* MediaManager.swift */; }; + AAA1B0C0D0E0F0A0B0C0D001 /* VolumeHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA1B0C0D0E0F0A0B0C0D002 /* VolumeHUD.swift */; }; F373D8BB2561D21900642274 /* Stories.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8BA2561D21900642274 /* Stories.swift */; }; F373D8BF2561D22000642274 /* VolumeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8BD2561D22000642274 /* VolumeViewController.swift */; }; F373D8C62561D2A600642274 /* Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = F373D8C52561D2A600642274 /* Audio.swift */; }; @@ -38,6 +39,7 @@ F373D8B02561D1A600642274 /* StatusBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusBarController.swift; sourceTree = ""; }; F373D8B12561D1A600642274 /* AudioManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AudioManager.swift; sourceTree = ""; }; F373D8B22561D1A600642274 /* MediaManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaManager.swift; sourceTree = ""; }; + AAA1B0C0D0E0F0A0B0C0D002 /* VolumeHUD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VolumeHUD.swift; sourceTree = ""; }; F373D8BA2561D21900642274 /* Stories.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stories.swift; sourceTree = ""; }; F373D8BC2561D22000642274 /* Volume.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Volume.storyboard; sourceTree = ""; }; F373D8BD2561D22000642274 /* VolumeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VolumeViewController.swift; sourceTree = ""; }; @@ -157,6 +159,7 @@ F373D8B02561D1A600642274 /* StatusBarController.swift */, F373D8B12561D1A600642274 /* AudioManager.swift */, F373D8B22561D1A600642274 /* MediaManager.swift */, + AAA1B0C0D0E0F0A0B0C0D002 /* VolumeHUD.swift */, ); path = Classes; sourceTree = ""; @@ -363,6 +366,7 @@ F373D8C62561D2A600642274 /* Audio.swift in Sources */, F37C2ED1256AAA4C001C3D36 /* Strings.swift in Sources */, F373D8B52561D1A600642274 /* MediaManager.swift in Sources */, + AAA1B0C0D0E0F0A0B0C0D001 /* VolumeHUD.swift in Sources */, F373D8B42561D1A600642274 /* AudioManager.swift in Sources */, F373D8B32561D1A600642274 /* StatusBarController.swift in Sources */, 4743EFAB1E91493B0032F5AA /* AppDelegate.swift in Sources */, diff --git a/MultiSoundChanger/Sources/Classes/ApplicationController.swift b/MultiSoundChanger/Sources/Classes/ApplicationController.swift index be9666c..6498952 100644 --- a/MultiSoundChanger/Sources/Classes/ApplicationController.swift +++ b/MultiSoundChanger/Sources/Classes/ApplicationController.swift @@ -32,9 +32,7 @@ final class ApplicationControllerImp: ApplicationController { extension ApplicationControllerImp: MediaManagerDelegate { func onMediaKeyTap(mediaKey: MediaKey) { - NSLog("MSC: onMediaKeyTap called, key=\(mediaKey)") guard let selectedDeviceVolume = audioManager.getSelectedDeviceVolume() else { - NSLog("MSC: getSelectedDeviceVolume returned nil") return } diff --git a/MultiSoundChanger/Sources/Classes/MediaManager.swift b/MultiSoundChanger/Sources/Classes/MediaManager.swift index 3c62c5f..cee9c30 100644 --- a/MultiSoundChanger/Sources/Classes/MediaManager.swift +++ b/MultiSoundChanger/Sources/Classes/MediaManager.swift @@ -43,37 +43,7 @@ final class MediaManagerImpl: MediaManager { } func showOSD(volume: Float, chicletsCount: Int = 16) { - NSLog("MSC: showOSD called, volume=\(volume)") - guard let osdBundle = Bundle(path: "/System/Library/PrivateFrameworks/OSD.framework"), - osdBundle.load(), - let osdClass = NSClassFromString("OSDManager") as? NSObject.Type else { - return - } - - guard let manager = osdClass.perform(NSSelectorFromString("sharedManager"))?.takeUnretainedValue() as? NSObject else { - return - } - - let mouseloc: NSPoint = NSEvent.mouseLocation - var displayForPoint: CGDirectDisplayID = 0 - var count: UInt32 = 0 - - if CGGetDisplaysWithPoint(mouseloc, 1, &displayForPoint, &count) != .success { - Logger.warning(Constants.InnerMessages.getDisplayError) - displayForPoint = CGMainDisplayID() - } - - let image: Int64 = (volume == 0) ? 4 : 3 // OSDGraphicSpeakerMuted / OSDGraphicSpeaker - let volumeStep: Float = 100 / Float(chicletsCount) - - let sel = NSSelectorFromString("showImage:onDisplayID:priority:msecUntilFade:filledChiclets:totalChiclets:locked:") - NSLog("MSC OSD: responds=\(manager.responds(to: sel)), volume=\(volume), filled=\(UInt32(volume / volumeStep)), total=\(UInt32(100.0 / volumeStep))") - if manager.responds(to: sel) { - let imp = manager.method(for: sel) - typealias ShowImageFunc = @convention(c) (NSObject, Selector, Int64, UInt32, UInt32, UInt32, UInt32, UInt32, Bool) -> Void - let f = unsafeBitCast(imp, to: ShowImageFunc.self) - f(manager, sel, image, displayForPoint, 0x1F4, 1_000, UInt32(volume / volumeStep), UInt32(100.0 / volumeStep), false) - } + VolumeHUD.shared.show(volume: volume, muted: volume == 0) } // MARK: Private diff --git a/MultiSoundChanger/Sources/Classes/VolumeHUD.swift b/MultiSoundChanger/Sources/Classes/VolumeHUD.swift new file mode 100644 index 0000000..f1ea5ff --- /dev/null +++ b/MultiSoundChanger/Sources/Classes/VolumeHUD.swift @@ -0,0 +1,170 @@ +// +// VolumeHUD.swift +// MultiSoundChanger +// + +import Cocoa + +final class VolumeHUD { + static let shared = VolumeHUD() + + private var window: NSWindow? + private var hideWorkItem: DispatchWorkItem? + private let chicletCount = 16 + + private init() {} + + func show(volume: Float, muted: Bool) { + DispatchQueue.main.async { [weak self] in + self?.present(volume: volume, muted: muted) + } + } + + private func present(volume: Float, muted: Bool) { + let win = window ?? makeWindow() + window = win + + let view = win.contentView as? HUDView + view?.update(volume: volume, muted: muted, chicletCount: chicletCount) + + positionWindow(win) + win.orderFrontRegardless() + + hideWorkItem?.cancel() + let item = DispatchWorkItem { [weak self] in + self?.fadeOut() + } + hideWorkItem = item + DispatchQueue.main.asyncAfter(deadline: .now() + 1.2, execute: item) + + win.alphaValue = 1.0 + } + + private func fadeOut() { + guard let win = window else { return } + NSAnimationContext.runAnimationGroup({ ctx in + ctx.duration = 0.3 + win.animator().alphaValue = 0.0 + }, completionHandler: { [weak self] in + self?.window?.orderOut(nil) + }) + } + + private func makeWindow() -> NSWindow { + let size = NSSize(width: 220, height: 220) + let w = NSWindow( + contentRect: NSRect(origin: .zero, size: size), + styleMask: .borderless, + backing: .buffered, + defer: false + ) + w.isOpaque = false + w.backgroundColor = .clear + w.level = .statusBar + w.ignoresMouseEvents = true + w.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenAuxiliary] + w.hasShadow = false + w.contentView = HUDView(frame: NSRect(origin: .zero, size: size)) + return w + } + + private func positionWindow(_ win: NSWindow) { + let mouseLoc = NSEvent.mouseLocation + let screen = NSScreen.screens.first { NSMouseInRect(mouseLoc, $0.frame, false) } ?? NSScreen.main + guard let screen = screen else { return } + let frame = screen.frame + let size = win.frame.size + let origin = NSPoint( + x: frame.midX - size.width / 2, + y: frame.minY + 140 + ) + win.setFrameOrigin(origin) + } +} + +// MARK: - HUD View + +final class HUDView: NSView { + private var volume: Float = 0 + private var muted: Bool = false + private var chicletCount: Int = 16 + + override init(frame: NSRect) { + super.init(frame: frame) + wantsLayer = true + } + + required init?(coder: NSCoder) { super.init(coder: coder) } + + func update(volume: Float, muted: Bool, chicletCount: Int) { + self.volume = volume + self.muted = muted + self.chicletCount = chicletCount + needsDisplay = true + } + + override var isFlipped: Bool { false } + + override func draw(_ dirtyRect: NSRect) { + let ctx = NSGraphicsContext.current?.cgContext + + // Rounded background panel + let panelRect = bounds.insetBy(dx: 0, dy: 0) + let panelPath = NSBezierPath(roundedRect: panelRect, xRadius: 22, yRadius: 22) + NSColor(white: 0.10, alpha: 0.86).setFill() + panelPath.fill() + + // Speaker icon + let iconRect = NSRect(x: bounds.midX - 32, y: bounds.maxY - 90, width: 64, height: 64) + let speakerName: String = muted ? "speaker.slash.fill" : (volume < 1 ? "speaker.fill" : (volume < 50 ? "speaker.wave.1.fill" : (volume < 80 ? "speaker.wave.2.fill" : "speaker.wave.3.fill"))) + if let img = NSImage(systemSymbolName: speakerName, accessibilityDescription: nil) { + let conf = NSImage.SymbolConfiguration(pointSize: 44, weight: .regular) + let configured = img.withSymbolConfiguration(conf) ?? img + configured.isTemplate = true + NSColor.white.set() + configured.draw(in: iconRect, from: .zero, operation: .sourceOver, fraction: 1.0, + respectFlipped: true, hints: [.interpolation: NSImageInterpolation.high.rawValue]) + } + + // Chiclets + let chicletAreaWidth: CGFloat = 180 + let chicletAreaHeight: CGFloat = 14 + let originX = bounds.midX - chicletAreaWidth / 2 + let originY: CGFloat = 50 + let gap: CGFloat = 2 + let totalGap = gap * CGFloat(chicletCount - 1) + let chicletWidth = (chicletAreaWidth - totalGap) / CGFloat(chicletCount) + let step = 100.0 / Float(chicletCount) + let filled = Int((volume / step).rounded()) + + for i in 0.. Date: Sun, 10 May 2026 19:38:44 +0900 Subject: [PATCH 3/4] Remove unused ctx variable in HUDView.draw --- MultiSoundChanger/Sources/Classes/VolumeHUD.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/MultiSoundChanger/Sources/Classes/VolumeHUD.swift b/MultiSoundChanger/Sources/Classes/VolumeHUD.swift index f1ea5ff..64e3a5d 100644 --- a/MultiSoundChanger/Sources/Classes/VolumeHUD.swift +++ b/MultiSoundChanger/Sources/Classes/VolumeHUD.swift @@ -106,8 +106,6 @@ final class HUDView: NSView { override var isFlipped: Bool { false } override func draw(_ dirtyRect: NSRect) { - let ctx = NSGraphicsContext.current?.cgContext - // Rounded background panel let panelRect = bounds.insetBy(dx: 0, dy: 0) let panelPath = NSBezierPath(roundedRect: panelRect, xRadius: 22, yRadius: 22) @@ -164,7 +162,5 @@ final class HUDView: NSView { height: textSize.height ) str.draw(in: textRect) - - _ = ctx } } From 9ee97de6c54a7a63020e4063e61ed60258b9d161 Mon Sep 17 00:00:00 2001 From: juniq Date: Sun, 10 May 2026 22:40:17 +0900 Subject: [PATCH 4/4] Fix HUD speaker icon rendering as black Tint the SF Symbol via an offscreen image with sourceAtop fill since NSColor.set() is ignored when drawing template images directly. Co-Authored-By: Claude Opus 4.7 (1M context) --- MultiSoundChanger/Sources/Classes/VolumeHUD.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/MultiSoundChanger/Sources/Classes/VolumeHUD.swift b/MultiSoundChanger/Sources/Classes/VolumeHUD.swift index 64e3a5d..8bedb26 100644 --- a/MultiSoundChanger/Sources/Classes/VolumeHUD.swift +++ b/MultiSoundChanger/Sources/Classes/VolumeHUD.swift @@ -119,9 +119,14 @@ final class HUDView: NSView { let conf = NSImage.SymbolConfiguration(pointSize: 44, weight: .regular) let configured = img.withSymbolConfiguration(conf) ?? img configured.isTemplate = true - NSColor.white.set() - configured.draw(in: iconRect, from: .zero, operation: .sourceOver, fraction: 1.0, - respectFlipped: true, hints: [.interpolation: NSImageInterpolation.high.rawValue]) + let tinted = NSImage(size: configured.size, flipped: false) { rect in + configured.draw(in: rect) + NSColor.white.set() + rect.fill(using: .sourceAtop) + return true + } + tinted.draw(in: iconRect, from: .zero, operation: .sourceOver, fraction: 1.0, + respectFlipped: true, hints: [.interpolation: NSImageInterpolation.high.rawValue]) } // Chiclets