// 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) } }