From 26a1900d7e7a9e0b737c5a1f64ffde84e4809016 Mon Sep 17 00:00:00 2001 From: Karan Shah <42066323+karanshah229@users.noreply.github.com> Date: Sat, 18 Apr 2026 15:37:16 +0530 Subject: [PATCH 1/2] feat: Add numerical input and arrow key support for sliders - Converted menu bar percentage labels into editable text fields. - Implemented `NSTextFieldDelegate` to handle manual typing with bound clamping (0-100). - Handled invalid string parsing with graceful fallbacks to the current slider state. - Added a native rounded bezel effect when the field is actively focused. - Overrode `control(_:textView:doCommandBy:)` to capture Up/Down arrow presses for live value manipulation. - Added a `Shift` modifier for arrow keys to toggle between 1% and 10% step increments. - Ensured all manual inputs immediately sync physical hardware brightness, volume, and contrast via DDC. --- MonitorControl/Support/SliderHandler.swift | 49 +++++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift index 1d7d20ec..cec290bc 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -3,7 +3,7 @@ import Cocoa import os.log -class SliderHandler { +class SliderHandler: NSObject, NSTextFieldDelegate { var slider: MCSlider? var view: NSView? var percentageBox: NSTextField? @@ -212,6 +212,7 @@ class SliderHandler { public init(display: Display?, command: Command, title: String = "", position _: Int = 0) { self.command = command self.title = title + super.init() let slider = SliderHandler.MCSlider(value: 0, minValue: 0, maxValue: 1, target: self, action: #selector(SliderHandler.valueChanged)) let showPercent = prefs.bool(forKey: PrefKey.enableSliderPercent.rawValue) slider.isEnabled = true @@ -277,11 +278,12 @@ class SliderHandler { func setupPercentageBox(_ percentageBox: NSTextField) { percentageBox.font = NSFont.systemFont(ofSize: 12) - percentageBox.isEditable = false + percentageBox.isEditable = true percentageBox.isBordered = false percentageBox.drawsBackground = false percentageBox.alignment = .right percentageBox.alphaValue = 0.7 + percentageBox.delegate = self } func valueChangedOtherDisplay(otherDisplay: OtherDisplay, value: Float) { @@ -383,4 +385,47 @@ class SliderHandler { } } } + + // MARK: - NSTextFieldDelegate + + func controlTextDidBeginEditing(_: Notification) { + self.percentageBox?.isBordered = true + self.percentageBox?.alphaValue = 1.0 + } + + func controlTextDidEndEditing(_ obj: Notification) { + self.percentageBox?.isBordered = false + self.percentageBox?.alphaValue = 0.7 + guard let textField = obj.object as? NSTextField else { return } + + let stringValue = textField.stringValue.replacingOccurrences(of: "%", with: "").trimmingCharacters(in: .whitespaces) + if let value = Float(stringValue), let slider = self.slider { + slider.floatValue = max(0, min(100, value)) / 100 + self.valueChanged(slider: slider) + } else if let slider = self.slider { + self.setValue(slider.floatValue) + } + } + + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + guard commandSelector == #selector(NSResponder.moveUp(_:)) || commandSelector == #selector(NSResponder.moveDown(_:)) else { + return false + } + + let stringValue = textView.string.replacingOccurrences(of: "%", with: "").trimmingCharacters(in: .whitespaces) + var currentValue = Float(stringValue) ?? (self.slider?.floatValue ?? 0) * 100 + let step: Float = NSEvent.modifierFlags.contains(.shift) ? 10 : 1 + + currentValue += commandSelector == #selector(NSResponder.moveUp(_:)) ? step : -step + + let normalizedValue = max(0, min(100, currentValue)) + textView.string = "\(Int(normalizedValue))%" + + if let slider = self.slider { + slider.floatValue = normalizedValue / 100 + self.valueChanged(slider: slider) + } + + return true + } } From d6459158aab092e5d31da68b019e9447ec321201 Mon Sep 17 00:00:00 2001 From: Karan Shah <42066323+karanshah229@users.noreply.github.com> Date: Mon, 20 Apr 2026 11:53:37 +0530 Subject: [PATCH 2/2] fix: Review comments I --- MonitorControl/Support/SliderHandler.swift | 70 ++++++++++++++-------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/MonitorControl/Support/SliderHandler.swift b/MonitorControl/Support/SliderHandler.swift index 92a82b8d..f28cca6d 100644 --- a/MonitorControl/Support/SliderHandler.swift +++ b/MonitorControl/Support/SliderHandler.swift @@ -209,6 +209,13 @@ class SliderHandler: NSObject, NSTextFieldDelegate { } } + class PercentageTextField: NSTextField { + override func mouseDown(with event: NSEvent) { + self.isEditable = true + self.window?.makeFirstResponder(self) + } + } + init(display: Display?, command: Command, title: String = "", position _: Int = 0) { self.command = command self.title = title @@ -239,7 +246,7 @@ class SliderHandler: NSObject, NSTextFieldDelegate { view.addSubview(icon) self.icon = icon if showPercent { - let percentageBox = NSTextField(frame: NSRect(x: 15 + slider.frame.size.width - 2, y: 17, width: 40, height: 12)) + let percentageBox = PercentageTextField(frame: NSRect(x: 15 + slider.frame.size.width - 2, y: 17, width: 40, height: 12)) self.setupPercentageBox(percentageBox) self.percentageBox = percentageBox view.addSubview(percentageBox) @@ -251,7 +258,7 @@ class SliderHandler: NSObject, NSTextFieldDelegate { let view = NSView(frame: NSRect(x: 0, y: 0, width: slider.frame.width + 30 + (showPercent ? 38 : 0), height: slider.frame.height + 10)) view.addSubview(slider) if showPercent { - let percentageBox = NSTextField(frame: NSRect(x: 15 + slider.frame.size.width - 2, y: 18, width: 40, height: 12)) + let percentageBox = PercentageTextField(frame: NSRect(x: 15 + slider.frame.size.width - 2, y: 18, width: 40, height: 12)) self.setupPercentageBox(percentageBox) self.percentageBox = percentageBox view.addSubview(percentageBox) @@ -278,7 +285,7 @@ class SliderHandler: NSObject, NSTextFieldDelegate { func setupPercentageBox(_ percentageBox: NSTextField) { percentageBox.font = NSFont.systemFont(ofSize: 12) - percentageBox.isEditable = true + percentageBox.isEditable = false percentageBox.isBordered = false percentageBox.drawsBackground = false percentageBox.alignment = .right @@ -323,7 +330,9 @@ class SliderHandler: NSObject, NSTextFieldDelegate { } } if self.percentageBox == self.percentageBox { - self.percentageBox?.stringValue = "" + String(Int(value * 100)) + "%" + if self.percentageBox?.currentEditor() == nil { + self.percentageBox?.stringValue = "" + String(Int(value * 100)) + "%" + } } for display in self.displays { slider.setHighlightItem(display.identifier, value: value) @@ -381,7 +390,9 @@ class SliderHandler: NSObject, NSTextFieldDelegate { slider.setDisplayHighlightItems(false) } if self.percentageBox == self.percentageBox { - self.percentageBox?.stringValue = "\(String(format: "%.0f%%", Double(value) * 100))" + if self.percentageBox?.currentEditor() == nil { + self.percentageBox?.stringValue = "\(String(format: "%.0f%%", Double(value) * 100))" + } } } } @@ -389,43 +400,54 @@ class SliderHandler: NSObject, NSTextFieldDelegate { // MARK: - NSTextFieldDelegate func controlTextDidBeginEditing(_: Notification) { - self.percentageBox?.isBordered = true + self.percentageBox?.isBezeled = true + self.percentageBox?.bezelStyle = .roundedBezel + self.percentageBox?.drawsBackground = true self.percentageBox?.alphaValue = 1.0 } func controlTextDidEndEditing(_ obj: Notification) { - self.percentageBox?.isBordered = false + self.percentageBox?.isEditable = false + self.percentageBox?.isBezeled = false + self.percentageBox?.drawsBackground = false self.percentageBox?.alphaValue = 0.7 guard let textField = obj.object as? NSTextField else { return } let stringValue = textField.stringValue.replacingOccurrences(of: "%", with: "").trimmingCharacters(in: .whitespaces) if let value = Float(stringValue), let slider = self.slider { - slider.floatValue = max(0, min(100, value)) / 100 + let normalizedValue = max(0, min(100, value)) + slider.floatValue = normalizedValue / 100 self.valueChanged(slider: slider) + textField.stringValue = "\(Int(normalizedValue))%" } else if let slider = self.slider { self.setValue(slider.floatValue) } } func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - guard commandSelector == #selector(NSResponder.moveUp(_:)) || commandSelector == #selector(NSResponder.moveDown(_:)) else { - return false - } - - let stringValue = textView.string.replacingOccurrences(of: "%", with: "").trimmingCharacters(in: .whitespaces) - var currentValue = Float(stringValue) ?? (self.slider?.floatValue ?? 0) * 100 - let step: Float = NSEvent.modifierFlags.contains(.shift) ? 10 : 1 - - currentValue += commandSelector == #selector(NSResponder.moveUp(_:)) ? step : -step + let isUp = commandSelector == #selector(NSResponder.moveUp(_:)) || commandSelector == #selector(NSResponder.moveUpAndModifySelection(_:)) + let isDown = commandSelector == #selector(NSResponder.moveDown(_:)) || commandSelector == #selector(NSResponder.moveDownAndModifySelection(_:)) - let normalizedValue = max(0, min(100, currentValue)) - textView.string = "\(Int(normalizedValue))%" - - if let slider = self.slider { - slider.floatValue = normalizedValue / 100 - self.valueChanged(slider: slider) + if isUp || isDown { + let stringValue = textView.string.replacingOccurrences(of: "%", with: "").trimmingCharacters(in: .whitespaces) + var currentValue = Float(stringValue) ?? (self.slider?.floatValue ?? 0) * 100 + let step: Float = NSEvent.modifierFlags.contains(.shift) ? 10 : 1 + + currentValue += isUp ? step : -step + + let normalizedValue = max(0, min(100, currentValue)) + textView.string = "\(Int(normalizedValue))%" + + if let slider = self.slider { + slider.floatValue = normalizedValue / 100 + self.valueChanged(slider: slider) + } + return true + } else if commandSelector == #selector(NSResponder.cancelOperation(_:)) || commandSelector == #selector(NSResponder.insertNewline(_:)) { + self.percentageBox?.window?.makeFirstResponder(nil) + return true } - return true + return false } }