// // KMSlider.swift // PDF Reader Pro // // Created by kdanmobile on 2023/10/31. // import Cocoa @objc(SJTSliderDelegate) protocol SJTSliderDelegate: AnyObject { func tipForValue(inSlider slider: KMSlider, value: Double) -> String func valueDidSelect(inSlider slider: KMSlider) } let SJTSliderTipPopoverMinWidthX = 60.0; let SJTSliderTipPopoverMinWidthY = 40.0; let SJTSliderTipPopoverMinHeightX = 38.0; let SJTSliderTipPopoverMinHeightY = 24.0; let SJTSliderMinHeight = 21.0; let SJTSliderMinHeightWithTickMark = 26.0; let SJTSliderMinWidth = 100.0; let SJTSliderTickMarkMinWidth = 5.0; let SJTSliderPositioningRectkey = "positioningRect" class KMSlider: NSSlider, NSPopoverDelegate{ var tipEnabled: Bool = false var tipAutoAlignment: Bool = false var tipAlignment: NSTextAlignment = .left var tipAppearance: NSAppearance? var delegate: SJTSliderDelegate? private var tipPopover: NSPopover? required init?(coder: NSCoder) { super.init(coder: coder) self.initView() } func initView() { self.isContinuous = true self.tipEnabled = true self.tipAutoAlignment = true self.tipAlignment = .center self.tipAppearance = NSAppearance(named: NSAppearance.Name.vibrantLight) let trackingArea = NSTrackingArea(rect: NSZeroRect, options: [.inVisibleRect, .mouseEnteredAndExited, .mouseMoved, .activeInActiveApp], owner: self, userInfo: nil) self.addTrackingArea(trackingArea) let tipView = NSTextField() tipView.isBordered = false tipView.backgroundColor = NSColor.clear tipView.isEditable = false tipView.isSelectable = false tipView.alignment = .center let contentView = NSView() contentView.addSubview(tipView) self.tipPopover = NSPopover() self.tipPopover?.contentViewController = NSViewController() self.tipPopover?.contentViewController?.view = contentView self.tipPopover?.delegate = self } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if object as? NSObject == self.tipPopover && keyPath == SJTSliderPositioningRectkey { self.adjustTipAlignment() } } override func sendAction(_ action: Selector?, to target: Any?) -> Bool { if tipEnabled { showTipPopover(animated: false) } let event = NSApplication.shared.currentEvent if event?.type == NSEvent.EventType.leftMouseUp { delegate?.valueDidSelect(inSlider: self) } return super.sendAction(action, to: target) } func adjustTipAlignment() { guard let tipContentView = self.tipPopover?.contentViewController?.view else { return } guard let tipView = tipContentView.subviews.first as? NSTextField else { return } var tipAlignment: NSTextAlignment = .center if self.tipAutoAlignment { var tipContentFrame = tipContentView.convert(tipContentView.bounds, to: nil) tipContentFrame = tipContentView.window?.convertToScreen(tipContentFrame) ?? NSRect.zero tipContentFrame = self.window?.convertFromScreen(tipContentFrame) ?? NSRect.zero if let tipTargetFrame = self.tipPopover?.positioningRect { if NSMaxX(tipContentFrame) < NSMinX(tipTargetFrame) { tipAlignment = .right } else if NSMinX(tipContentFrame) > NSMaxX(tipTargetFrame) { tipAlignment = .left } } } else { tipAlignment = self.tipAlignment } tipView.alignment = tipAlignment } func showTipPopover(animated: Bool) { let tip = self.tipForValue(self.doubleValue) if !tip.isEmpty { let contentView = self.tipPopover?.contentViewController?.view if let tipView = contentView?.subviews[0] as? NSTextField { let knobRect = (self.cell as? NSSliderCell)?.knobRect(flipped: self.isFlipped) var preferredEdge: NSRectEdge? var newcell: NSSliderCell = self.cell as! NSSliderCell if newcell.sliderType == .circular { let tickMartCenter = NSMakePoint(NSMidX(knobRect!), NSMidY(knobRect!)) let viewCenter = NSMakePoint(NSMidX(self.bounds), NSMidY(self.bounds)) let cutoffValue = sqrt(((viewCenter.x - tickMartCenter.x) * (viewCenter.x - tickMartCenter.x) + (viewCenter.y - tickMartCenter.y) * (viewCenter.y - tickMartCenter.y)) / 2) as CGFloat if viewCenter.x-tickMartCenter.x > cutoffValue { preferredEdge = NSRectEdge.minX } else if tickMartCenter.y-viewCenter.y > cutoffValue { preferredEdge = NSRectEdge.maxY } else if tickMartCenter.x-viewCenter.x > cutoffValue { preferredEdge = NSRectEdge.maxX } else { preferredEdge = NSRectEdge.minY } } else if self.isVertical { if self.tickMarkPosition == NSSlider.TickMarkPosition.leading { preferredEdge = NSRectEdge.minX } else { preferredEdge = NSRectEdge.maxX } } else { if self.tickMarkPosition == NSSlider.TickMarkPosition.below { preferredEdge = self.isFlipped ? NSRectEdge.maxY : NSRectEdge.minY } else { preferredEdge = self.isFlipped ? NSRectEdge.minY : NSRectEdge.maxY } } self.tipPopover?.appearance = self.tipAppearance tipView.stringValue = tip tipView.sizeToFit() var tipViewFrame = tipView.bounds var contentViewSize = tipView.bounds.size var minWidth, minHeight: Int if preferredEdge == NSRectEdge.minX || preferredEdge == NSRectEdge.maxX { minWidth = Int(SJTSliderTipPopoverMinWidthX) minHeight = Int(SJTSliderTipPopoverMinHeightX) } else { minWidth = Int(SJTSliderTipPopoverMinWidthY) minHeight = Int(SJTSliderTipPopoverMinHeightY) } if Int(tipViewFrame.size.width) < minWidth { tipViewFrame.size.width = CGFloat(minWidth) tipViewFrame.origin.x = (CGFloat(minWidth)-tipViewFrame.size.width)/2 contentViewSize.width = CGFloat(minWidth) } if Int(tipViewFrame.size.height) < minHeight { tipViewFrame.origin.y = (CGFloat(minHeight)-tipViewFrame.size.height)/2 contentViewSize.height = CGFloat(minHeight) } contentView?.setFrameSize(contentViewSize) tipView.frame = tipViewFrame self.tipPopover?.contentSize = contentViewSize self.tipPopover?.animates = animated self.tipPopover?.show(relativeTo: knobRect!, of: self, preferredEdge: preferredEdge!) } } } func tipForValue(_ value: Double) -> String { if let tip = self.delegate?.tipForValue(inSlider: self, value: self.doubleValue) { return tip } return String(format: "%0.f", self.doubleValue) } func closeTipPopover(animated: Bool) { if ((self.tipPopover?.isShown) != nil) { self.tipPopover?.animates = animated self.tipPopover?.close() } } } extension KMSlider { override func mouseDown(with event: NSEvent) { if self.tipEnabled { self.showTipPopover(animated: true) } super.mouseDown(with: event) } override func mouseEntered(with theEvent: NSEvent) { if self.tipEnabled { self.showTipPopover(animated: true) } } override func mouseExited(with theEvent: NSEvent) { self.closeTipPopover(animated: true) } }