Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7816aff

Browse files
committedDec 11, 2024
Initial submission
1 parent ad3115b commit 7816aff

File tree

5 files changed

+511
-0
lines changed

5 files changed

+511
-0
lines changed
 
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// The Swift Programming Language
2+
// https://docs.swift.org/swift-book
3+
4+
//
5+
// LCWindowButton.swift
6+
// WindowButton
7+
//
8+
// Created by DevLiuSir on 2020/12/11.
9+
//
10+
11+
import Foundation
12+
import Cocoa
13+
14+
15+
/// 窗口按钮
16+
class LCWindowButton: NSControl {
17+
18+
// MARK: - Properties
19+
20+
/// 按钮的类型,用于表示窗口按钮的功能(关闭、最小化、全屏等)。
21+
var buttonType: LCWindowButtonType = .close {
22+
didSet {
23+
hover = false // 当按钮类型变化时,取消鼠标悬停效果。
24+
needsDisplay = true // 标记需要重新绘制视图。
25+
}
26+
}
27+
28+
/// 按钮的`激活状态`,用于指示按钮是否处于激活状态(例如,与当前窗口相关)。
29+
var isActive: Bool = false {
30+
didSet {
31+
needsDisplay = true // 标记需要重新绘制视图。
32+
}
33+
}
34+
35+
/// 窗口是否处于`全屏状态`的标志。
36+
private var isWindowFullScreen: Bool = false {
37+
didSet {
38+
needsDisplay = true // 标记需要重新绘制视图。
39+
}
40+
}
41+
42+
/// 忽略自己的鼠标滑入, default = false
43+
var ignoreMouseHover: Bool = false {
44+
didSet {
45+
updateTrackingAreas()
46+
}
47+
}
48+
/// 是否选中状态
49+
var hover: Bool = false {
50+
didSet {
51+
needsDisplay = true
52+
}
53+
}
54+
55+
// MARK: - Initializers
56+
57+
/// 根据`指定的按钮类型`创建一个窗口按钮实例。
58+
/// - Parameter buttonType: 按钮的类型,例如关闭、最小化、全屏等。
59+
/// - Returns: 配置好的 `LCWindowButton` 实例。
60+
static func button(withType buttonType: LCWindowButtonType) -> LCWindowButton {
61+
return LCWindowButton(type: buttonType)
62+
}
63+
64+
/// 使用`指定的按钮类型`初始化`窗口按钮`。
65+
/// - Parameter type: 按钮的类型,例如关闭、最小化、全屏等。
66+
init(type: LCWindowButtonType) {
67+
self.buttonType = type // 设置按钮的类型。
68+
super.init(frame: .zero) // 调用父类的初始化方法,设置默认的 frame。
69+
}
70+
71+
required init?(coder: NSCoder) {
72+
super.init(coder: coder)
73+
}
74+
75+
// MARK: - Window State Listeners
76+
77+
override func viewDidMoveToWindow() {
78+
super.viewDidMoveToWindow()
79+
NotificationCenter.default.addObserver(self, selector: #selector(windowDidBecomeActive), name: NSWindow.didBecomeKeyNotification, object: window)
80+
NotificationCenter.default.addObserver(self, selector: #selector(windowDidResignActive), name: NSWindow.didResignKeyNotification, object: window)
81+
NotificationCenter.default.addObserver(self, selector: #selector(windowDidFullScreen), name: NSWindow.didEnterFullScreenNotification, object: window)
82+
NotificationCenter.default.addObserver(self, selector: #selector(windowDidExitFullScreen), name: NSWindow.didExitFullScreenNotification, object: window)
83+
}
84+
85+
deinit {
86+
NotificationCenter.default.removeObserver(self)
87+
}
88+
89+
//MARK: - Handle notification
90+
91+
@objc private func windowDidBecomeActive() {
92+
isActive = true
93+
}
94+
95+
@objc private func windowDidResignActive() {
96+
isActive = false
97+
}
98+
99+
@objc private func windowDidFullScreen() {
100+
isWindowFullScreen = true
101+
}
102+
103+
@objc private func windowDidExitFullScreen() {
104+
isWindowFullScreen = false
105+
}
106+
107+
// MARK: - Mouse Tracking
108+
109+
110+
/// 更新追踪区域以响应鼠标进入和退出事件
111+
override func updateTrackingAreas() {
112+
super.updateTrackingAreas()
113+
// 移除所有现有的追踪区域
114+
trackingAreas.forEach(removeTrackingArea)
115+
guard !ignoreMouseHover else { return }
116+
// 添加新的追踪区域
117+
let trackingArea = NSTrackingArea(
118+
rect: bounds,
119+
options: [.activeAlways, .mouseEnteredAndExited],
120+
owner: self,
121+
userInfo: nil
122+
)
123+
addTrackingArea(trackingArea)
124+
}
125+
126+
/*** 光标进入跟踪区域 ***/
127+
override func mouseEntered(with event: NSEvent) {
128+
hover = true // 更新记录值
129+
}
130+
131+
/*** 光标已退出跟踪区域 ***/
132+
override func mouseExited(with event: NSEvent) {
133+
hover = false // 更新记录值
134+
}
135+
136+
// MARK: - Mouse Events
137+
138+
/// 处理鼠标按下事件
139+
override func mouseDown(with event: NSEvent) {
140+
if isEnabled {
141+
// 设置窗口的第一响应者为当前控件
142+
window?.makeFirstResponder(self)
143+
}
144+
super.mouseDown(with: event)
145+
}
146+
147+
/// 处理鼠标抬起事件
148+
override func mouseUp(with event: NSEvent) {
149+
if isEnabled {
150+
if let action = action {
151+
// 发送指定的动作到目标对象
152+
NSApp.sendAction(action, to: target, from: self)
153+
}
154+
}
155+
super.mouseUp(with: event)
156+
}
157+
158+
/// 控件是否接受第一响应者状态
159+
override var acceptsFirstResponder: Bool {
160+
return true
161+
}
162+
163+
/// 成为第一响应者时的处理
164+
override func becomeFirstResponder() -> Bool {
165+
return true
166+
}
167+
168+
169+
170+
// MARK: - Drawing
171+
172+
override func draw(_ dirtyRect: NSRect) {
173+
super.draw(dirtyRect)
174+
let width = bounds.width
175+
let height = bounds.height
176+
177+
var bgGradient: NSGradient?
178+
var strokeColor: NSColor = .clear
179+
var symbolColor: NSColor = .clear
180+
181+
switch buttonType {
182+
case .close:
183+
bgGradient = NSGradient(starting: .rgba(255, 95, 86, 1), ending: .rgba(255, 99, 91, 1))
184+
strokeColor = .rgba(226, 62, 55, 1)
185+
symbolColor = .rgba(77, 0, 0, 1)
186+
case .mini:
187+
bgGradient = NSGradient(starting: .rgba(255, 189, 46, 1), ending: .rgba(255, 197, 47, 1))
188+
strokeColor = .rgba(223, 157, 24, 1)
189+
symbolColor = .rgba(153, 88, 1, 1)
190+
case .fullScreen, .exitFullScreen:
191+
bgGradient = NSGradient(starting: .rgba(39, 201, 63, 1), ending: .rgba(39, 208, 65, 1))
192+
strokeColor = .rgba(46, 176, 60, 1)
193+
symbolColor = .rgba(1, 100, 0, 1)
194+
}
195+
196+
if !isActive && !hover {
197+
bgGradient = NSGradient(starting: .rgba(79, 83, 79, 1), ending: .rgba(75, 79, 75, 1))
198+
strokeColor = .rgba(65, 65, 65, 1)
199+
}
200+
201+
if buttonType == .mini && isWindowFullScreen {
202+
bgGradient = NSGradient(starting: .rgba(94, 98, 94, 1), ending: .rgba(90, 94, 90, 1))
203+
strokeColor = .rgba(80, 80, 80, 1)
204+
}
205+
206+
let path = NSBezierPath(ovalIn: NSRect(x: 0.5, y: 0.5, width: width - 1, height: height - 1))
207+
bgGradient?.draw(in: path, relativeCenterPosition: .zero)
208+
strokeColor.setStroke()
209+
path.lineWidth = 0.5
210+
path.stroke()
211+
212+
if buttonType == .mini && isWindowFullScreen { return }
213+
214+
guard hover else { return }
215+
216+
drawSymbol(for: buttonType, in: NSRect(x: 0, y: 0, width: width, height: height), with: symbolColor)
217+
}
218+
219+
220+
/// 绘制按钮符号(如关闭、最小化、全屏等)
221+
/// - Parameters:
222+
/// - buttonType: 按钮类型,用于决定绘制的符号类型
223+
/// - rect: 绘制区域的矩形框
224+
/// - color: 符号的颜色
225+
private func drawSymbol(for buttonType: LCWindowButtonType, in rect: NSRect, with color: NSColor) {
226+
let width = rect.width
227+
let height = rect.height
228+
229+
switch buttonType {
230+
case .close:
231+
let path = NSBezierPath()
232+
path.move(to: NSPoint(x: width * 0.3, y: height * 0.3))
233+
path.line(to: NSPoint(x: width * 0.7, y: height * 0.7))
234+
path.move(to: NSPoint(x: width * 0.7, y: height * 0.3))
235+
path.line(to: NSPoint(x: width * 0.3, y: height * 0.7))
236+
path.lineWidth = 1
237+
color.setStroke()
238+
path.stroke()
239+
case .mini:
240+
let path = NSBezierPath()
241+
path.move(to: NSPoint(x: width * 0.2, y: height * 0.5))
242+
path.line(to: NSPoint(x: width * 0.8, y: height * 0.5))
243+
path.lineWidth = 2
244+
color.setStroke()
245+
path.stroke()
246+
case .fullScreen:
247+
let path = NSBezierPath()
248+
path.move(to: NSPoint(x: width * 0.25, y: height * 0.75))
249+
path.line(to: NSPoint(x: width * 0.25, y: height / 3))
250+
path.line(to: NSPoint(x: width * 2 / 3, y: height * 0.75))
251+
path.close()
252+
color.setFill()
253+
path.fill()
254+
255+
path.move(to: NSPoint(x: width * 0.75, y: height * 0.25))
256+
path.line(to: NSPoint(x: width * 0.75, y: height * 2 / 3))
257+
path.line(to: NSPoint(x: width / 3, y: height * 0.25))
258+
path.close()
259+
color.setFill()
260+
path.fill()
261+
case .exitFullScreen:
262+
let path = NSBezierPath()
263+
path.move(to: NSPoint(x: width * 0.1, y: height * 0.52))
264+
path.line(to: NSPoint(x: width * 0.48, y: height * 0.52))
265+
path.line(to: NSPoint(x: width * 0.48, y: height * 0.9))
266+
path.close()
267+
color.setFill()
268+
path.fill()
269+
270+
path.move(to: NSPoint(x: width * 0.9, y: height * 0.48))
271+
path.line(to: NSPoint(x: width * 0.52, y: height * 0.48))
272+
path.line(to: NSPoint(x: width * 0.52, y: height * 0.1))
273+
path.close()
274+
color.setFill()
275+
path.fill()
276+
}
277+
}
278+
279+
280+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// LCWindowButtonOperateType.swift
3+
// WindowButton
4+
//
5+
// Created by DevLiuSir on 2020/12/11.
6+
//
7+
8+
import Foundation
9+
10+
/// 操作视图的按钮类型枚举,只有3个按钮
11+
public enum LCWindowButtonOperateType: String {
12+
/// 关闭按钮
13+
case close
14+
/// 最小化按钮
15+
case mini
16+
/// 进入全屏按钮
17+
case fullScreen
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// LCWindowButtonType.swift
3+
// WindowButton
4+
//
5+
// Created by DevLiuSir on 2020/12/11.
6+
//
7+
8+
import Foundation
9+
10+
/// 窗口按钮类型枚举, 包括关闭、最小化、全屏、退出全屏。
11+
public enum LCWindowButtonType: String {
12+
/// 关闭按钮
13+
case close
14+
/// 最小化按钮
15+
case mini
16+
/// 进入全屏按钮
17+
case fullScreen
18+
/// 退出全屏按钮
19+
case exitFullScreen
20+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//
2+
// LCWindowOperateView.swift
3+
// WindowButton
4+
//
5+
// Created by DevLiuSir on 2020/12/11.
6+
//
7+
8+
import Foundation
9+
import Cocoa
10+
11+
12+
13+
/// 按钮的高度
14+
let kLCWindowButtonWH: CGFloat = 13.0
15+
16+
17+
18+
/// 窗口操作视图
19+
public class LCWindowOperateView: NSView {
20+
21+
/// 按钮类型数组,用于确定窗口操作视图中包含哪些按钮
22+
var buttonTypes: [LCWindowButtonOperateType]
23+
24+
/// 点击事件回调,返回被点击的按钮类型
25+
var clickHandler: ((LCWindowButtonType) -> Void)?
26+
27+
28+
// MARK: - Initializers
29+
30+
/// 初始化 LCWindowOperateView
31+
/// - Parameter buttonTypes: 包含按钮类型的数组,用于确定需要显示的按钮
32+
init(buttonTypes: [LCWindowButtonOperateType]) {
33+
self.buttonTypes = buttonTypes
34+
super.init(frame: .zero)
35+
setupButtons() // 设置按钮
36+
}
37+
38+
39+
required init?(coder: NSCoder) {
40+
fatalError("init(coder:) has not been implemented")
41+
}
42+
43+
44+
/// 创建并返回一个 LCWindowOperateView 实例
45+
/// - Parameter buttonTypes: 包含按钮类型的数组
46+
/// - Returns: 配置了指定按钮类型的 LCWindowOperateView 实例
47+
static func operateView(buttonTypes: [LCWindowButtonOperateType]) -> LCWindowOperateView {
48+
return LCWindowOperateView(buttonTypes: buttonTypes)
49+
}
50+
51+
52+
// MARK: - Setup Buttons
53+
54+
/// 设置按钮
55+
private func setupButtons() {
56+
for (_ , buttonType) in buttonTypes.enumerated() {
57+
guard let buttonType = LCWindowButtonType(rawValue: buttonType.rawValue) else { continue }
58+
if buttonType == .exitFullScreen {
59+
// 退出全屏
60+
continue
61+
}
62+
let button = LCWindowButton(type: buttonType)
63+
button.ignoreMouseHover = true
64+
button.target = self
65+
button.action = #selector(operateButtonClicked(_:))
66+
addSubview(button)
67+
}
68+
self.frame = NSRect(x: 0, y: 0, width: CGFloat(subviews.count) * (kLCWindowButtonWH + 7.5) - 7.5, height: 15)
69+
}
70+
71+
72+
// MARK: - Layout
73+
74+
75+
/// 指定视图的坐标系是否是翻转的
76+
/// 返回 `true` 表示原点位于左上角
77+
public override var isFlipped: Bool {
78+
return true
79+
}
80+
81+
/// 布局子视图
82+
/// 根据视图宽度和子按钮的宽度动态排列子视图
83+
public override func layout() {
84+
super.layout()
85+
var left: CGFloat = 0 // 子视图的起始 x 坐标
86+
var frame = NSRect(x: 0, y: 0, width: kLCWindowButtonWH, height: kLCWindowButtonWH)
87+
88+
// 设置子视图在垂直方向上的居中
89+
frame.origin.y = (bounds.height - kLCWindowButtonWH) / 2
90+
91+
for subview in subviews {
92+
guard let button = subview as? LCWindowButton else { continue } // 忽略非 LCWindowButton 类型的子视图
93+
frame.origin.x = left // 设置按钮的 x 坐标
94+
button.frame = frame // 应用计算好的框架
95+
left = frame.maxX + 7.5 // 更新下一个按钮的起始 x 坐标
96+
}
97+
}
98+
99+
100+
101+
// MARK: - Mouse Tracking
102+
// 更新鼠标跟踪区域, 用于监控鼠标进入和离开视图的事件
103+
public override func updateTrackingAreas() {
104+
super.updateTrackingAreas()
105+
trackingAreas.forEach { removeTrackingArea($0) } // 移除已有的跟踪区域
106+
let trackingArea = NSTrackingArea(
107+
rect: bounds,
108+
options: [.activeAlways, .mouseEnteredAndExited], // 始终激活,监听鼠标进入和退出
109+
owner: self,
110+
userInfo: nil
111+
)
112+
addTrackingArea(trackingArea) // 添加新的跟踪区域
113+
}
114+
115+
/** 处理鼠标进入区域 **/
116+
public override func mouseEntered(with event: NSEvent) {
117+
// 设置所有子按钮的 hover 状态为 true
118+
subviews.forEach { (subview) in
119+
(subview as? LCWindowButton)?.hover = true
120+
}
121+
}
122+
123+
/** 处理鼠标离开区域 **/
124+
public override func mouseExited(with event: NSEvent) {
125+
// 设置所有子按钮的 hover 状态为 false
126+
subviews.forEach { (subview) in
127+
(subview as? LCWindowButton)?.hover = false
128+
}
129+
}
130+
131+
132+
// MARK: - Button Actions
133+
134+
/// 按钮点击事件的处理方法
135+
/// - Parameter sender: 被点击的按钮
136+
@objc private func operateButtonClicked(_ sender: LCWindowButton) {
137+
guard let window = self.window else { return }
138+
switch sender.buttonType {
139+
case .close:
140+
window.performClose(sender)
141+
case .mini:
142+
window.performMiniaturize(sender)
143+
case .fullScreen:
144+
window.toggleFullScreen(sender)
145+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
146+
sender.buttonType = .exitFullScreen
147+
}
148+
case .exitFullScreen:
149+
window.toggleFullScreen(sender)
150+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
151+
sender.buttonType = .fullScreen
152+
}
153+
}
154+
subviews.forEach { (subview) in
155+
(subview as? LCWindowButton)?.hover = false
156+
}
157+
clickHandler?(sender.buttonType)
158+
}
159+
160+
// MARK: - Helper Method
161+
162+
/// 查找`指定类型的按钮`
163+
/// - Parameter buttonType: 按钮的类型
164+
/// - Returns: 与指定类型匹配的按钮,如果未找到则返回 `nil`
165+
func button(withType buttonType: LCWindowButtonType) -> LCWindowButton? {
166+
return subviews.compactMap { $0 as? LCWindowButton }.first { $0.buttonType == buttonType }
167+
}
168+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// NSColor+Extension.swift
3+
// WindowButton
4+
//
5+
// Created by DevLiuSir on 2020/12/11.
6+
//
7+
8+
import Cocoa
9+
10+
/// 扩展NSColor
11+
extension NSColor {
12+
13+
/// 创建一个指定 RGBA 值的颜色
14+
///
15+
/// - Parameters:
16+
/// - r: 红色通道的值(0~255)
17+
/// - g: 绿色通道的值(0~255)
18+
/// - b: 蓝色通道的值(0~255)
19+
/// - a: 透明度通道的值(0~1.0)
20+
/// - Returns: 根据指定 RGBA 值生成的 `NSColor` 对象
21+
static func rgba(_ r: CGFloat, _ g: CGFloat, _ b: CGFloat, _ a: CGFloat) -> NSColor {
22+
return NSColor(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: a)
23+
}
24+
}
25+

0 commit comments

Comments
 (0)
Please sign in to comment.