From fcb8d5b72156cf9577a3d3e3020b9556eeadc925 Mon Sep 17 00:00:00 2001 From: "Alex E. Tafoya" Date: Fri, 18 Sep 2026 06:25:57 -0500 Subject: [PATCH] editor tool fixes --- .../Editor/EditorWindowController.swift | 40 +++++-- GreenshotMac/Editor/StyleBarView.swift | 113 ++++++++---------- 2 files changed, 80 insertions(+), 73 deletions(-) diff --git a/GreenshotMac/Editor/EditorWindowController.swift b/GreenshotMac/Editor/EditorWindowController.swift index e3112fc..2653603 100644 --- a/GreenshotMac/Editor/EditorWindowController.swift +++ b/GreenshotMac/Editor/EditorWindowController.swift @@ -26,6 +26,7 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate, private var toolButtons: [ToolKind: NSButton] = [:] private var deleteButton: NSButton? private let currentColorWell = NSColorWell() + private let fillColorWell = NSColorWell() // MARK: - Lifecycle @@ -221,16 +222,30 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate, delete.heightAnchor.constraint(equalToConstant: 28).isActive = true 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.target = self currentColorWell.action = #selector(currentColorChanged) - currentColorWell.toolTip = "Current colour" + currentColorWell.toolTip = "Line colour" currentColorWell.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - currentColorWell.widthAnchor.constraint(equalToConstant: 44), - currentColorWell.heightAnchor.constraint(equalToConstant: 26) - ]) + + 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([ + well.widthAnchor.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 swatchPair: [NSView] = [] @@ -259,7 +274,7 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate, EditorWindowController.railDivider(), delete, EditorWindowController.railDivider(), - currentColorWell, + wells, swatchGrid, spacer]) stack.orientation = .vertical @@ -392,6 +407,12 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate, 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) { // The highlighter has its own colour field; everything else uses the line colour. let isHighlight = canvas.tool == .highlight || surface.selection is HighlightElement @@ -423,9 +444,12 @@ final class EditorWindowController: NSWindowController, NSWindowDelegate, } styleBar.update(capabilities: capabilities, style: style, - showCrop: surface.cropElement != nil) + showCrop: surface.cropElement != nil, + toolName: canvas.tool.title) deleteButton?.isEnabled = surface.selection != nil currentColorWell.color = capabilities.contains(.highlightColor) ? style.highlightColor : style.lineColor + fillColorWell.color = style.fillColor + fillColorWell.isEnabled = capabilities.contains(.fillColor) } private func capabilitiesForTool(_ tool: ToolKind) -> StyleCapabilities { diff --git a/GreenshotMac/Editor/StyleBarView.swift b/GreenshotMac/Editor/StyleBarView.swift index c78954d..fe14f7d 100644 --- a/GreenshotMac/Editor/StyleBarView.swift +++ b/GreenshotMac/Editor/StyleBarView.swift @@ -1,7 +1,9 @@ // StyleBarView.swift // 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 @@ -14,12 +16,13 @@ protocol StyleBarDelegate: AnyObject { @MainActor final class StyleBarView: NSView { + /// Fixed height of the options row, occupied or not. + static let barHeight: CGFloat = 34 + weak var delegate: StyleBarDelegate? private let stack = NSStackView() - private let lineColorWell = NSColorWell() - private let fillColorWell = NSColorWell() - private let highlightColorWell = NSColorWell() + private let placeholder = NSTextField(labelWithString: "") private let thicknessSlider = NSSlider() private let thicknessLabel = NSTextField(labelWithString: "2 px") private let fontPopUp = NSPopUpButton() @@ -28,10 +31,11 @@ final class StyleBarView: NSView { private let italicButton = NSButton() private let obfuscatePopUp = NSPopUpButton() private let obfuscateSlider = NSSlider() + private let obfuscateLabel = NSTextField(labelWithString: "8") private let shadowButton = NSButton() private let cropButton = NSButton() - private var labelledPairs: [StyleCapabilities: [NSView]] = [:] + private var groups: [StyleCapabilities: [NSView]] = [:] override init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -40,55 +44,43 @@ final class StyleBarView: NSView { required init?(coder: NSCoder) { fatalError("init(coder:) is not used") } + override var intrinsicContentSize: NSSize { + NSSize(width: NSView.noIntrinsicMetric, height: StyleBarView.barHeight) + } + private func build() { - wantsLayer = true + translatesAutoresizingMaskIntoConstraints = false + heightAnchor.constraint(equalToConstant: StyleBarView.barHeight).isActive = true + stack.orientation = .horizontal stack.alignment = .centerY 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 addSubview(stack) NSLayoutConstraint.activate([ stack.leadingAnchor.constraint(equalTo: leadingAnchor), stack.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor), - stack.topAnchor.constraint(equalTo: topAnchor), - stack.bottomAnchor.constraint(equalTo: bottomAnchor) + stack.centerYAnchor.constraint(equalTo: centerYAnchor) ]) - 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] + placeholder.font = NSFont.systemFont(ofSize: 11) + placeholder.textColor = .tertiaryLabelColor 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] + thicknessSlider.widthAnchor.constraint(equalToConstant: 100).isActive = true + 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"] { 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 @@ -96,22 +88,21 @@ final class StyleBarView: NSView { 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] + groups[.font] = [label("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] + obfuscateSlider.widthAnchor.constraint(equalToConstant: 100).isActive = true + obfuscateLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular) + groups[.obfuscation] = [obfuscatePopUp, label("Strength"), obfuscateSlider, obfuscateLabel] configureToggle(shadowButton, title: "Shadow", action: #selector(shadowChanged)) - shadowButton.state = Configuration.shared.editorDropShadow ? .on : .off - labelledPairs[.shadow] = [shadowButton] + groups[.shadow] = [shadowButton] cropButton.title = "Apply crop" cropButton.bezelStyle = .rounded @@ -119,6 +110,13 @@ final class StyleBarView: NSView { 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) { button.title = title button.setButtonType(.pushOnPushOff) @@ -127,16 +125,13 @@ final class StyleBarView: NSView { 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) { + /// Rebuilds the row for the current tool or selection. The row itself never disappears. + func update(capabilities: StyleCapabilities, style: ElementStyle, showCrop: Bool, toolName: String) { 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) @@ -144,40 +139,27 @@ final class StyleBarView: NSView { 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) + let strength = style.obfuscateMode == .pixelate ? style.pixelSize : style.blurRadius + obfuscateSlider.doubleValue = Double(strength) + obfuscateLabel.stringValue = "\(Int(strength))" 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 } + // Colour is intentionally not offered here; the tool rail owns it. + for capability in [StyleCapabilities.lineThickness, .font, .obfuscation, .shadow] { + guard capabilities.contains(capability), let views = groups[capability] else { continue } for view in views { stack.addArrangedSubview(view) } } if showCrop { stack.addArrangedSubview(cropButton) } - isHidden = stack.arrangedSubviews.isEmpty + if stack.arrangedSubviews.isEmpty { + placeholder.stringValue = "\(toolName) — no options" + stack.addArrangedSubview(placeholder) + } } // 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" @@ -203,6 +185,7 @@ final class StyleBarView: NSView { @objc private func obfuscationChanged() { let mode = ObfuscateMode.allCases[max(0, obfuscatePopUp.indexOfSelectedItem)] let value = CGFloat(obfuscateSlider.doubleValue.rounded()) + obfuscateLabel.stringValue = "\(Int(value))" if mode == .pixelate { Configuration.shared.editorPixelSize = Int(value) } else {