editor tool fixes

This commit is contained in:
Alex E. Tafoya
2026-09-18 06:25:57 -05:00
parent 7e3801c7c6
commit fcb8d5b721
2 changed files with 80 additions and 73 deletions
@@ -26,6 +26,7 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate,
private var toolButtons: [ToolKind: NSButton] = [:] private var toolButtons: [ToolKind: NSButton] = [:]
private var deleteButton: NSButton? private var deleteButton: NSButton?
private let currentColorWell = NSColorWell() private let currentColorWell = NSColorWell()
private let fillColorWell = NSColorWell()
// MARK: - Lifecycle // MARK: - Lifecycle
@@ -221,16 +222,30 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate,
delete.heightAnchor.constraint(equalToConstant: 28).isActive = true delete.heightAnchor.constraint(equalToConstant: 28).isActive = true
deleteButton = delete deleteButton = delete
// Current colour, shown the way an editor shows its foreground swatch. // Line and fill colour, the way an editor shows foreground and background swatches.
currentColorWell.color = Configuration.shared.editorLineColor currentColorWell.color = Configuration.shared.editorLineColor
currentColorWell.target = self currentColorWell.target = self
currentColorWell.action = #selector(currentColorChanged) currentColorWell.action = #selector(currentColorChanged)
currentColorWell.toolTip = "Current colour" currentColorWell.toolTip = "Line colour"
currentColorWell.translatesAutoresizingMaskIntoConstraints = false currentColorWell.translatesAutoresizingMaskIntoConstraints = false
fillColorWell.color = Configuration.shared.editorFillColor
fillColorWell.target = self
fillColorWell.action = #selector(fillColorChanged)
fillColorWell.toolTip = "Fill colour"
fillColorWell.translatesAutoresizingMaskIntoConstraints = false
for well in [currentColorWell, fillColorWell] {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
currentColorWell.widthAnchor.constraint(equalToConstant: 44), well.widthAnchor.constraint(equalToConstant: 26),
currentColorWell.heightAnchor.constraint(equalToConstant: 26) well.heightAnchor.constraint(equalToConstant: 24)
]) ])
}
let wells = NSStackView(views: [currentColorWell, fillColorWell])
wells.orientation = .horizontal
wells.spacing = 4
wells.translatesAutoresizingMaskIntoConstraints = false
var swatchRows: [[NSView]] = [] var swatchRows: [[NSView]] = []
var swatchPair: [NSView] = [] var swatchPair: [NSView] = []
@@ -259,7 +274,7 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate,
EditorWindowController.railDivider(), EditorWindowController.railDivider(),
delete, delete,
EditorWindowController.railDivider(), EditorWindowController.railDivider(),
currentColorWell, wells,
swatchGrid, swatchGrid,
spacer]) spacer])
stack.orientation = .vertical stack.orientation = .vertical
@@ -392,6 +407,12 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate,
apply(color: currentColorWell.color) apply(color: currentColorWell.color)
} }
@objc private func fillColorChanged() {
let color = fillColorWell.color
Configuration.shared.editorFillColor = color
canvas.applyStyleToSelection { $0.fillColor = color }
}
private func apply(color: NSColor) { private func apply(color: NSColor) {
// The highlighter has its own colour field; everything else uses the line colour. // The highlighter has its own colour field; everything else uses the line colour.
let isHighlight = canvas.tool == .highlight || surface.selection is HighlightElement let isHighlight = canvas.tool == .highlight || surface.selection is HighlightElement
@@ -423,9 +444,12 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate,
} }
styleBar.update(capabilities: capabilities, styleBar.update(capabilities: capabilities,
style: style, style: style,
showCrop: surface.cropElement != nil) showCrop: surface.cropElement != nil,
toolName: canvas.tool.title)
deleteButton?.isEnabled = surface.selection != nil deleteButton?.isEnabled = surface.selection != nil
currentColorWell.color = capabilities.contains(.highlightColor) ? style.highlightColor : style.lineColor currentColorWell.color = capabilities.contains(.highlightColor) ? style.highlightColor : style.lineColor
fillColorWell.color = style.fillColor
fillColorWell.isEnabled = capabilities.contains(.fillColor)
} }
private func capabilitiesForTool(_ tool: ToolKind) -> StyleCapabilities { private func capabilitiesForTool(_ tool: ToolKind) -> StyleCapabilities {
+48 -65
View File
@@ -1,7 +1,9 @@
// StyleBarView.swift // StyleBarView.swift
// GreenshotMac // GreenshotMac
// //
// The context sensitive style controls, equivalent to Greenshot's editor field toolbar. // The options row beneath the toolbar. It keeps a constant height whether or not the
// active tool has options, so selecting a tool never shifts the canvas. Colour lives
// in the tool rail on the left and is deliberately absent here.
import AppKit import AppKit
@@ -14,12 +16,13 @@ protocol StyleBarDelegate: AnyObject {
@MainActor @MainActor
final class StyleBarView: NSView { final class StyleBarView: NSView {
/// Fixed height of the options row, occupied or not.
static let barHeight: CGFloat = 34
weak var delegate: StyleBarDelegate? weak var delegate: StyleBarDelegate?
private let stack = NSStackView() private let stack = NSStackView()
private let lineColorWell = NSColorWell() private let placeholder = NSTextField(labelWithString: "")
private let fillColorWell = NSColorWell()
private let highlightColorWell = NSColorWell()
private let thicknessSlider = NSSlider() private let thicknessSlider = NSSlider()
private let thicknessLabel = NSTextField(labelWithString: "2 px") private let thicknessLabel = NSTextField(labelWithString: "2 px")
private let fontPopUp = NSPopUpButton() private let fontPopUp = NSPopUpButton()
@@ -28,10 +31,11 @@ final class StyleBarView: NSView {
private let italicButton = NSButton() private let italicButton = NSButton()
private let obfuscatePopUp = NSPopUpButton() private let obfuscatePopUp = NSPopUpButton()
private let obfuscateSlider = NSSlider() private let obfuscateSlider = NSSlider()
private let obfuscateLabel = NSTextField(labelWithString: "8")
private let shadowButton = NSButton() private let shadowButton = NSButton()
private let cropButton = NSButton() private let cropButton = NSButton()
private var labelledPairs: [StyleCapabilities: [NSView]] = [:] private var groups: [StyleCapabilities: [NSView]] = [:]
override init(frame frameRect: NSRect) { override init(frame frameRect: NSRect) {
super.init(frame: frameRect) super.init(frame: frameRect)
@@ -40,55 +44,43 @@ final class StyleBarView: NSView {
required init?(coder: NSCoder) { fatalError("init(coder:) is not used") } required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }
override var intrinsicContentSize: NSSize {
NSSize(width: NSView.noIntrinsicMetric, height: StyleBarView.barHeight)
}
private func build() { private func build() {
wantsLayer = true translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: StyleBarView.barHeight).isActive = true
stack.orientation = .horizontal stack.orientation = .horizontal
stack.alignment = .centerY stack.alignment = .centerY
stack.spacing = 10 stack.spacing = 10
stack.edgeInsets = NSEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) stack.edgeInsets = NSEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
stack.translatesAutoresizingMaskIntoConstraints = false stack.translatesAutoresizingMaskIntoConstraints = false
addSubview(stack) addSubview(stack)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: leadingAnchor), stack.leadingAnchor.constraint(equalTo: leadingAnchor),
stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor), stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
stack.topAnchor.constraint(equalTo: topAnchor), stack.centerYAnchor.constraint(equalTo: centerYAnchor)
stack.bottomAnchor.constraint(equalTo: bottomAnchor)
]) ])
lineColorWell.color = Configuration.shared.editorLineColor placeholder.font = NSFont.systemFont(ofSize: 11)
lineColorWell.target = self placeholder.textColor = .tertiaryLabelColor
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.minValue = 0
thicknessSlider.maxValue = 20 thicknessSlider.maxValue = 20
thicknessSlider.doubleValue = Double(Configuration.shared.editorLineThickness)
thicknessSlider.target = self thicknessSlider.target = self
thicknessSlider.action = #selector(thicknessChanged) thicknessSlider.action = #selector(thicknessChanged)
thicknessSlider.widthAnchor.constraint(equalToConstant: 90).isActive = true thicknessSlider.widthAnchor.constraint(equalToConstant: 100).isActive = true
labelledPairs[.lineThickness] = [NSTextField(labelWithString: "Width"), thicknessSlider, thicknessLabel] thicknessLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular)
groups[.lineThickness] = [label("Width"), thicknessSlider, thicknessLabel]
for family in ["Helvetica", "Helvetica Neue", "Menlo", "SF Pro", "Times New Roman", "Arial", "Georgia"] { for family in ["Helvetica", "Helvetica Neue", "Menlo", "SF Pro", "Times New Roman", "Arial", "Georgia"] {
fontPopUp.addItem(withTitle: family) fontPopUp.addItem(withTitle: family)
} }
fontPopUp.selectItem(withTitle: Configuration.shared.editorFontFamily)
fontPopUp.target = self fontPopUp.target = self
fontPopUp.action = #selector(fontChanged) fontPopUp.action = #selector(fontChanged)
fontSizeField.stringValue = String(Configuration.shared.editorFontSize)
fontSizeField.target = self fontSizeField.target = self
fontSizeField.action = #selector(fontChanged) fontSizeField.action = #selector(fontChanged)
fontSizeField.widthAnchor.constraint(equalToConstant: 46).isActive = true fontSizeField.widthAnchor.constraint(equalToConstant: 46).isActive = true
@@ -96,22 +88,21 @@ final class StyleBarView: NSView {
configureToggle(boldButton, title: "B", action: #selector(fontChanged)) configureToggle(boldButton, title: "B", action: #selector(fontChanged))
boldButton.font = NSFont.boldSystemFont(ofSize: 12) boldButton.font = NSFont.boldSystemFont(ofSize: 12)
configureToggle(italicButton, title: "I", action: #selector(fontChanged)) configureToggle(italicButton, title: "I", action: #selector(fontChanged))
labelledPairs[.font] = [fontPopUp, fontSizeField, boldButton, italicButton] groups[.font] = [label("Font"), fontPopUp, fontSizeField, boldButton, italicButton]
obfuscatePopUp.addItems(withTitles: ObfuscateMode.allCases.map(\.title)) obfuscatePopUp.addItems(withTitles: ObfuscateMode.allCases.map(\.title))
obfuscatePopUp.target = self obfuscatePopUp.target = self
obfuscatePopUp.action = #selector(obfuscationChanged) obfuscatePopUp.action = #selector(obfuscationChanged)
obfuscateSlider.minValue = 2 obfuscateSlider.minValue = 2
obfuscateSlider.maxValue = 40 obfuscateSlider.maxValue = 40
obfuscateSlider.doubleValue = Double(Configuration.shared.editorPixelSize)
obfuscateSlider.target = self obfuscateSlider.target = self
obfuscateSlider.action = #selector(obfuscationChanged) obfuscateSlider.action = #selector(obfuscationChanged)
obfuscateSlider.widthAnchor.constraint(equalToConstant: 90).isActive = true obfuscateSlider.widthAnchor.constraint(equalToConstant: 100).isActive = true
labelledPairs[.obfuscation] = [obfuscatePopUp, NSTextField(labelWithString: "Strength"), obfuscateSlider] obfuscateLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular)
groups[.obfuscation] = [obfuscatePopUp, label("Strength"), obfuscateSlider, obfuscateLabel]
configureToggle(shadowButton, title: "Shadow", action: #selector(shadowChanged)) configureToggle(shadowButton, title: "Shadow", action: #selector(shadowChanged))
shadowButton.state = Configuration.shared.editorDropShadow ? .on : .off groups[.shadow] = [shadowButton]
labelledPairs[.shadow] = [shadowButton]
cropButton.title = "Apply crop" cropButton.title = "Apply crop"
cropButton.bezelStyle = .rounded cropButton.bezelStyle = .rounded
@@ -119,6 +110,13 @@ final class StyleBarView: NSView {
cropButton.action = #selector(applyCrop) cropButton.action = #selector(applyCrop)
} }
private func label(_ text: String) -> NSTextField {
let field = NSTextField(labelWithString: text)
field.font = NSFont.systemFont(ofSize: 11)
field.textColor = .secondaryLabelColor
return field
}
private func configureToggle(_ button: NSButton, title: String, action: Selector) { private func configureToggle(_ button: NSButton, title: String, action: Selector) {
button.title = title button.title = title
button.setButtonType(.pushOnPushOff) button.setButtonType(.pushOnPushOff)
@@ -127,16 +125,13 @@ final class StyleBarView: NSView {
button.action = action button.action = action
} }
/// Rebuilds the bar so it shows only the fields the current tool or selection supports. /// Rebuilds the row for the current tool or selection. The row itself never disappears.
func update(capabilities: StyleCapabilities, style: ElementStyle, showCrop: Bool) { func update(capabilities: StyleCapabilities, style: ElementStyle, showCrop: Bool, toolName: String) {
for view in stack.arrangedSubviews { for view in stack.arrangedSubviews {
stack.removeArrangedSubview(view) stack.removeArrangedSubview(view)
view.removeFromSuperview() view.removeFromSuperview()
} }
lineColorWell.color = style.lineColor
fillColorWell.color = style.fillColor
highlightColorWell.color = style.highlightColor
thicknessSlider.doubleValue = Double(style.lineThickness) thicknessSlider.doubleValue = Double(style.lineThickness)
thicknessLabel.stringValue = "\(Int(style.lineThickness)) px" thicknessLabel.stringValue = "\(Int(style.lineThickness)) px"
fontPopUp.selectItem(withTitle: style.fontFamily) fontPopUp.selectItem(withTitle: style.fontFamily)
@@ -144,40 +139,27 @@ final class StyleBarView: NSView {
boldButton.state = style.bold ? .on : .off boldButton.state = style.bold ? .on : .off
italicButton.state = style.italic ? .on : .off italicButton.state = style.italic ? .on : .off
obfuscatePopUp.selectItem(withTitle: style.obfuscateMode.title) obfuscatePopUp.selectItem(withTitle: style.obfuscateMode.title)
obfuscateSlider.doubleValue = Double(style.obfuscateMode == .pixelate ? style.pixelSize : style.blurRadius) let strength = style.obfuscateMode == .pixelate ? style.pixelSize : style.blurRadius
obfuscateSlider.doubleValue = Double(strength)
obfuscateLabel.stringValue = "\(Int(strength))"
shadowButton.state = style.dropShadow ? .on : .off shadowButton.state = style.dropShadow ? .on : .off
for capability in [StyleCapabilities.lineColor, .fillColor, .highlightColor, // Colour is intentionally not offered here; the tool rail owns it.
.lineThickness, .font, .obfuscation, .shadow] { for capability in [StyleCapabilities.lineThickness, .font, .obfuscation, .shadow] {
guard capabilities.contains(capability), let views = labelledPairs[capability] else { continue } guard capabilities.contains(capability), let views = groups[capability] else { continue }
for view in views { stack.addArrangedSubview(view) } for view in views { stack.addArrangedSubview(view) }
} }
if showCrop { if showCrop {
stack.addArrangedSubview(cropButton) stack.addArrangedSubview(cropButton)
} }
isHidden = stack.arrangedSubviews.isEmpty if stack.arrangedSubviews.isEmpty {
placeholder.stringValue = "\(toolName) — no options"
stack.addArrangedSubview(placeholder)
}
} }
// MARK: - Actions // 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() { @objc private func thicknessChanged() {
let thickness = CGFloat(thicknessSlider.doubleValue.rounded()) let thickness = CGFloat(thicknessSlider.doubleValue.rounded())
thicknessLabel.stringValue = "\(Int(thickness)) px" thicknessLabel.stringValue = "\(Int(thickness)) px"
@@ -203,6 +185,7 @@ final class StyleBarView: NSView {
@objc private func obfuscationChanged() { @objc private func obfuscationChanged() {
let mode = ObfuscateMode.allCases[max(0, obfuscatePopUp.indexOfSelectedItem)] let mode = ObfuscateMode.allCases[max(0, obfuscatePopUp.indexOfSelectedItem)]
let value = CGFloat(obfuscateSlider.doubleValue.rounded()) let value = CGFloat(obfuscateSlider.doubleValue.rounded())
obfuscateLabel.stringValue = "\(Int(value))"
if mode == .pixelate { if mode == .pixelate {
Configuration.shared.editorPixelSize = Int(value) Configuration.shared.editorPixelSize = Int(value)
} else { } else {