|
|
@@ -24,15 +24,20 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
private let sharedHeader = ScannerSectionHeader(title: "SHARED")
|
|
|
private let sharedList = NSStackView()
|
|
|
private let previewContainer = NSView()
|
|
|
- private let previewImageView = NSImageView()
|
|
|
+ private let previewImageArea = NSView()
|
|
|
+ private let previewImageView = ScanPreviewImageView()
|
|
|
private let placeholderView = ScannerPlaceholderView()
|
|
|
+ private let previewActionRow = NSStackView()
|
|
|
+ private var previewImageAreaBottomToContainer: NSLayoutConstraint!
|
|
|
+ private var previewImageAreaBottomToActions: NSLayoutConstraint!
|
|
|
+ private var previewActionRowHeight: NSLayoutConstraint!
|
|
|
private let statusLabel = NSTextField(labelWithString: "Select a scanner to begin")
|
|
|
private let paperSizePopup = NSPopUpButton()
|
|
|
private let scanButton = ScannerPrimaryButton(title: "Scan")
|
|
|
private let progressIndicator = NSProgressIndicator()
|
|
|
|
|
|
private let resultPreviewContainer = NSView()
|
|
|
- private let resultImageView = NSImageView()
|
|
|
+ private let resultImageView = ScanPreviewImageView()
|
|
|
private let savePDFButton = ScannerSecondaryButton(title: "Save as PDF", symbolName: "square.and.arrow.down")
|
|
|
private let printButton = ScannerSecondaryButton(title: "Print", symbolName: "printer.fill")
|
|
|
|
|
|
@@ -79,12 +84,15 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
|
|
|
func showResult(_ image: NSImage) {
|
|
|
hasResult = true
|
|
|
- scannedImage = image
|
|
|
- resultImageView.image = image
|
|
|
+ displayScannedImage(image)
|
|
|
scanPhaseView.isHidden = true
|
|
|
resultPhaseView.isHidden = false
|
|
|
}
|
|
|
|
|
|
+ func updateScanPreview(_ image: NSImage) {
|
|
|
+ displayScannedImage(image)
|
|
|
+ }
|
|
|
+
|
|
|
func present(in parent: NSView) {
|
|
|
guard superview == nil else { return }
|
|
|
parent.addSubview(self)
|
|
|
@@ -120,7 +128,8 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
func refreshAppearance() {
|
|
|
layer?.backgroundColor = AppTheme.background.cgColor
|
|
|
titleLabel.textColor = AppTheme.textPrimary
|
|
|
- sidebarView.layer?.backgroundColor = AppTheme.sidebarBackground.cgColor
|
|
|
+ sidebarView.layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
|
+ sidebarView.layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
previewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
|
previewContainer.layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
resultPreviewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
|
@@ -170,6 +179,9 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
titleLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
sidebarView.wantsLayer = true
|
|
|
+ sidebarView.layer?.cornerRadius = AppTheme.contentPanelCornerRadius
|
|
|
+ sidebarView.layer?.borderWidth = 1.5
|
|
|
+ sidebarView.layer?.masksToBounds = true
|
|
|
sidebarView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
localList.orientation = .vertical
|
|
|
@@ -192,6 +204,8 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
previewContainer.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
previewContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
|
|
|
|
+ previewImageArea.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
previewImageView.imageScaling = .scaleProportionallyUpOrDown
|
|
|
previewImageView.imageAlignment = .alignCenter
|
|
|
previewImageView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -230,15 +244,29 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
progressIndicator.isDisplayedWhenStopped = false
|
|
|
progressIndicator.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
+ savePDFButton.onClick = { [weak self] in self?.saveAsPDF() }
|
|
|
+ printButton.onClick = { [weak self] in self?.printScan() }
|
|
|
+ savePDFButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ printButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+
|
|
|
+ previewActionRow.orientation = .horizontal
|
|
|
+ previewActionRow.spacing = 12
|
|
|
+ previewActionRow.distribution = .fillEqually
|
|
|
+ previewActionRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ previewActionRow.isHidden = true
|
|
|
+ previewActionRow.addArrangedSubview(savePDFButton)
|
|
|
+ previewActionRow.addArrangedSubview(printButton)
|
|
|
+
|
|
|
let sidebarStack = NSStackView()
|
|
|
sidebarStack.orientation = .vertical
|
|
|
sidebarStack.alignment = .leading
|
|
|
- sidebarStack.spacing = 6
|
|
|
+ sidebarStack.spacing = 8
|
|
|
sidebarStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
sidebarStack.addArrangedSubview(localHeader)
|
|
|
sidebarStack.addArrangedSubview(localList)
|
|
|
sidebarStack.addArrangedSubview(sharedHeader)
|
|
|
sidebarStack.addArrangedSubview(sharedList)
|
|
|
+ sidebarStack.setCustomSpacing(18, after: localList)
|
|
|
|
|
|
let sidebarScroll = NSScrollView()
|
|
|
sidebarScroll.hasVerticalScroller = true
|
|
|
@@ -254,8 +282,10 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
sidebarScroll.documentView = sidebarDocument
|
|
|
sidebarView.addSubview(sidebarScroll)
|
|
|
|
|
|
- previewContainer.addSubview(placeholderView)
|
|
|
- previewContainer.addSubview(previewImageView)
|
|
|
+ previewImageArea.addSubview(previewImageView)
|
|
|
+ previewImageArea.addSubview(placeholderView)
|
|
|
+ previewContainer.addSubview(previewImageArea)
|
|
|
+ previewContainer.addSubview(previewActionRow)
|
|
|
|
|
|
scanPhaseView.addSubview(backButton)
|
|
|
scanPhaseView.addSubview(titleLabel)
|
|
|
@@ -275,20 +305,20 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
titleLabel.centerXAnchor.constraint(equalTo: scanPhaseView.centerXAnchor),
|
|
|
titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
|
|
|
|
|
|
- sidebarView.leadingAnchor.constraint(equalTo: scanPhaseView.leadingAnchor),
|
|
|
+ sidebarView.leadingAnchor.constraint(equalTo: scanPhaseView.leadingAnchor, constant: AppTheme.contentPanelInset),
|
|
|
sidebarView.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 16),
|
|
|
- sidebarView.bottomAnchor.constraint(equalTo: scanPhaseView.bottomAnchor),
|
|
|
- sidebarView.widthAnchor.constraint(equalToConstant: 168),
|
|
|
-
|
|
|
- sidebarScroll.leadingAnchor.constraint(equalTo: sidebarView.leadingAnchor),
|
|
|
- sidebarScroll.trailingAnchor.constraint(equalTo: sidebarView.trailingAnchor),
|
|
|
- sidebarScroll.topAnchor.constraint(equalTo: sidebarView.topAnchor),
|
|
|
- sidebarScroll.bottomAnchor.constraint(equalTo: sidebarView.bottomAnchor),
|
|
|
-
|
|
|
- sidebarStack.leadingAnchor.constraint(equalTo: sidebarDocument.leadingAnchor, constant: 8),
|
|
|
- sidebarStack.trailingAnchor.constraint(equalTo: sidebarDocument.trailingAnchor, constant: -8),
|
|
|
- sidebarStack.topAnchor.constraint(equalTo: sidebarDocument.topAnchor, constant: 8),
|
|
|
- sidebarStack.bottomAnchor.constraint(equalTo: sidebarDocument.bottomAnchor, constant: -8),
|
|
|
+ sidebarView.bottomAnchor.constraint(equalTo: previewContainer.bottomAnchor),
|
|
|
+ sidebarView.widthAnchor.constraint(equalToConstant: 180),
|
|
|
+
|
|
|
+ sidebarScroll.leadingAnchor.constraint(equalTo: sidebarView.leadingAnchor, constant: 4),
|
|
|
+ sidebarScroll.trailingAnchor.constraint(equalTo: sidebarView.trailingAnchor, constant: -4),
|
|
|
+ sidebarScroll.topAnchor.constraint(equalTo: sidebarView.topAnchor, constant: 6),
|
|
|
+ sidebarScroll.bottomAnchor.constraint(equalTo: sidebarView.bottomAnchor, constant: -4),
|
|
|
+
|
|
|
+ sidebarStack.leadingAnchor.constraint(equalTo: sidebarDocument.leadingAnchor, constant: 14),
|
|
|
+ sidebarStack.trailingAnchor.constraint(equalTo: sidebarDocument.trailingAnchor, constant: -14),
|
|
|
+ sidebarStack.topAnchor.constraint(equalTo: sidebarDocument.topAnchor, constant: 22),
|
|
|
+ sidebarStack.bottomAnchor.constraint(equalTo: sidebarDocument.bottomAnchor, constant: -14),
|
|
|
sidebarDocument.widthAnchor.constraint(equalTo: sidebarScroll.widthAnchor),
|
|
|
localHeader.widthAnchor.constraint(equalTo: sidebarStack.widthAnchor),
|
|
|
sharedHeader.widthAnchor.constraint(equalTo: sidebarStack.widthAnchor),
|
|
|
@@ -300,15 +330,25 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
previewContainer.topAnchor.constraint(equalTo: sidebarView.topAnchor),
|
|
|
previewContainer.bottomAnchor.constraint(equalTo: statusLabel.topAnchor, constant: -12),
|
|
|
|
|
|
- placeholderView.centerXAnchor.constraint(equalTo: previewContainer.centerXAnchor),
|
|
|
- placeholderView.centerYAnchor.constraint(equalTo: previewContainer.centerYAnchor),
|
|
|
- placeholderView.widthAnchor.constraint(equalTo: previewContainer.widthAnchor, multiplier: 0.55),
|
|
|
- placeholderView.heightAnchor.constraint(equalTo: previewContainer.heightAnchor, multiplier: 0.7),
|
|
|
+ previewImageArea.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor, constant: 16),
|
|
|
+ previewImageArea.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor, constant: -16),
|
|
|
+ previewImageArea.topAnchor.constraint(equalTo: previewContainer.topAnchor, constant: 16),
|
|
|
|
|
|
- previewImageView.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor, constant: 12),
|
|
|
- previewImageView.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor, constant: -12),
|
|
|
- previewImageView.topAnchor.constraint(equalTo: previewContainer.topAnchor, constant: 12),
|
|
|
- previewImageView.bottomAnchor.constraint(equalTo: previewContainer.bottomAnchor, constant: -12),
|
|
|
+ previewActionRow.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor, constant: 16),
|
|
|
+ previewActionRow.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor, constant: -16),
|
|
|
+ previewActionRow.bottomAnchor.constraint(equalTo: previewContainer.bottomAnchor, constant: -16),
|
|
|
+ savePDFButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
+ printButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
+
|
|
|
+ placeholderView.centerXAnchor.constraint(equalTo: previewImageArea.centerXAnchor),
|
|
|
+ placeholderView.centerYAnchor.constraint(equalTo: previewImageArea.centerYAnchor),
|
|
|
+ placeholderView.widthAnchor.constraint(equalTo: previewImageArea.widthAnchor, multiplier: 0.42),
|
|
|
+ placeholderView.heightAnchor.constraint(equalTo: placeholderView.widthAnchor),
|
|
|
+
|
|
|
+ previewImageView.leadingAnchor.constraint(equalTo: previewImageArea.leadingAnchor, constant: 20),
|
|
|
+ previewImageView.trailingAnchor.constraint(equalTo: previewImageArea.trailingAnchor, constant: -20),
|
|
|
+ previewImageView.topAnchor.constraint(equalTo: previewImageArea.topAnchor, constant: 20),
|
|
|
+ previewImageView.bottomAnchor.constraint(equalTo: previewImageArea.bottomAnchor, constant: -20),
|
|
|
|
|
|
statusLabel.leadingAnchor.constraint(equalTo: previewContainer.leadingAnchor),
|
|
|
statusLabel.trailingAnchor.constraint(equalTo: previewContainer.trailingAnchor),
|
|
|
@@ -326,6 +366,20 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
scanButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 96),
|
|
|
scanButton.heightAnchor.constraint(equalToConstant: 32),
|
|
|
])
|
|
|
+
|
|
|
+ previewImageAreaBottomToContainer = previewImageArea.bottomAnchor.constraint(
|
|
|
+ equalTo: previewContainer.bottomAnchor,
|
|
|
+ constant: -16
|
|
|
+ )
|
|
|
+ previewImageAreaBottomToActions = previewImageArea.bottomAnchor.constraint(
|
|
|
+ equalTo: previewActionRow.topAnchor,
|
|
|
+ constant: -12
|
|
|
+ )
|
|
|
+ previewActionRowHeight = previewActionRow.heightAnchor.constraint(equalToConstant: 0)
|
|
|
+
|
|
|
+ previewImageAreaBottomToContainer.isActive = true
|
|
|
+ previewImageAreaBottomToActions.isActive = false
|
|
|
+ previewActionRowHeight.isActive = true
|
|
|
}
|
|
|
|
|
|
private func setupResultPhase() {
|
|
|
@@ -359,10 +413,12 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
|
|
|
|
- savePDFButton.onClick = { [weak self] in self?.saveAsPDF() }
|
|
|
- printButton.onClick = { [weak self] in self?.printScan() }
|
|
|
+ let resultSavePDFButton = ScannerSecondaryButton(title: "Save as PDF", symbolName: "square.and.arrow.down")
|
|
|
+ let resultPrintButton = ScannerSecondaryButton(title: "Print", symbolName: "printer.fill")
|
|
|
+ resultSavePDFButton.onClick = { [weak self] in self?.saveAsPDF() }
|
|
|
+ resultPrintButton.onClick = { [weak self] in self?.printScan() }
|
|
|
|
|
|
- let actionRow = NSStackView(views: [savePDFButton, printButton])
|
|
|
+ let actionRow = NSStackView(views: [resultSavePDFButton, resultPrintButton])
|
|
|
actionRow.orientation = .horizontal
|
|
|
actionRow.spacing = 12
|
|
|
actionRow.distribution = .fillEqually
|
|
|
@@ -398,8 +454,8 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
actionRow.leadingAnchor.constraint(equalTo: resultPhaseView.leadingAnchor, constant: AppTheme.contentPanelInset),
|
|
|
actionRow.trailingAnchor.constraint(equalTo: resultPhaseView.trailingAnchor, constant: -AppTheme.contentPanelInset),
|
|
|
actionRow.bottomAnchor.constraint(equalTo: resultPhaseView.bottomAnchor, constant: -28),
|
|
|
- savePDFButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
- printButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
+ resultSavePDFButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
+ resultPrintButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
])
|
|
|
}
|
|
|
|
|
|
@@ -453,17 +509,19 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
let label = NSTextField(labelWithString: text)
|
|
|
label.font = AppTheme.regularFont(size: 12)
|
|
|
label.textColor = AppTheme.textSecondary
|
|
|
- label.lineBreakMode = .byTruncatingTail
|
|
|
+ label.alignment = .center
|
|
|
+ label.lineBreakMode = .byWordWrapping
|
|
|
+ label.maximumNumberOfLines = 2
|
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
let container = NSView()
|
|
|
container.translatesAutoresizingMaskIntoConstraints = false
|
|
|
container.addSubview(label)
|
|
|
NSLayoutConstraint.activate([
|
|
|
- label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 4),
|
|
|
- label.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -4),
|
|
|
- label.topAnchor.constraint(equalTo: container.topAnchor, constant: 4),
|
|
|
- label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -4),
|
|
|
- container.heightAnchor.constraint(equalToConstant: 28),
|
|
|
+ label.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
|
+ label.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
|
+ label.topAnchor.constraint(equalTo: container.topAnchor, constant: 6),
|
|
|
+ label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -6),
|
|
|
+ container.heightAnchor.constraint(greaterThanOrEqualToConstant: 32),
|
|
|
])
|
|
|
return container
|
|
|
}
|
|
|
@@ -507,6 +565,7 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
guard scanner.selectedDevice != nil else { return }
|
|
|
previewImageView.isHidden = true
|
|
|
placeholderView.isHidden = false
|
|
|
+ setPreviewActionsVisible(false)
|
|
|
|
|
|
if case .ready = scanner.state {
|
|
|
scanner.scan()
|
|
|
@@ -515,10 +574,34 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private func setPreviewActionsVisible(_ visible: Bool) {
|
|
|
+ previewActionRow.isHidden = !visible
|
|
|
+ previewActionRowHeight.constant = visible ? 40 : 0
|
|
|
+ previewImageAreaBottomToContainer.isActive = !visible
|
|
|
+ previewImageAreaBottomToActions.isActive = visible
|
|
|
+ previewContainer.layoutSubtreeIfNeeded()
|
|
|
+ }
|
|
|
+
|
|
|
+ private func displayScannedImage(_ image: NSImage) {
|
|
|
+ let displayImage = image.normalizedForDisplay()
|
|
|
+ hasResult = true
|
|
|
+ scannedImage = displayImage
|
|
|
+ previewImageView.image = displayImage
|
|
|
+ previewImageView.isHidden = false
|
|
|
+ placeholderView.isHidden = true
|
|
|
+ setPreviewActionsVisible(true)
|
|
|
+ resultImageView.image = displayImage
|
|
|
+ previewImageView.invalidateIntrinsicContentSize()
|
|
|
+ resultImageView.invalidateIntrinsicContentSize()
|
|
|
+ previewContainer.layoutSubtreeIfNeeded()
|
|
|
+ resultPreviewContainer.layoutSubtreeIfNeeded()
|
|
|
+ }
|
|
|
+
|
|
|
private func handleScanResult(_ result: Result<NSImage, Error>) {
|
|
|
progressIndicator.stopAnimation(nil)
|
|
|
switch result {
|
|
|
case .success(let image):
|
|
|
+ displayScannedImage(image)
|
|
|
onScanComplete?(image)
|
|
|
case .failure(let error):
|
|
|
if case ScanError.cancelled = error { return }
|
|
|
@@ -708,8 +791,8 @@ private final class ScannerPlaceholderView: NSView, AppearanceRefreshable {
|
|
|
}
|
|
|
|
|
|
override func draw(_ dirtyRect: NSRect) {
|
|
|
- let rect = bounds.insetBy(dx: 20, dy: 20)
|
|
|
- let path = NSBezierPath(roundedRect: rect, xRadius: 4, yRadius: 4)
|
|
|
+ let rect = bounds.insetBy(dx: 1, dy: 1)
|
|
|
+ let path = NSBezierPath(roundedRect: rect, xRadius: 16, yRadius: 16)
|
|
|
path.setLineDash([6, 4], count: 2, phase: 0)
|
|
|
AppTheme.border.setStroke()
|
|
|
path.lineWidth = 1.5
|
|
|
@@ -777,6 +860,8 @@ private final class ScannerSecondaryButton: NSControl, AppearanceRefreshable {
|
|
|
|
|
|
private let titleLabel = NSTextField(labelWithString: "")
|
|
|
private let iconView = NSImageView()
|
|
|
+ private var hoverTracker: HoverTracker?
|
|
|
+ private var isHovered = false
|
|
|
|
|
|
init(title: String, symbolName: String) {
|
|
|
super.init(frame: .zero)
|
|
|
@@ -804,6 +889,10 @@ private final class ScannerSecondaryButton: NSControl, AppearanceRefreshable {
|
|
|
titleLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor, constant: -16),
|
|
|
])
|
|
|
+
|
|
|
+ hoverTracker = HoverTracker(view: self) { [weak self] hovering in
|
|
|
+ self?.setHovered(hovering)
|
|
|
+ }
|
|
|
refreshAppearance()
|
|
|
}
|
|
|
|
|
|
@@ -811,12 +900,19 @@ private final class ScannerSecondaryButton: NSControl, AppearanceRefreshable {
|
|
|
required init?(coder: NSCoder) { nil }
|
|
|
|
|
|
func refreshAppearance() {
|
|
|
- layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
|
|
|
+ layer?.backgroundColor = (isHovered
|
|
|
+ ? AppTheme.elevatedBackground.highlight(withLevel: 0.05) ?? AppTheme.elevatedBackground
|
|
|
+ : AppTheme.elevatedBackground).cgColor
|
|
|
layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
titleLabel.textColor = AppTheme.textPrimary
|
|
|
iconView.contentTintColor = AppTheme.textPrimary
|
|
|
}
|
|
|
|
|
|
+ private func setHovered(_ hovering: Bool) {
|
|
|
+ isHovered = hovering
|
|
|
+ refreshAppearance()
|
|
|
+ }
|
|
|
+
|
|
|
override func mouseUp(with event: NSEvent) {
|
|
|
guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
onClick?()
|