227 lines
9.3 KiB
Swift
227 lines
9.3 KiB
Swift
// StyleBarView.swift
|
|
// GreenshotMac
|
|
//
|
|
// The context sensitive style controls, equivalent to Greenshot's editor field toolbar.
|
|
|
|
import AppKit
|
|
|
|
@MainActor
|
|
protocol StyleBarDelegate: AnyObject {
|
|
func styleBar(_ bar: StyleBarView, didChange mutate: @escaping (inout ElementStyle) -> Void)
|
|
func styleBarDidApplyCrop(_ bar: StyleBarView)
|
|
}
|
|
|
|
@MainActor
|
|
final class StyleBarView: NSView {
|
|
|
|
weak var delegate: StyleBarDelegate?
|
|
|
|
private let stack = NSStackView()
|
|
private let lineColorWell = NSColorWell()
|
|
private let fillColorWell = NSColorWell()
|
|
private let highlightColorWell = NSColorWell()
|
|
private let thicknessSlider = NSSlider()
|
|
private let thicknessLabel = NSTextField(labelWithString: "2 px")
|
|
private let fontPopUp = NSPopUpButton()
|
|
private let fontSizeField = NSTextField()
|
|
private let boldButton = NSButton()
|
|
private let italicButton = NSButton()
|
|
private let obfuscatePopUp = NSPopUpButton()
|
|
private let obfuscateSlider = NSSlider()
|
|
private let shadowButton = NSButton()
|
|
private let cropButton = NSButton()
|
|
|
|
private var labelledPairs: [StyleCapabilities: [NSView]] = [:]
|
|
|
|
override init(frame frameRect: NSRect) {
|
|
super.init(frame: frameRect)
|
|
build()
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }
|
|
|
|
private func build() {
|
|
wantsLayer = true
|
|
stack.orientation = .horizontal
|
|
stack.alignment = .centerY
|
|
stack.spacing = 10
|
|
stack.edgeInsets = NSEdgeInsets(top: 6, left: 12, bottom: 6, right: 12)
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
addSubview(stack)
|
|
NSLayoutConstraint.activate([
|
|
stack.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
|
|
stack.topAnchor.constraint(equalTo: topAnchor),
|
|
stack.bottomAnchor.constraint(equalTo: bottomAnchor)
|
|
])
|
|
|
|
lineColorWell.color = Configuration.shared.editorLineColor
|
|
lineColorWell.target = self
|
|
lineColorWell.action = #selector(lineColorChanged)
|
|
let lineLabel = NSTextField(labelWithString: "Line")
|
|
labelledPairs[.lineColor] = [lineLabel, lineColorWell]
|
|
|
|
fillColorWell.color = Configuration.shared.editorFillColor
|
|
fillColorWell.target = self
|
|
fillColorWell.action = #selector(fillColorChanged)
|
|
let fillLabel = NSTextField(labelWithString: "Fill")
|
|
labelledPairs[.fillColor] = [fillLabel, fillColorWell]
|
|
|
|
highlightColorWell.color = Configuration.shared.editorHighlightColor
|
|
highlightColorWell.target = self
|
|
highlightColorWell.action = #selector(highlightColorChanged)
|
|
let highlightLabel = NSTextField(labelWithString: "Highlight")
|
|
labelledPairs[.highlightColor] = [highlightLabel, highlightColorWell]
|
|
|
|
thicknessSlider.minValue = 0
|
|
thicknessSlider.maxValue = 20
|
|
thicknessSlider.doubleValue = Double(Configuration.shared.editorLineThickness)
|
|
thicknessSlider.target = self
|
|
thicknessSlider.action = #selector(thicknessChanged)
|
|
thicknessSlider.widthAnchor.constraint(equalToConstant: 90).isActive = true
|
|
labelledPairs[.lineThickness] = [NSTextField(labelWithString: "Width"), thicknessSlider, thicknessLabel]
|
|
|
|
for family in ["Helvetica", "Helvetica Neue", "Menlo", "SF Pro", "Times New Roman", "Arial", "Georgia"] {
|
|
fontPopUp.addItem(withTitle: family)
|
|
}
|
|
fontPopUp.selectItem(withTitle: Configuration.shared.editorFontFamily)
|
|
fontPopUp.target = self
|
|
fontPopUp.action = #selector(fontChanged)
|
|
|
|
fontSizeField.stringValue = String(Configuration.shared.editorFontSize)
|
|
fontSizeField.target = self
|
|
fontSizeField.action = #selector(fontChanged)
|
|
fontSizeField.widthAnchor.constraint(equalToConstant: 46).isActive = true
|
|
|
|
configureToggle(boldButton, title: "B", action: #selector(fontChanged))
|
|
boldButton.font = NSFont.boldSystemFont(ofSize: 12)
|
|
configureToggle(italicButton, title: "I", action: #selector(fontChanged))
|
|
labelledPairs[.font] = [fontPopUp, fontSizeField, boldButton, italicButton]
|
|
|
|
obfuscatePopUp.addItems(withTitles: ObfuscateMode.allCases.map(\.title))
|
|
obfuscatePopUp.target = self
|
|
obfuscatePopUp.action = #selector(obfuscationChanged)
|
|
obfuscateSlider.minValue = 2
|
|
obfuscateSlider.maxValue = 40
|
|
obfuscateSlider.doubleValue = Double(Configuration.shared.editorPixelSize)
|
|
obfuscateSlider.target = self
|
|
obfuscateSlider.action = #selector(obfuscationChanged)
|
|
obfuscateSlider.widthAnchor.constraint(equalToConstant: 90).isActive = true
|
|
labelledPairs[.obfuscation] = [obfuscatePopUp, NSTextField(labelWithString: "Strength"), obfuscateSlider]
|
|
|
|
configureToggle(shadowButton, title: "Shadow", action: #selector(shadowChanged))
|
|
shadowButton.state = Configuration.shared.editorDropShadow ? .on : .off
|
|
labelledPairs[.shadow] = [shadowButton]
|
|
|
|
cropButton.title = "Apply crop"
|
|
cropButton.bezelStyle = .rounded
|
|
cropButton.target = self
|
|
cropButton.action = #selector(applyCrop)
|
|
}
|
|
|
|
private func configureToggle(_ button: NSButton, title: String, action: Selector) {
|
|
button.title = title
|
|
button.setButtonType(.pushOnPushOff)
|
|
button.bezelStyle = .rounded
|
|
button.target = self
|
|
button.action = action
|
|
}
|
|
|
|
/// Rebuilds the bar so it shows only the fields the current tool or selection supports.
|
|
func update(capabilities: StyleCapabilities, style: ElementStyle, showCrop: Bool) {
|
|
for view in stack.arrangedSubviews {
|
|
stack.removeArrangedSubview(view)
|
|
view.removeFromSuperview()
|
|
}
|
|
|
|
lineColorWell.color = style.lineColor
|
|
fillColorWell.color = style.fillColor
|
|
highlightColorWell.color = style.highlightColor
|
|
thicknessSlider.doubleValue = Double(style.lineThickness)
|
|
thicknessLabel.stringValue = "\(Int(style.lineThickness)) px"
|
|
fontPopUp.selectItem(withTitle: style.fontFamily)
|
|
fontSizeField.stringValue = String(Int(style.fontSize))
|
|
boldButton.state = style.bold ? .on : .off
|
|
italicButton.state = style.italic ? .on : .off
|
|
obfuscatePopUp.selectItem(withTitle: style.obfuscateMode.title)
|
|
obfuscateSlider.doubleValue = Double(style.obfuscateMode == .pixelate ? style.pixelSize : style.blurRadius)
|
|
shadowButton.state = style.dropShadow ? .on : .off
|
|
|
|
for capability in [StyleCapabilities.lineColor, .fillColor, .highlightColor,
|
|
.lineThickness, .font, .obfuscation, .shadow] {
|
|
guard capabilities.contains(capability), let views = labelledPairs[capability] else { continue }
|
|
for view in views { stack.addArrangedSubview(view) }
|
|
}
|
|
if showCrop {
|
|
stack.addArrangedSubview(cropButton)
|
|
}
|
|
isHidden = stack.arrangedSubviews.isEmpty
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
@objc private func lineColorChanged() {
|
|
let color = lineColorWell.color
|
|
Configuration.shared.editorLineColor = color
|
|
delegate?.styleBar(self) { $0.lineColor = color }
|
|
}
|
|
|
|
@objc private func fillColorChanged() {
|
|
let color = fillColorWell.color
|
|
Configuration.shared.editorFillColor = color
|
|
delegate?.styleBar(self) { $0.fillColor = color }
|
|
}
|
|
|
|
@objc private func highlightColorChanged() {
|
|
let color = highlightColorWell.color
|
|
Configuration.shared.editorHighlightColor = color
|
|
delegate?.styleBar(self) { $0.highlightColor = color }
|
|
}
|
|
|
|
@objc private func thicknessChanged() {
|
|
let thickness = CGFloat(thicknessSlider.doubleValue.rounded())
|
|
thicknessLabel.stringValue = "\(Int(thickness)) px"
|
|
Configuration.shared.editorLineThickness = Int(thickness)
|
|
delegate?.styleBar(self) { $0.lineThickness = thickness }
|
|
}
|
|
|
|
@objc private func fontChanged() {
|
|
let family = fontPopUp.titleOfSelectedItem ?? "Helvetica"
|
|
let size = CGFloat(max(6, Int(fontSizeField.stringValue) ?? 14))
|
|
let bold = boldButton.state == .on
|
|
let italic = italicButton.state == .on
|
|
Configuration.shared.editorFontFamily = family
|
|
Configuration.shared.editorFontSize = Int(size)
|
|
delegate?.styleBar(self) { style in
|
|
style.fontFamily = family
|
|
style.fontSize = size
|
|
style.bold = bold
|
|
style.italic = italic
|
|
}
|
|
}
|
|
|
|
@objc private func obfuscationChanged() {
|
|
let mode = ObfuscateMode.allCases[max(0, obfuscatePopUp.indexOfSelectedItem)]
|
|
let value = CGFloat(obfuscateSlider.doubleValue.rounded())
|
|
if mode == .pixelate {
|
|
Configuration.shared.editorPixelSize = Int(value)
|
|
} else {
|
|
Configuration.shared.editorBlurRadius = Int(value)
|
|
}
|
|
delegate?.styleBar(self) { style in
|
|
style.obfuscateMode = mode
|
|
if mode == .pixelate { style.pixelSize = value } else { style.blurRadius = value }
|
|
}
|
|
}
|
|
|
|
@objc private func shadowChanged() {
|
|
let enabled = shadowButton.state == .on
|
|
Configuration.shared.editorDropShadow = enabled
|
|
delegate?.styleBar(self) { $0.dropShadow = enabled }
|
|
}
|
|
|
|
@objc private func applyCrop() {
|
|
delegate?.styleBarDidApplyCrop(self)
|
|
}
|
|
}
|