|
|
@@ -50,7 +50,7 @@ struct CanvasSnapshot {
|
|
|
// MARK: - Overlay
|
|
|
|
|
|
final class DrawPrintOverlayView: NSView, AppearanceRefreshable {
|
|
|
- private static let toolsPanelWidth: CGFloat = 56
|
|
|
+ private static let toolsPanelWidth: CGFloat = 48
|
|
|
private static let toolsPanelGap: CGFloat = 12
|
|
|
|
|
|
var onDismiss: (() -> Void)?
|
|
|
@@ -61,6 +61,9 @@ final class DrawPrintOverlayView: NSView, AppearanceRefreshable {
|
|
|
private let titleLabel = NSTextField(labelWithString: "Blank Canvas")
|
|
|
private let canvasContainer = NSView()
|
|
|
private let canvasView = BlankCanvasView()
|
|
|
+ private let addTextPromptView = AddTextPromptView()
|
|
|
+ private var pendingTextPlacement: NSPoint?
|
|
|
+ private var suppressCanvasTextPrompt = false
|
|
|
private let toolsPanel = CanvasToolsPanel()
|
|
|
private let bottomButtonStack = NSStackView()
|
|
|
private let savePDFButton = DrawPrintSecondaryButton(title: "Save PDF", symbolName: "square.and.arrow.down")
|
|
|
@@ -100,6 +103,7 @@ final class DrawPrintOverlayView: NSView, AppearanceRefreshable {
|
|
|
printButton.refreshAppearance()
|
|
|
canvasView.refreshAppearance()
|
|
|
toolsPanel.refreshAppearance()
|
|
|
+ addTextPromptView.refreshAppearance()
|
|
|
}
|
|
|
|
|
|
func present(in parent: NSView) {
|
|
|
@@ -160,7 +164,8 @@ final class DrawPrintOverlayView: NSView, AppearanceRefreshable {
|
|
|
self?.canvasView.activeTool = tool
|
|
|
}
|
|
|
toolsPanel.onAddText = { [weak self] in
|
|
|
- self?.canvasView.addTextItem()
|
|
|
+ self?.pendingTextPlacement = nil
|
|
|
+ self?.presentAddTextDialog()
|
|
|
}
|
|
|
toolsPanel.onTextAlignment = { [weak self] alignment in
|
|
|
self?.canvasView.setSelectedTextAlignment(alignment)
|
|
|
@@ -177,12 +182,14 @@ final class DrawPrintOverlayView: NSView, AppearanceRefreshable {
|
|
|
toolsPanel.onDrawLineWidth = { [weak self] width in
|
|
|
self?.canvasView.drawLineWidth = width
|
|
|
}
|
|
|
- toolsPanel.onAddImage = { [weak self] in
|
|
|
- self?.pickImage()
|
|
|
- }
|
|
|
canvasView.onRequestAddImage = { [weak self] point in
|
|
|
self?.pickImage(at: point)
|
|
|
}
|
|
|
+ canvasView.onRequestAddText = { [weak self] point in
|
|
|
+ guard let self, !self.suppressCanvasTextPrompt else { return }
|
|
|
+ self.pendingTextPlacement = point
|
|
|
+ self.presentAddTextDialog()
|
|
|
+ }
|
|
|
|
|
|
bottomButtonStack.orientation = .horizontal
|
|
|
bottomButtonStack.spacing = 12
|
|
|
@@ -196,6 +203,7 @@ final class DrawPrintOverlayView: NSView, AppearanceRefreshable {
|
|
|
addSubview(canvasContainer)
|
|
|
addSubview(toolsPanel)
|
|
|
addSubview(bottomButtonStack)
|
|
|
+ addSubview(addTextPromptView)
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
backButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24),
|
|
|
@@ -225,7 +233,22 @@ final class DrawPrintOverlayView: NSView, AppearanceRefreshable {
|
|
|
|
|
|
bottomButtonStack.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
bottomButtonStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -28),
|
|
|
+
|
|
|
+ addTextPromptView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
|
+ addTextPromptView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
|
+ addTextPromptView.topAnchor.constraint(equalTo: topAnchor),
|
|
|
+ addTextPromptView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
])
|
|
|
+
|
|
|
+ addTextPromptView.isHidden = true
|
|
|
+ addTextPromptView.alphaValue = 0
|
|
|
+ addTextPromptView.onAdd = { [weak self] text in
|
|
|
+ self?.commitAddedText(text)
|
|
|
+ }
|
|
|
+ addTextPromptView.onCancel = { [weak self] in
|
|
|
+ self?.pendingTextPlacement = nil
|
|
|
+ self?.hideAddTextPrompt()
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private func pickImage(at placement: NSPoint? = nil) {
|
|
|
@@ -243,6 +266,52 @@ final class DrawPrintOverlayView: NSView, AppearanceRefreshable {
|
|
|
canvasView.addImage(image, at: placement)
|
|
|
}
|
|
|
|
|
|
+ private func commitAddedText(_ text: String) {
|
|
|
+ let placement = pendingTextPlacement
|
|
|
+ pendingTextPlacement = nil
|
|
|
+ suppressCanvasTextPrompt = true
|
|
|
+
|
|
|
+ canvasView.stopInlineTextEditing()
|
|
|
+ layoutSubtreeIfNeeded()
|
|
|
+ canvasContainer.layoutSubtreeIfNeeded()
|
|
|
+ canvasView.layoutSubtreeIfNeeded()
|
|
|
+ canvasView.addTextItem(text: text, at: placement)
|
|
|
+
|
|
|
+ DispatchQueue.main.async { [weak self] in
|
|
|
+ self?.hideAddTextPrompt(animated: false)
|
|
|
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
|
|
|
+ self?.suppressCanvasTextPrompt = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func presentAddTextDialog() {
|
|
|
+ addSubview(addTextPromptView, positioned: .above, relativeTo: nil)
|
|
|
+ addTextPromptView.prepareForPresentation()
|
|
|
+ addTextPromptView.isHidden = false
|
|
|
+ NSAnimationContext.runAnimationGroup { context in
|
|
|
+ context.duration = 0.18
|
|
|
+ addTextPromptView.animator().alphaValue = 1
|
|
|
+ } completionHandler: { [weak self] in
|
|
|
+ self?.addTextPromptView.focusInput()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func hideAddTextPrompt(animated: Bool = true) {
|
|
|
+ let hide = { [weak self] in
|
|
|
+ self?.addTextPromptView.alphaValue = 0
|
|
|
+ self?.addTextPromptView.isHidden = true
|
|
|
+ }
|
|
|
+ guard animated else {
|
|
|
+ hide()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ NSAnimationContext.runAnimationGroup({ context in
|
|
|
+ context.duration = 0.14
|
|
|
+ addTextPromptView.animator().alphaValue = 0
|
|
|
+ }, completionHandler: hide)
|
|
|
+ }
|
|
|
+
|
|
|
private func canvasSnapshot() -> NSImage? {
|
|
|
guard canvasView.hasContent else {
|
|
|
PrintService.showEmptyCanvasAlert()
|
|
|
@@ -273,13 +342,15 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
|
|
|
if oldValue == .text, activeTool != .text {
|
|
|
endEditingText()
|
|
|
}
|
|
|
+ updateCursorForActiveTool()
|
|
|
needsDisplay = true
|
|
|
}
|
|
|
}
|
|
|
var drawColor: NSColor = NSColor(red: 0.13, green: 0.68, blue: 0.42, alpha: 1)
|
|
|
- var drawLineWidth: CGFloat = 3
|
|
|
+ var drawLineWidth: CGFloat = 2
|
|
|
var onUndoStateChanged: ((_ canUndo: Bool, _ canRedo: Bool) -> Void)?
|
|
|
var onRequestAddImage: ((NSPoint) -> Void)?
|
|
|
+ var onRequestAddText: ((NSPoint) -> Void)?
|
|
|
|
|
|
var hasContent: Bool {
|
|
|
syncTextFromFields()
|
|
|
@@ -322,8 +393,13 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
|
|
|
redoButton.refreshAppearance()
|
|
|
}
|
|
|
|
|
|
+ func stopInlineTextEditing() {
|
|
|
+ endEditingText()
|
|
|
+ }
|
|
|
+
|
|
|
private func setup() {
|
|
|
wantsLayer = true
|
|
|
+ layerContentsRedrawPolicy = .onSetNeedsDisplay
|
|
|
layer?.cornerRadius = AppTheme.contentPanelCornerRadius
|
|
|
layer?.masksToBounds = true
|
|
|
|
|
|
@@ -347,6 +423,25 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
|
|
|
redoButton.widthAnchor.constraint(equalToConstant: 32),
|
|
|
redoButton.heightAnchor.constraint(equalToConstant: 32),
|
|
|
])
|
|
|
+
|
|
|
+ updateCursorForActiveTool()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateCursorForActiveTool() {
|
|
|
+ window?.invalidateCursorRects(for: self)
|
|
|
+ }
|
|
|
+
|
|
|
+ override func resetCursorRects() {
|
|
|
+ let cursor: NSCursor
|
|
|
+ switch activeTool {
|
|
|
+ case .draw:
|
|
|
+ cursor = .crosshair
|
|
|
+ case .text:
|
|
|
+ cursor = .iBeam
|
|
|
+ case .image:
|
|
|
+ cursor = .pointingHand
|
|
|
+ }
|
|
|
+ addCursorRect(bounds, cursor: cursor)
|
|
|
}
|
|
|
|
|
|
override func hitTest(_ point: NSPoint) -> NSView? {
|
|
|
@@ -468,7 +563,7 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
|
|
|
} else if editingTextID != nil {
|
|
|
endEditingText()
|
|
|
} else {
|
|
|
- addTextItem(at: point)
|
|
|
+ onRequestAddText?(point)
|
|
|
}
|
|
|
case .image:
|
|
|
if let index = imageIndex(at: point) {
|
|
|
@@ -553,12 +648,19 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
|
|
|
notifyUndoState()
|
|
|
}
|
|
|
|
|
|
- func addTextItem(at origin: NSPoint? = nil) {
|
|
|
+ func addTextItem(text: String = "Text", at origin: NSPoint? = nil) {
|
|
|
+ guard bounds.width > 1, bounds.height > 1 else {
|
|
|
+ DispatchQueue.main.async { [weak self] in
|
|
|
+ self?.addTextItem(text: text, at: origin)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
pushSnapshot()
|
|
|
let defaultSize = textBounds(
|
|
|
for: CanvasTextData(
|
|
|
id: UUID(),
|
|
|
- text: "Text",
|
|
|
+ text: text,
|
|
|
origin: .zero,
|
|
|
fontSize: 18,
|
|
|
color: .black,
|
|
|
@@ -582,7 +684,7 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
|
|
|
clampedOrigin.y = max(0, min(clampedOrigin.y, bounds.height - defaultSize.height))
|
|
|
let item = CanvasTextData(
|
|
|
id: UUID(),
|
|
|
- text: "Text",
|
|
|
+ text: text,
|
|
|
origin: clampedOrigin,
|
|
|
fontSize: 18,
|
|
|
color: .black,
|
|
|
@@ -590,8 +692,8 @@ final class BlankCanvasView: NSView, AppearanceRefreshable {
|
|
|
)
|
|
|
textItems.append(item)
|
|
|
selectedTextID = item.id
|
|
|
- beginEditingText(id: item.id)
|
|
|
- needsDisplay = true
|
|
|
+ setNeedsDisplay(bounds)
|
|
|
+ displayIfNeeded()
|
|
|
notifyUndoState()
|
|
|
}
|
|
|
|
|
|
@@ -801,7 +903,6 @@ final class CanvasToolsPanel: NSView, AppearanceRefreshable {
|
|
|
var onTextColor: ((NSColor) -> Void)?
|
|
|
var onDrawColor: ((NSColor) -> Void)?
|
|
|
var onDrawLineWidth: ((CGFloat) -> Void)?
|
|
|
- var onAddImage: (() -> Void)?
|
|
|
|
|
|
private var activeTool: CanvasTool = .draw
|
|
|
private let toolStack = NSStackView()
|
|
|
@@ -870,8 +971,8 @@ final class CanvasToolsPanel: NSView, AppearanceRefreshable {
|
|
|
NSLayoutConstraint.activate([
|
|
|
toolStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6),
|
|
|
toolStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6),
|
|
|
- toolStack.topAnchor.constraint(equalTo: topAnchor, constant: 10),
|
|
|
- toolStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10),
|
|
|
+ toolStack.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
|
|
+ toolStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
|
|
|
|
|
subToolbar.trailingAnchor.constraint(equalTo: leadingAnchor, constant: -8),
|
|
|
subToolbar.centerYAnchor.constraint(equalTo: toolStack.arrangedSubviews.first?.centerYAnchor ?? centerYAnchor),
|
|
|
@@ -914,9 +1015,7 @@ final class CanvasToolsPanel: NSView, AppearanceRefreshable {
|
|
|
self?.pickTextColor()
|
|
|
}))
|
|
|
case .image:
|
|
|
- stack.addArrangedSubview(makeSubButton(symbol: "photo.on.rectangle.angled", action: { [weak self] in
|
|
|
- self?.onAddImage?()
|
|
|
- }))
|
|
|
+ break
|
|
|
case .draw:
|
|
|
for color in drawColors {
|
|
|
stack.addArrangedSubview(makeColorButton(color: color, action: { [weak self] in
|
|
|
@@ -924,7 +1023,7 @@ final class CanvasToolsPanel: NSView, AppearanceRefreshable {
|
|
|
}))
|
|
|
}
|
|
|
stack.addArrangedSubview(makeSubButton(symbol: "lineweight", action: { [weak self] in
|
|
|
- self?.onDrawLineWidth?(5)
|
|
|
+ self?.onDrawLineWidth?(3)
|
|
|
}))
|
|
|
}
|
|
|
|
|
|
@@ -936,9 +1035,11 @@ final class CanvasToolsPanel: NSView, AppearanceRefreshable {
|
|
|
stack.bottomAnchor.constraint(equalTo: subToolbar.bottomAnchor, constant: -6),
|
|
|
])
|
|
|
|
|
|
+ subToolbar.isHidden = activeTool == .image
|
|
|
+
|
|
|
let itemCount = max(stack.arrangedSubviews.count, 1)
|
|
|
- subToolbarHeight?.constant = 44
|
|
|
- let width = CGFloat(itemCount) * 38 + CGFloat(max(itemCount - 1, 0)) * 6 + 20
|
|
|
+ subToolbarHeight?.constant = 40
|
|
|
+ let width = CGFloat(itemCount) * 34 + CGFloat(max(itemCount - 1, 0)) * 6 + 18
|
|
|
subToolbarWidth?.constant = width
|
|
|
}
|
|
|
|
|
|
@@ -986,6 +1087,155 @@ final class CanvasToolsPanel: NSView, AppearanceRefreshable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+private final class AddTextPromptView: NSView, AppearanceRefreshable, NSTextViewDelegate {
|
|
|
+ var onAdd: ((String) -> Void)?
|
|
|
+ var onCancel: (() -> Void)?
|
|
|
+
|
|
|
+ private let dimView = NSView()
|
|
|
+ private let cardView = NSView()
|
|
|
+ private let titleLabel = NSTextField(labelWithString: "Add Text")
|
|
|
+ private let inputScrollView = NSScrollView()
|
|
|
+ private let inputTextView = NSTextView()
|
|
|
+ private let placeholderLabel = NSTextField(labelWithString: "Enter any text here...")
|
|
|
+ private let buttonStack = NSStackView()
|
|
|
+ private let addButton = DrawPrintPrintButton(title: "Add", symbolName: "plus", compact: true, firesOnMouseDown: true)
|
|
|
+ private let cancelButton = DrawPrintSecondaryButton(title: "Cancel", symbolName: "xmark", compact: true, firesOnMouseDown: true)
|
|
|
+
|
|
|
+ override init(frame frameRect: NSRect) {
|
|
|
+ super.init(frame: frameRect)
|
|
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ setup()
|
|
|
+ refreshAppearance()
|
|
|
+ }
|
|
|
+
|
|
|
+ @available(*, unavailable)
|
|
|
+ required init?(coder: NSCoder) { nil }
|
|
|
+
|
|
|
+ func refreshAppearance() {
|
|
|
+ dimView.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.28).cgColor
|
|
|
+ cardView.layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
|
+ cardView.layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
+ titleLabel.textColor = AppTheme.textPrimary
|
|
|
+ inputTextView.backgroundColor = .clear
|
|
|
+ inputTextView.textColor = AppTheme.textPrimary
|
|
|
+ placeholderLabel.textColor = AppTheme.textSecondary
|
|
|
+ addButton.refreshAppearance()
|
|
|
+ cancelButton.refreshAppearance()
|
|
|
+ }
|
|
|
+
|
|
|
+ func prepareForPresentation() {
|
|
|
+ inputTextView.string = ""
|
|
|
+ placeholderLabel.isHidden = false
|
|
|
+ }
|
|
|
+
|
|
|
+ func focusInput() {
|
|
|
+ window?.makeFirstResponder(inputTextView)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func setup() {
|
|
|
+ wantsLayer = true
|
|
|
+
|
|
|
+ dimView.wantsLayer = true
|
|
|
+ dimView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ cardView.wantsLayer = true
|
|
|
+ cardView.layer?.cornerRadius = 24
|
|
|
+ cardView.layer?.borderWidth = 1
|
|
|
+ cardView.applyCardShadow()
|
|
|
+ cardView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ titleLabel.font = AppTheme.semiboldFont(size: 22)
|
|
|
+ titleLabel.alignment = .center
|
|
|
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ inputScrollView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ inputScrollView.drawsBackground = false
|
|
|
+ inputScrollView.hasVerticalScroller = true
|
|
|
+ inputScrollView.autohidesScrollers = true
|
|
|
+ inputScrollView.borderType = .noBorder
|
|
|
+
|
|
|
+ inputTextView.isRichText = false
|
|
|
+ inputTextView.font = AppTheme.regularFont(size: 20)
|
|
|
+ inputTextView.textContainerInset = NSSize(width: 16, height: 14)
|
|
|
+ inputTextView.isAutomaticQuoteSubstitutionEnabled = true
|
|
|
+ inputTextView.isAutomaticDashSubstitutionEnabled = true
|
|
|
+ inputTextView.isAutomaticTextReplacementEnabled = true
|
|
|
+ inputTextView.delegate = self
|
|
|
+ inputScrollView.documentView = inputTextView
|
|
|
+
|
|
|
+ placeholderLabel.font = AppTheme.regularFont(size: 18)
|
|
|
+ placeholderLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ buttonStack.orientation = .horizontal
|
|
|
+ buttonStack.spacing = 12
|
|
|
+ buttonStack.distribution = .fillEqually
|
|
|
+ buttonStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ addButton.onClick = { [weak self] in
|
|
|
+ self?.handleAdd()
|
|
|
+ }
|
|
|
+ cancelButton.onClick = { [weak self] in
|
|
|
+ self?.handleCancel()
|
|
|
+ }
|
|
|
+
|
|
|
+ addSubview(dimView)
|
|
|
+ addSubview(cardView)
|
|
|
+ cardView.addSubview(titleLabel)
|
|
|
+ cardView.addSubview(inputScrollView)
|
|
|
+ cardView.addSubview(placeholderLabel)
|
|
|
+ cardView.addSubview(buttonStack)
|
|
|
+ buttonStack.addArrangedSubview(addButton)
|
|
|
+ buttonStack.addArrangedSubview(cancelButton)
|
|
|
+
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
+ dimView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
|
+ dimView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
|
+ dimView.topAnchor.constraint(equalTo: topAnchor),
|
|
|
+ dimView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
|
+
|
|
|
+ cardView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
+ cardView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
+ cardView.widthAnchor.constraint(lessThanOrEqualToConstant: 560),
|
|
|
+ cardView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.62),
|
|
|
+ cardView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor, constant: 40),
|
|
|
+ cardView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -40),
|
|
|
+ cardView.heightAnchor.constraint(equalToConstant: 280),
|
|
|
+
|
|
|
+ titleLabel.topAnchor.constraint(equalTo: cardView.topAnchor, constant: 18),
|
|
|
+ titleLabel.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 24),
|
|
|
+ titleLabel.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -24),
|
|
|
+
|
|
|
+ inputScrollView.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 24),
|
|
|
+ inputScrollView.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -24),
|
|
|
+ inputScrollView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 14),
|
|
|
+ inputScrollView.bottomAnchor.constraint(equalTo: buttonStack.topAnchor, constant: -16),
|
|
|
+
|
|
|
+ placeholderLabel.leadingAnchor.constraint(equalTo: inputScrollView.leadingAnchor, constant: 6),
|
|
|
+ placeholderLabel.topAnchor.constraint(equalTo: inputScrollView.topAnchor, constant: 10),
|
|
|
+
|
|
|
+ buttonStack.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 24),
|
|
|
+ buttonStack.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -24),
|
|
|
+ buttonStack.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -20),
|
|
|
+ buttonStack.heightAnchor.constraint(equalToConstant: 44),
|
|
|
+ ])
|
|
|
+ }
|
|
|
+
|
|
|
+ private func handleAdd() {
|
|
|
+ let raw = inputTextView.textStorage?.string ?? inputTextView.string
|
|
|
+ let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
+ guard !text.isEmpty else { return }
|
|
|
+ onAdd?(text)
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc private func handleCancel() {
|
|
|
+ onCancel?()
|
|
|
+ }
|
|
|
+
|
|
|
+ func textDidChange(_ notification: Notification) {
|
|
|
+ placeholderLabel.isHidden = !inputTextView.string.isEmpty
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// MARK: - Controls
|
|
|
|
|
|
private final class DrawPrintToolbarButton: NSControl, AppearanceRefreshable {
|
|
|
@@ -1043,22 +1293,33 @@ private final class DrawPrintSecondaryButton: NSControl, AppearanceRefreshable {
|
|
|
private var hoverTracker: HoverTracker?
|
|
|
private var isHovered = false
|
|
|
|
|
|
- init(title: String, symbolName: String) {
|
|
|
+ private let firesOnMouseDown: Bool
|
|
|
+
|
|
|
+ init(title: String, symbolName: String, compact: Bool = false, firesOnMouseDown: Bool = false) {
|
|
|
self.title = title
|
|
|
+ self.firesOnMouseDown = firesOnMouseDown
|
|
|
super.init(frame: .zero)
|
|
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
wantsLayer = true
|
|
|
- layer?.cornerRadius = 22
|
|
|
+ let height: CGFloat = compact ? 44 : 52
|
|
|
+ let cornerRadius: CGFloat = compact ? 18 : 22
|
|
|
+ let fontSize: CGFloat = compact ? 14 : 16
|
|
|
+ let horizontalPadding: CGFloat = compact ? 18 : 24
|
|
|
+ let minWidth: CGFloat = compact ? 0 : 180
|
|
|
+ let iconSize: CGFloat = compact ? 14 : 18
|
|
|
+ let symbolPointSize: CGFloat = compact ? 12 : 14
|
|
|
+ layer?.cornerRadius = cornerRadius
|
|
|
layer?.borderWidth = 1.5
|
|
|
|
|
|
titleLabel.stringValue = title
|
|
|
- titleLabel.font = AppTheme.semiboldFont(size: 16)
|
|
|
+ titleLabel.font = AppTheme.semiboldFont(size: fontSize)
|
|
|
titleLabel.isBordered = false
|
|
|
titleLabel.isEditable = false
|
|
|
titleLabel.drawsBackground = false
|
|
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
|
|
|
- let config = NSImage.SymbolConfiguration(pointSize: 14, weight: .semibold)
|
|
|
+ let config = NSImage.SymbolConfiguration(pointSize: symbolPointSize, weight: .semibold)
|
|
|
iconView.image = image.withSymbolConfiguration(config)
|
|
|
}
|
|
|
iconView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -1066,17 +1327,20 @@ private final class DrawPrintSecondaryButton: NSControl, AppearanceRefreshable {
|
|
|
addSubview(titleLabel)
|
|
|
addSubview(iconView)
|
|
|
|
|
|
- NSLayoutConstraint.activate([
|
|
|
- heightAnchor.constraint(equalToConstant: 52),
|
|
|
- widthAnchor.constraint(greaterThanOrEqualToConstant: 180),
|
|
|
- titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24),
|
|
|
+ var constraints = [
|
|
|
+ heightAnchor.constraint(equalToConstant: height),
|
|
|
+ titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalPadding),
|
|
|
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
iconView.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8),
|
|
|
iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
- iconView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24),
|
|
|
- iconView.widthAnchor.constraint(equalToConstant: 18),
|
|
|
- iconView.heightAnchor.constraint(equalToConstant: 18),
|
|
|
- ])
|
|
|
+ iconView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalPadding),
|
|
|
+ iconView.widthAnchor.constraint(equalToConstant: iconSize),
|
|
|
+ iconView.heightAnchor.constraint(equalToConstant: iconSize),
|
|
|
+ ]
|
|
|
+ if minWidth > 0 {
|
|
|
+ constraints.append(widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth))
|
|
|
+ }
|
|
|
+ NSLayoutConstraint.activate(constraints)
|
|
|
|
|
|
hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
self?.setHovered(hovering)
|
|
|
@@ -1108,8 +1372,13 @@ private final class DrawPrintSecondaryButton: NSControl, AppearanceRefreshable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ override func mouseDown(with event: NSEvent) {
|
|
|
+ guard firesOnMouseDown, bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
+ onClick?()
|
|
|
+ }
|
|
|
+
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
|
- guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
+ guard !firesOnMouseDown, bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
onClick?()
|
|
|
}
|
|
|
|
|
|
@@ -1121,22 +1390,37 @@ private final class DrawPrintSecondaryButton: NSControl, AppearanceRefreshable {
|
|
|
private final class DrawPrintPrintButton: NSControl, AppearanceRefreshable {
|
|
|
var onClick: (() -> Void)?
|
|
|
|
|
|
- private let titleLabel = NSTextField(labelWithString: "Print Canvas")
|
|
|
+ private let titleLabel: NSTextField
|
|
|
private let iconView = NSImageView()
|
|
|
private var hoverTracker: HoverTracker?
|
|
|
private var isHovered = false
|
|
|
-
|
|
|
- init() {
|
|
|
+ private let firesOnMouseDown: Bool
|
|
|
+
|
|
|
+ init(
|
|
|
+ title: String = "Print Canvas",
|
|
|
+ symbolName: String = "printer.fill",
|
|
|
+ compact: Bool = false,
|
|
|
+ firesOnMouseDown: Bool = false
|
|
|
+ ) {
|
|
|
+ titleLabel = NSTextField(labelWithString: title)
|
|
|
+ self.firesOnMouseDown = firesOnMouseDown
|
|
|
super.init(frame: .zero)
|
|
|
+ translatesAutoresizingMaskIntoConstraints = false
|
|
|
wantsLayer = true
|
|
|
- layer?.cornerRadius = 22
|
|
|
-
|
|
|
- titleLabel.font = AppTheme.semiboldFont(size: 16)
|
|
|
+ let height: CGFloat = compact ? 44 : 52
|
|
|
+ let cornerRadius: CGFloat = compact ? 18 : 22
|
|
|
+ let fontSize: CGFloat = compact ? 14 : 16
|
|
|
+ let horizontalPadding: CGFloat = compact ? 20 : 28
|
|
|
+ let iconSize: CGFloat = compact ? 16 : 20
|
|
|
+ let symbolPointSize: CGFloat = compact ? 12 : 14
|
|
|
+ layer?.cornerRadius = cornerRadius
|
|
|
+
|
|
|
+ titleLabel.font = AppTheme.semiboldFont(size: fontSize)
|
|
|
titleLabel.textColor = .white
|
|
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
- if let image = NSImage(systemSymbolName: "printer.fill", accessibilityDescription: "Print") {
|
|
|
- let config = NSImage.SymbolConfiguration(pointSize: 14, weight: .semibold)
|
|
|
+ if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: title) {
|
|
|
+ let config = NSImage.SymbolConfiguration(pointSize: symbolPointSize, weight: .semibold)
|
|
|
iconView.image = image.withSymbolConfiguration(config)
|
|
|
}
|
|
|
iconView.contentTintColor = .white
|
|
|
@@ -1146,14 +1430,14 @@ private final class DrawPrintPrintButton: NSControl, AppearanceRefreshable {
|
|
|
addSubview(iconView)
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
- heightAnchor.constraint(equalToConstant: 52),
|
|
|
- titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 28),
|
|
|
+ heightAnchor.constraint(equalToConstant: height),
|
|
|
+ titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: horizontalPadding),
|
|
|
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
iconView.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 10),
|
|
|
iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
- iconView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -28),
|
|
|
- iconView.widthAnchor.constraint(equalToConstant: 20),
|
|
|
- iconView.heightAnchor.constraint(equalToConstant: 20),
|
|
|
+ iconView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -horizontalPadding),
|
|
|
+ iconView.widthAnchor.constraint(equalToConstant: iconSize),
|
|
|
+ iconView.heightAnchor.constraint(equalToConstant: iconSize),
|
|
|
])
|
|
|
|
|
|
hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
@@ -1187,8 +1471,13 @@ private final class DrawPrintPrintButton: NSControl, AppearanceRefreshable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ override func mouseDown(with event: NSEvent) {
|
|
|
+ guard firesOnMouseDown, bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
+ onClick?()
|
|
|
+ }
|
|
|
+
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
|
- guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
+ guard !firesOnMouseDown, bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
onClick?()
|
|
|
}
|
|
|
|
|
|
@@ -1259,7 +1548,7 @@ private final class CanvasToolButton: NSControl, AppearanceRefreshable {
|
|
|
self.tool = tool
|
|
|
super.init(frame: .zero)
|
|
|
wantsLayer = true
|
|
|
- layer?.cornerRadius = 14
|
|
|
+ layer?.cornerRadius = 12
|
|
|
|
|
|
let symbol: String
|
|
|
switch tool {
|
|
|
@@ -1275,8 +1564,8 @@ private final class CanvasToolButton: NSControl, AppearanceRefreshable {
|
|
|
addSubview(iconView)
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
- widthAnchor.constraint(equalToConstant: 44),
|
|
|
- heightAnchor.constraint(equalToConstant: 44),
|
|
|
+ widthAnchor.constraint(equalToConstant: 38),
|
|
|
+ heightAnchor.constraint(equalToConstant: 38),
|
|
|
iconView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
])
|
|
|
@@ -1324,7 +1613,7 @@ private final class CanvasSubToolButton: NSControl, AppearanceRefreshable {
|
|
|
init(symbolName: String) {
|
|
|
super.init(frame: .zero)
|
|
|
wantsLayer = true
|
|
|
- layer?.cornerRadius = 16
|
|
|
+ layer?.cornerRadius = 14
|
|
|
|
|
|
if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) {
|
|
|
let config = NSImage.SymbolConfiguration(pointSize: 13, weight: .medium)
|
|
|
@@ -1334,8 +1623,8 @@ private final class CanvasSubToolButton: NSControl, AppearanceRefreshable {
|
|
|
addSubview(iconView)
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
- widthAnchor.constraint(equalToConstant: 32),
|
|
|
- heightAnchor.constraint(equalToConstant: 32),
|
|
|
+ widthAnchor.constraint(equalToConstant: 28),
|
|
|
+ heightAnchor.constraint(equalToConstant: 28),
|
|
|
iconView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
])
|
|
|
@@ -1371,13 +1660,13 @@ private final class CanvasColorButton: NSControl, AppearanceRefreshable {
|
|
|
self.swatchColor = color
|
|
|
super.init(frame: .zero)
|
|
|
wantsLayer = true
|
|
|
- layer?.cornerRadius = 14
|
|
|
+ layer?.cornerRadius = 12
|
|
|
layer?.borderWidth = 2
|
|
|
layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
|
- widthAnchor.constraint(equalToConstant: 28),
|
|
|
- heightAnchor.constraint(equalToConstant: 28),
|
|
|
+ widthAnchor.constraint(equalToConstant: 24),
|
|
|
+ heightAnchor.constraint(equalToConstant: 24),
|
|
|
])
|
|
|
refreshAppearance()
|
|
|
}
|