|
@@ -1,173 +1,6 @@
|
|
|
import Cocoa
|
|
import Cocoa
|
|
|
import ImageCaptureCore
|
|
import ImageCaptureCore
|
|
|
|
|
|
|
|
-// MARK: - Scanner Panel
|
|
|
|
|
-
|
|
|
|
|
-@MainActor
|
|
|
|
|
-final class ScannerPanelController: NSObject, NSWindowDelegate {
|
|
|
|
|
- private static var activeController: ScannerPanelController?
|
|
|
|
|
-
|
|
|
|
|
- private static let panelWidth: CGFloat = 560
|
|
|
|
|
- private static let panelHeight: CGFloat = 480
|
|
|
|
|
-
|
|
|
|
|
- private var panel: NSPanel?
|
|
|
|
|
- private var panelView: ScannerOverlayView?
|
|
|
|
|
- private weak var parentWindow: NSWindow?
|
|
|
|
|
- private let showResultPreview: Bool
|
|
|
|
|
- private var onComplete: ((Result<NSImage, Error>) -> Void)?
|
|
|
|
|
- private var didComplete = false
|
|
|
|
|
-
|
|
|
|
|
- static func present(
|
|
|
|
|
- from parentWindow: NSWindow?,
|
|
|
|
|
- showResultPreview: Bool,
|
|
|
|
|
- onComplete: @escaping (Result<NSImage, Error>) -> Void
|
|
|
|
|
- ) {
|
|
|
|
|
- Task { @MainActor in
|
|
|
|
|
- if let active = activeController {
|
|
|
|
|
- active.teardownPanel()
|
|
|
|
|
- }
|
|
|
|
|
- let controller = ScannerPanelController(
|
|
|
|
|
- parentWindow: parentWindow,
|
|
|
|
|
- showResultPreview: showResultPreview,
|
|
|
|
|
- onComplete: onComplete
|
|
|
|
|
- )
|
|
|
|
|
- activeController = controller
|
|
|
|
|
- await controller.show()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private init(
|
|
|
|
|
- parentWindow: NSWindow?,
|
|
|
|
|
- showResultPreview: Bool,
|
|
|
|
|
- onComplete: @escaping (Result<NSImage, Error>) -> Void
|
|
|
|
|
- ) {
|
|
|
|
|
- self.parentWindow = parentWindow
|
|
|
|
|
- self.showResultPreview = showResultPreview
|
|
|
|
|
- self.onComplete = onComplete
|
|
|
|
|
- super.init()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func show() async {
|
|
|
|
|
- let contentSize = NSSize(width: Self.panelWidth, height: Self.panelHeight)
|
|
|
|
|
- let panel = NSPanel(
|
|
|
|
|
- contentRect: NSRect(origin: .zero, size: contentSize),
|
|
|
|
|
- styleMask: [.titled, .closable, .fullSizeContentView],
|
|
|
|
|
- backing: .buffered,
|
|
|
|
|
- defer: false
|
|
|
|
|
- )
|
|
|
|
|
- panel.title = "Scanner"
|
|
|
|
|
- panel.isFloatingPanel = false
|
|
|
|
|
- panel.hidesOnDeactivate = false
|
|
|
|
|
- panel.delegate = self
|
|
|
|
|
- panel.isReleasedWhenClosed = false
|
|
|
|
|
- panel.backgroundColor = AppTheme.background
|
|
|
|
|
- hideTrafficLights(on: panel)
|
|
|
|
|
-
|
|
|
|
|
- let container = NSView(frame: NSRect(origin: .zero, size: contentSize))
|
|
|
|
|
- container.translatesAutoresizingMaskIntoConstraints = true
|
|
|
|
|
- panel.contentView = container
|
|
|
|
|
-
|
|
|
|
|
- let scannerView = ScannerOverlayView()
|
|
|
|
|
- scannerView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- scannerView.onScanComplete = { [weak self] image in
|
|
|
|
|
- self?.handleScanComplete(image)
|
|
|
|
|
- }
|
|
|
|
|
- scannerView.onDismiss = { [weak self] in
|
|
|
|
|
- self?.close()
|
|
|
|
|
- }
|
|
|
|
|
- container.addSubview(scannerView)
|
|
|
|
|
- NSLayoutConstraint.activate([
|
|
|
|
|
- scannerView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
|
|
|
|
|
- scannerView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
|
|
|
|
- scannerView.topAnchor.constraint(equalTo: container.topAnchor),
|
|
|
|
|
- scannerView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
|
|
|
|
- ])
|
|
|
|
|
-
|
|
|
|
|
- center(panel, relativeTo: parentWindow)
|
|
|
|
|
- self.panel = panel
|
|
|
|
|
- self.panelView = scannerView
|
|
|
|
|
-
|
|
|
|
|
- await scannerView.activate()
|
|
|
|
|
- NSApp.activate(ignoringOtherApps: true)
|
|
|
|
|
- parentWindow?.makeKeyAndOrderFront(nil)
|
|
|
|
|
- panel.makeKeyAndOrderFront(nil)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func teardownPanel() {
|
|
|
|
|
- panelView?.teardown()
|
|
|
|
|
- panel?.orderOut(nil)
|
|
|
|
|
- panel = nil
|
|
|
|
|
- panelView = nil
|
|
|
|
|
- if Self.activeController === self {
|
|
|
|
|
- Self.activeController = nil
|
|
|
|
|
- }
|
|
|
|
|
- onComplete = nil
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func handleScanComplete(_ image: NSImage) {
|
|
|
|
|
- Task { @MainActor in
|
|
|
|
|
- let processed: NSImage
|
|
|
|
|
- if let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) {
|
|
|
|
|
- processed = await ScanDocumentProcessor.process(cgImage) ?? image
|
|
|
|
|
- } else {
|
|
|
|
|
- processed = image
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if showResultPreview {
|
|
|
|
|
- panel?.title = "Scan Result"
|
|
|
|
|
- panelView?.showResult(processed)
|
|
|
|
|
- } else {
|
|
|
|
|
- didComplete = true
|
|
|
|
|
- onComplete?(.success(processed))
|
|
|
|
|
- close()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- func windowWillClose(_ notification: Notification) {
|
|
|
|
|
- let cancelled = !didComplete && panelView?.hasResult != true
|
|
|
|
|
- Task { @MainActor in
|
|
|
|
|
- finish(cancelled: cancelled)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func finish(cancelled: Bool) {
|
|
|
|
|
- panelView?.teardown()
|
|
|
|
|
- panel?.orderOut(nil)
|
|
|
|
|
- panel = nil
|
|
|
|
|
- panelView = nil
|
|
|
|
|
- if Self.activeController === self {
|
|
|
|
|
- Self.activeController = nil
|
|
|
|
|
- }
|
|
|
|
|
- if cancelled {
|
|
|
|
|
- onComplete?(.failure(ScanError.cancelled))
|
|
|
|
|
- }
|
|
|
|
|
- onComplete = nil
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func close() {
|
|
|
|
|
- panel?.close()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func center(_ panelWindow: NSWindow, relativeTo parentWindow: NSWindow?) {
|
|
|
|
|
- guard let parentWindow else {
|
|
|
|
|
- panelWindow.center()
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
- let parentFrame = parentWindow.frame
|
|
|
|
|
- var panelFrame = panelWindow.frame
|
|
|
|
|
- panelFrame.origin.x = parentFrame.midX - panelFrame.width / 2
|
|
|
|
|
- panelFrame.origin.y = parentFrame.midY - panelFrame.height / 2
|
|
|
|
|
- panelWindow.setFrame(panelFrame, display: true)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private func hideTrafficLights(on window: NSWindow) {
|
|
|
|
|
- for buttonType: NSWindow.ButtonType in [.miniaturizeButton, .zoomButton] {
|
|
|
|
|
- window.standardWindowButton(buttonType)?.isHidden = true
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// MARK: - Scanner View
|
|
// MARK: - Scanner View
|
|
|
|
|
|
|
|
final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
@@ -183,6 +16,7 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
private let scanPhaseView = NSView()
|
|
private let scanPhaseView = NSView()
|
|
|
private let resultPhaseView = NSView()
|
|
private let resultPhaseView = NSView()
|
|
|
|
|
|
|
|
|
|
+ private let backButton = ScannerToolbarButton(symbolName: "chevron.left", accessibilityLabel: "Back")
|
|
|
private let titleLabel = NSTextField(labelWithString: "Scanner")
|
|
private let titleLabel = NSTextField(labelWithString: "Scanner")
|
|
|
private let sidebarView = NSView()
|
|
private let sidebarView = NSView()
|
|
|
private let localHeader = ScannerSectionHeader(title: "DEVICES")
|
|
private let localHeader = ScannerSectionHeader(title: "DEVICES")
|
|
@@ -251,6 +85,38 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
resultPhaseView.isHidden = false
|
|
resultPhaseView.isHidden = false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ func present(in parent: NSView) {
|
|
|
|
|
+ guard superview == nil else { return }
|
|
|
|
|
+ parent.addSubview(self)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ leadingAnchor.constraint(equalTo: parent.leadingAnchor),
|
|
|
|
|
+ trailingAnchor.constraint(equalTo: parent.trailingAnchor),
|
|
|
|
|
+ topAnchor.constraint(equalTo: parent.topAnchor),
|
|
|
|
|
+ bottomAnchor.constraint(equalTo: parent.bottomAnchor),
|
|
|
|
|
+ ])
|
|
|
|
|
+ alphaValue = 0
|
|
|
|
|
+ NSAnimationContext.runAnimationGroup { context in
|
|
|
|
|
+ context.duration = 0.2
|
|
|
|
|
+ animator().alphaValue = 1
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ func dismiss(animated: Bool = true) {
|
|
|
|
|
+ let remove = { [weak self] in
|
|
|
|
|
+ self?.teardown()
|
|
|
|
|
+ self?.removeFromSuperview()
|
|
|
|
|
+ self?.onDismiss?()
|
|
|
|
|
+ }
|
|
|
|
|
+ guard animated else {
|
|
|
|
|
+ remove()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ NSAnimationContext.runAnimationGroup({ context in
|
|
|
|
|
+ context.duration = 0.15
|
|
|
|
|
+ animator().alphaValue = 0
|
|
|
|
|
+ }, completionHandler: remove)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
func refreshAppearance() {
|
|
func refreshAppearance() {
|
|
|
layer?.backgroundColor = AppTheme.background.cgColor
|
|
layer?.backgroundColor = AppTheme.background.cgColor
|
|
|
titleLabel.textColor = AppTheme.textPrimary
|
|
titleLabel.textColor = AppTheme.textPrimary
|
|
@@ -260,6 +126,7 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
resultPreviewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
resultPreviewContainer.layer?.backgroundColor = AppTheme.cardBackground.cgColor
|
|
|
resultPreviewContainer.layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
resultPreviewContainer.layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
statusLabel.textColor = AppTheme.textSecondary
|
|
statusLabel.textColor = AppTheme.textSecondary
|
|
|
|
|
+ backButton.refreshAppearance()
|
|
|
scanButton.refreshAppearance()
|
|
scanButton.refreshAppearance()
|
|
|
localHeader.refreshAppearance()
|
|
localHeader.refreshAppearance()
|
|
|
sharedHeader.refreshAppearance()
|
|
sharedHeader.refreshAppearance()
|
|
@@ -351,6 +218,9 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
paperSizePopup.target = self
|
|
paperSizePopup.target = self
|
|
|
paperSizePopup.action = #selector(paperSizeChanged)
|
|
paperSizePopup.action = #selector(paperSizeChanged)
|
|
|
|
|
|
|
|
|
|
+ backButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ backButton.onClick = { [weak self] in self?.dismiss() }
|
|
|
|
|
+
|
|
|
scanButton.translatesAutoresizingMaskIntoConstraints = false
|
|
scanButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
scanButton.isEnabled = false
|
|
scanButton.isEnabled = false
|
|
|
scanButton.onClick = { [weak self] in self?.startScan() }
|
|
scanButton.onClick = { [weak self] in self?.startScan() }
|
|
@@ -387,6 +257,7 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
previewContainer.addSubview(placeholderView)
|
|
previewContainer.addSubview(placeholderView)
|
|
|
previewContainer.addSubview(previewImageView)
|
|
previewContainer.addSubview(previewImageView)
|
|
|
|
|
|
|
|
|
|
+ scanPhaseView.addSubview(backButton)
|
|
|
scanPhaseView.addSubview(titleLabel)
|
|
scanPhaseView.addSubview(titleLabel)
|
|
|
scanPhaseView.addSubview(sidebarView)
|
|
scanPhaseView.addSubview(sidebarView)
|
|
|
scanPhaseView.addSubview(previewContainer)
|
|
scanPhaseView.addSubview(previewContainer)
|
|
@@ -396,11 +267,16 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
scanPhaseView.addSubview(scanButton)
|
|
scanPhaseView.addSubview(scanButton)
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
|
- titleLabel.topAnchor.constraint(equalTo: scanPhaseView.topAnchor, constant: 16),
|
|
|
|
|
|
|
+ backButton.leadingAnchor.constraint(equalTo: scanPhaseView.leadingAnchor, constant: 24),
|
|
|
|
|
+ backButton.topAnchor.constraint(equalTo: scanPhaseView.topAnchor, constant: 20),
|
|
|
|
|
+ backButton.widthAnchor.constraint(equalToConstant: 40),
|
|
|
|
|
+ backButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
|
|
+
|
|
|
titleLabel.centerXAnchor.constraint(equalTo: scanPhaseView.centerXAnchor),
|
|
titleLabel.centerXAnchor.constraint(equalTo: scanPhaseView.centerXAnchor),
|
|
|
|
|
+ titleLabel.centerYAnchor.constraint(equalTo: backButton.centerYAnchor),
|
|
|
|
|
|
|
|
sidebarView.leadingAnchor.constraint(equalTo: scanPhaseView.leadingAnchor),
|
|
sidebarView.leadingAnchor.constraint(equalTo: scanPhaseView.leadingAnchor),
|
|
|
- sidebarView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12),
|
|
|
|
|
|
|
+ sidebarView.topAnchor.constraint(equalTo: backButton.bottomAnchor, constant: 16),
|
|
|
sidebarView.bottomAnchor.constraint(equalTo: scanPhaseView.bottomAnchor),
|
|
sidebarView.bottomAnchor.constraint(equalTo: scanPhaseView.bottomAnchor),
|
|
|
sidebarView.widthAnchor.constraint(equalToConstant: 168),
|
|
sidebarView.widthAnchor.constraint(equalToConstant: 168),
|
|
|
|
|
|
|
@@ -455,8 +331,12 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
private func setupResultPhase() {
|
|
private func setupResultPhase() {
|
|
|
resultPhaseView.translatesAutoresizingMaskIntoConstraints = false
|
|
resultPhaseView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
|
|
|
|
|
+ let resultBackButton = ScannerToolbarButton(symbolName: "chevron.left", accessibilityLabel: "Back")
|
|
|
|
|
+ resultBackButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ resultBackButton.onClick = { [weak self] in self?.dismiss() }
|
|
|
|
|
+
|
|
|
let resultTitle = NSTextField(labelWithString: "Scan Result")
|
|
let resultTitle = NSTextField(labelWithString: "Scan Result")
|
|
|
- resultTitle.font = AppTheme.semiboldFont(size: 16)
|
|
|
|
|
|
|
+ resultTitle.font = AppTheme.semiboldFont(size: 18)
|
|
|
resultTitle.alignment = .center
|
|
resultTitle.alignment = .center
|
|
|
resultTitle.textColor = AppTheme.textPrimary
|
|
resultTitle.textColor = AppTheme.textPrimary
|
|
|
resultTitle.translatesAutoresizingMaskIntoConstraints = false
|
|
resultTitle.translatesAutoresizingMaskIntoConstraints = false
|
|
@@ -479,40 +359,47 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
|
|
|
resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
resultImageView.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
|
|
|
|
|
|
|
- savePDFButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
- printButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
savePDFButton.onClick = { [weak self] in self?.saveAsPDF() }
|
|
savePDFButton.onClick = { [weak self] in self?.saveAsPDF() }
|
|
|
printButton.onClick = { [weak self] in self?.printScan() }
|
|
printButton.onClick = { [weak self] in self?.printScan() }
|
|
|
|
|
|
|
|
|
|
+ let actionRow = NSStackView(views: [savePDFButton, printButton])
|
|
|
|
|
+ actionRow.orientation = .horizontal
|
|
|
|
|
+ actionRow.spacing = 12
|
|
|
|
|
+ actionRow.distribution = .fillEqually
|
|
|
|
|
+ actionRow.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+ actionRow.setContentHuggingPriority(.required, for: .vertical)
|
|
|
|
|
+ actionRow.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
|
|
|
+
|
|
|
resultPreviewContainer.addSubview(resultImageView)
|
|
resultPreviewContainer.addSubview(resultImageView)
|
|
|
|
|
+ resultPhaseView.addSubview(resultBackButton)
|
|
|
resultPhaseView.addSubview(resultTitle)
|
|
resultPhaseView.addSubview(resultTitle)
|
|
|
resultPhaseView.addSubview(resultPreviewContainer)
|
|
resultPhaseView.addSubview(resultPreviewContainer)
|
|
|
- resultPhaseView.addSubview(savePDFButton)
|
|
|
|
|
- resultPhaseView.addSubview(printButton)
|
|
|
|
|
|
|
+ resultPhaseView.addSubview(actionRow)
|
|
|
|
|
|
|
|
NSLayoutConstraint.activate([
|
|
NSLayoutConstraint.activate([
|
|
|
- resultTitle.topAnchor.constraint(equalTo: resultPhaseView.topAnchor, constant: 16),
|
|
|
|
|
|
|
+ resultBackButton.leadingAnchor.constraint(equalTo: resultPhaseView.leadingAnchor, constant: 24),
|
|
|
|
|
+ resultBackButton.topAnchor.constraint(equalTo: resultPhaseView.topAnchor, constant: 20),
|
|
|
|
|
+ resultBackButton.widthAnchor.constraint(equalToConstant: 40),
|
|
|
|
|
+ resultBackButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
|
|
+
|
|
|
resultTitle.centerXAnchor.constraint(equalTo: resultPhaseView.centerXAnchor),
|
|
resultTitle.centerXAnchor.constraint(equalTo: resultPhaseView.centerXAnchor),
|
|
|
|
|
+ resultTitle.centerYAnchor.constraint(equalTo: resultBackButton.centerYAnchor),
|
|
|
|
|
|
|
|
- resultPreviewContainer.leadingAnchor.constraint(equalTo: resultPhaseView.leadingAnchor, constant: 16),
|
|
|
|
|
- resultPreviewContainer.trailingAnchor.constraint(equalTo: resultPhaseView.trailingAnchor, constant: -16),
|
|
|
|
|
- resultPreviewContainer.topAnchor.constraint(equalTo: resultTitle.bottomAnchor, constant: 12),
|
|
|
|
|
- resultPreviewContainer.bottomAnchor.constraint(equalTo: savePDFButton.topAnchor, constant: -16),
|
|
|
|
|
|
|
+ resultPreviewContainer.leadingAnchor.constraint(equalTo: resultPhaseView.leadingAnchor, constant: AppTheme.contentPanelInset),
|
|
|
|
|
+ resultPreviewContainer.trailingAnchor.constraint(equalTo: resultPhaseView.trailingAnchor, constant: -AppTheme.contentPanelInset),
|
|
|
|
|
+ resultPreviewContainer.topAnchor.constraint(equalTo: resultBackButton.bottomAnchor, constant: 28),
|
|
|
|
|
+ resultPreviewContainer.bottomAnchor.constraint(equalTo: actionRow.topAnchor, constant: -16),
|
|
|
|
|
|
|
|
resultImageView.leadingAnchor.constraint(equalTo: resultPreviewContainer.leadingAnchor, constant: 12),
|
|
resultImageView.leadingAnchor.constraint(equalTo: resultPreviewContainer.leadingAnchor, constant: 12),
|
|
|
resultImageView.trailingAnchor.constraint(equalTo: resultPreviewContainer.trailingAnchor, constant: -12),
|
|
resultImageView.trailingAnchor.constraint(equalTo: resultPreviewContainer.trailingAnchor, constant: -12),
|
|
|
resultImageView.topAnchor.constraint(equalTo: resultPreviewContainer.topAnchor, constant: 12),
|
|
resultImageView.topAnchor.constraint(equalTo: resultPreviewContainer.topAnchor, constant: 12),
|
|
|
resultImageView.bottomAnchor.constraint(equalTo: resultPreviewContainer.bottomAnchor, constant: -12),
|
|
resultImageView.bottomAnchor.constraint(equalTo: resultPreviewContainer.bottomAnchor, constant: -12),
|
|
|
|
|
|
|
|
- savePDFButton.leadingAnchor.constraint(equalTo: resultPhaseView.leadingAnchor, constant: 16),
|
|
|
|
|
- savePDFButton.bottomAnchor.constraint(equalTo: resultPhaseView.bottomAnchor, constant: -20),
|
|
|
|
|
|
|
+ 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),
|
|
savePDFButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
- savePDFButton.widthAnchor.constraint(equalTo: resultPhaseView.widthAnchor, multiplier: 0.5, constant: -22),
|
|
|
|
|
-
|
|
|
|
|
- printButton.trailingAnchor.constraint(equalTo: resultPhaseView.trailingAnchor, constant: -16),
|
|
|
|
|
- printButton.bottomAnchor.constraint(equalTo: resultPhaseView.bottomAnchor, constant: -20),
|
|
|
|
|
printButton.heightAnchor.constraint(equalToConstant: 40),
|
|
printButton.heightAnchor.constraint(equalToConstant: 40),
|
|
|
- printButton.widthAnchor.constraint(equalTo: resultPhaseView.widthAnchor, multiplier: 0.5, constant: -22),
|
|
|
|
|
])
|
|
])
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -659,6 +546,51 @@ final class ScannerOverlayView: NSView, AppearanceRefreshable {
|
|
|
|
|
|
|
|
// MARK: - Controls
|
|
// MARK: - Controls
|
|
|
|
|
|
|
|
|
|
+private final class ScannerToolbarButton: NSControl, AppearanceRefreshable {
|
|
|
|
|
+ var onClick: (() -> Void)?
|
|
|
|
|
+
|
|
|
|
|
+ private let iconView = NSImageView()
|
|
|
|
|
+
|
|
|
|
|
+ init(symbolName: String, accessibilityLabel: String) {
|
|
|
|
|
+ super.init(frame: .zero)
|
|
|
|
|
+ toolTip = accessibilityLabel
|
|
|
|
|
+ wantsLayer = true
|
|
|
|
|
+ layer?.cornerRadius = 12
|
|
|
|
|
+
|
|
|
|
|
+ if let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: accessibilityLabel) {
|
|
|
|
|
+ let config = NSImage.SymbolConfiguration(pointSize: 15, weight: .semibold)
|
|
|
|
|
+ iconView.image = image.withSymbolConfiguration(config)
|
|
|
|
|
+ }
|
|
|
|
|
+ iconView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
|
+
|
|
|
|
|
+ addSubview(iconView)
|
|
|
|
|
+ NSLayoutConstraint.activate([
|
|
|
|
|
+ iconView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
|
|
|
+ iconView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
|
|
|
|
+ ])
|
|
|
|
|
+ refreshAppearance()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @available(*, unavailable)
|
|
|
|
|
+ required init?(coder: NSCoder) { nil }
|
|
|
|
|
+
|
|
|
|
|
+ func refreshAppearance() {
|
|
|
|
|
+ layer?.backgroundColor = AppTheme.elevatedBackground.cgColor
|
|
|
|
|
+ layer?.borderColor = AppTheme.paywallBorder.cgColor
|
|
|
|
|
+ layer?.borderWidth = 1.5
|
|
|
|
|
+ iconView.contentTintColor = AppTheme.textPrimary
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func mouseUp(with event: NSEvent) {
|
|
|
|
|
+ guard bounds.contains(convert(event.locationInWindow, from: nil)) else { return }
|
|
|
|
|
+ onClick?()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override func resetCursorRects() {
|
|
|
|
|
+ addCursorRect(bounds, cursor: .pointingHand)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
private final class FlippedSidebarContentView: NSView {
|
|
private final class FlippedSidebarContentView: NSView {
|
|
|
override var isFlipped: Bool { true }
|
|
override var isFlipped: Bool { true }
|
|
|
}
|
|
}
|