KMSlider.swift 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. //
  2. // KMSlider.swift
  3. // PDF Reader Pro
  4. //
  5. // Created by kdanmobile on 2023/10/31.
  6. //
  7. import Cocoa
  8. @objc(SJTSliderDelegate)
  9. protocol SJTSliderDelegate: AnyObject {
  10. func tipForValue(inSlider slider: KMSlider, value: Double) -> String
  11. func valueDidSelect(inSlider slider: KMSlider)
  12. }
  13. let SJTSliderTipPopoverMinWidthX = 60.0;
  14. let SJTSliderTipPopoverMinWidthY = 40.0;
  15. let SJTSliderTipPopoverMinHeightX = 38.0;
  16. let SJTSliderTipPopoverMinHeightY = 24.0;
  17. let SJTSliderMinHeight = 21.0;
  18. let SJTSliderMinHeightWithTickMark = 26.0;
  19. let SJTSliderMinWidth = 100.0;
  20. let SJTSliderTickMarkMinWidth = 5.0;
  21. let SJTSliderPositioningRectkey = "positioningRect"
  22. class KMSlider: NSSlider, NSPopoverDelegate{
  23. var tipEnabled: Bool = false
  24. var tipAutoAlignment: Bool = false
  25. var tipAlignment: NSTextAlignment = .left
  26. var tipAppearance: NSAppearance?
  27. var delegate: SJTSliderDelegate?
  28. private var tipPopover: NSPopover?
  29. required init?(coder: NSCoder) {
  30. super.init(coder: coder)
  31. self.initView()
  32. }
  33. func initView() {
  34. self.isContinuous = true
  35. self.tipEnabled = true
  36. self.tipAutoAlignment = true
  37. self.tipAlignment = .center
  38. self.tipAppearance = NSAppearance(named: NSAppearance.Name.vibrantLight)
  39. let trackingArea = NSTrackingArea(rect: NSZeroRect, options: [.inVisibleRect, .mouseEnteredAndExited, .mouseMoved, .activeInActiveApp], owner: self, userInfo: nil)
  40. self.addTrackingArea(trackingArea)
  41. let tipView = NSTextField()
  42. tipView.isBordered = false
  43. tipView.backgroundColor = NSColor.clear
  44. tipView.isEditable = false
  45. tipView.isSelectable = false
  46. tipView.alignment = .center
  47. let contentView = NSView()
  48. contentView.addSubview(tipView)
  49. self.tipPopover = NSPopover()
  50. self.tipPopover?.contentViewController = NSViewController()
  51. self.tipPopover?.contentViewController?.view = contentView
  52. self.tipPopover?.delegate = self
  53. }
  54. override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  55. if object as? NSObject == self.tipPopover && keyPath == SJTSliderPositioningRectkey {
  56. self.adjustTipAlignment()
  57. }
  58. }
  59. override func sendAction(_ action: Selector?, to target: Any?) -> Bool {
  60. if tipEnabled {
  61. showTipPopover(animated: false)
  62. }
  63. let event = NSApplication.shared.currentEvent
  64. if event?.type == NSEvent.EventType.leftMouseUp {
  65. delegate?.valueDidSelect(inSlider: self)
  66. }
  67. return super.sendAction(action, to: target)
  68. }
  69. func adjustTipAlignment() {
  70. guard let tipContentView = self.tipPopover?.contentViewController?.view else {
  71. return
  72. }
  73. guard let tipView = tipContentView.subviews.first as? NSTextField else {
  74. return
  75. }
  76. var tipAlignment: NSTextAlignment = .center
  77. if self.tipAutoAlignment {
  78. var tipContentFrame = tipContentView.convert(tipContentView.bounds, to: nil)
  79. tipContentFrame = tipContentView.window?.convertToScreen(tipContentFrame) ?? NSRect.zero
  80. tipContentFrame = self.window?.convertFromScreen(tipContentFrame) ?? NSRect.zero
  81. if let tipTargetFrame = self.tipPopover?.positioningRect {
  82. if NSMaxX(tipContentFrame) < NSMinX(tipTargetFrame) {
  83. tipAlignment = .right
  84. } else if NSMinX(tipContentFrame) > NSMaxX(tipTargetFrame) {
  85. tipAlignment = .left
  86. }
  87. }
  88. } else {
  89. tipAlignment = self.tipAlignment
  90. }
  91. tipView.alignment = tipAlignment
  92. }
  93. func showTipPopover(animated: Bool) {
  94. let tip = self.tipForValue(self.doubleValue)
  95. if !tip.isEmpty {
  96. let contentView = self.tipPopover?.contentViewController?.view
  97. if let tipView = contentView?.subviews[0] as? NSTextField {
  98. let knobRect = (self.cell as? NSSliderCell)?.knobRect(flipped: self.isFlipped)
  99. var preferredEdge: NSRectEdge?
  100. var newcell: NSSliderCell = self.cell as! NSSliderCell
  101. if newcell.sliderType == .circular {
  102. let tickMartCenter = NSMakePoint(NSMidX(knobRect!), NSMidY(knobRect!))
  103. let viewCenter = NSMakePoint(NSMidX(self.bounds), NSMidY(self.bounds))
  104. let cutoffValue = sqrt(((viewCenter.x - tickMartCenter.x) * (viewCenter.x - tickMartCenter.x) + (viewCenter.y - tickMartCenter.y) * (viewCenter.y - tickMartCenter.y)) / 2) as CGFloat
  105. if viewCenter.x-tickMartCenter.x > cutoffValue {
  106. preferredEdge = NSRectEdge.minX
  107. } else if tickMartCenter.y-viewCenter.y > cutoffValue {
  108. preferredEdge = NSRectEdge.maxY
  109. } else if tickMartCenter.x-viewCenter.x > cutoffValue {
  110. preferredEdge = NSRectEdge.maxX
  111. } else {
  112. preferredEdge = NSRectEdge.minY
  113. }
  114. } else if self.isVertical {
  115. if self.tickMarkPosition == NSSlider.TickMarkPosition.leading {
  116. preferredEdge = NSRectEdge.minX
  117. } else {
  118. preferredEdge = NSRectEdge.maxX
  119. }
  120. } else {
  121. if self.tickMarkPosition == NSSlider.TickMarkPosition.below {
  122. preferredEdge = self.isFlipped ? NSRectEdge.maxY : NSRectEdge.minY
  123. } else {
  124. preferredEdge = self.isFlipped ? NSRectEdge.minY : NSRectEdge.maxY
  125. }
  126. }
  127. self.tipPopover?.appearance = self.tipAppearance
  128. tipView.stringValue = tip
  129. tipView.sizeToFit()
  130. var tipViewFrame = tipView.bounds
  131. var contentViewSize = tipView.bounds.size
  132. var minWidth, minHeight: Int
  133. if preferredEdge == NSRectEdge.minX || preferredEdge == NSRectEdge.maxX {
  134. minWidth = Int(SJTSliderTipPopoverMinWidthX)
  135. minHeight = Int(SJTSliderTipPopoverMinHeightX)
  136. } else {
  137. minWidth = Int(SJTSliderTipPopoverMinWidthY)
  138. minHeight = Int(SJTSliderTipPopoverMinHeightY)
  139. }
  140. if Int(tipViewFrame.size.width) < minWidth {
  141. tipViewFrame.size.width = CGFloat(minWidth)
  142. tipViewFrame.origin.x = (CGFloat(minWidth)-tipViewFrame.size.width)/2
  143. contentViewSize.width = CGFloat(minWidth)
  144. }
  145. if Int(tipViewFrame.size.height) < minHeight {
  146. tipViewFrame.origin.y = (CGFloat(minHeight)-tipViewFrame.size.height)/2
  147. contentViewSize.height = CGFloat(minHeight)
  148. }
  149. contentView?.setFrameSize(contentViewSize)
  150. tipView.frame = tipViewFrame
  151. self.tipPopover?.contentSize = contentViewSize
  152. self.tipPopover?.animates = animated
  153. self.tipPopover?.show(relativeTo: knobRect!, of: self, preferredEdge: preferredEdge!)
  154. }
  155. }
  156. }
  157. func tipForValue(_ value: Double) -> String {
  158. if let tip = self.delegate?.tipForValue(inSlider: self, value: self.doubleValue) {
  159. return tip
  160. }
  161. return String(format: "%0.f", self.doubleValue)
  162. }
  163. func closeTipPopover(animated: Bool) {
  164. if ((self.tipPopover?.isShown) != nil) {
  165. self.tipPopover?.animates = animated
  166. self.tipPopover?.close()
  167. }
  168. }
  169. }
  170. extension KMSlider {
  171. override func mouseDown(with event: NSEvent) {
  172. if self.tipEnabled {
  173. self.showTipPopover(animated: true)
  174. }
  175. super.mouseDown(with: event)
  176. }
  177. override func mouseEntered(with theEvent: NSEvent) {
  178. if self.tipEnabled {
  179. self.showTipPopover(animated: true)
  180. }
  181. }
  182. override func mouseExited(with theEvent: NSEvent) {
  183. self.closeTipPopover(animated: true)
  184. }
  185. }