Эх сурвалжийг харах

Fix contact printing layout and stop repeated contacts permission prompts.

Correct PDF text coordinate transforms so printed contacts read upright from the top, and cache contacts access so the system dialog is only shown when authorization is still undetermined.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 сар өмнө
parent
commit
ae654c06aa

+ 23 - 0
smart_printer/AppDelegate.swift

@@ -4,6 +4,7 @@
 //
 //
 
 
 import Cocoa
 import Cocoa
+import Contacts
 
 
 @main
 @main
 class AppDelegate: NSObject, NSApplicationDelegate {
 class AppDelegate: NSObject, NSApplicationDelegate {
@@ -19,9 +20,31 @@ class AppDelegate: NSObject, NSApplicationDelegate {
         resolveMainWindowController()
         resolveMainWindowController()
         configureMainWindow()
         configureMainWindow()
         configurePreferencesMenu()
         configurePreferencesMenu()
+        observeContactChanges()
+        prefetchContactsIfAuthorized()
         NSApp.activate(ignoringOtherApps: true)
         NSApp.activate(ignoringOtherApps: true)
     }
     }
 
 
+    private func observeContactChanges() {
+        NotificationCenter.default.addObserver(
+            self,
+            selector: #selector(contactsDidChange),
+            name: .CNContactStoreDidChange,
+            object: nil
+        )
+    }
+
+    @objc private func contactsDidChange() {
+        ContactsService.invalidateCache()
+    }
+
+    private func prefetchContactsIfAuthorized() {
+        guard ContactsService.isAuthorized else { return }
+        Task { @MainActor in
+            _ = try? await ContactsService.loadContacts()
+        }
+    }
+
     @objc func showSettings(_ sender: Any?) {
     @objc func showSettings(_ sender: Any?) {
         NSApp.activate(ignoringOtherApps: true)
         NSApp.activate(ignoringOtherApps: true)
         mainWindow?.makeKeyAndOrderFront(nil)
         mainWindow?.makeKeyAndOrderFront(nil)

+ 52 - 13
smart_printer/PrintContactsView.swift

@@ -21,18 +21,61 @@ struct PrintableContact: Equatable {
 
 
 // MARK: - Contacts Service
 // MARK: - Contacts Service
 
 
+enum ContactsError: Error {
+    case accessDenied
+}
+
 enum ContactsService {
 enum ContactsService {
-    static func requestAccess() async -> Bool {
-        let store = CNContactStore()
-        do {
-            return try await store.requestAccess(for: .contacts)
-        } catch {
+    private static let store = CNContactStore()
+    private static var cachedContacts: [PrintableContact]?
+
+    static var isAuthorized: Bool {
+        switch CNContactStore.authorizationStatus(for: .contacts) {
+        case .authorized, .limited:
+            return true
+        default:
+            return false
+        }
+    }
+
+    @MainActor
+    static func ensureAccess() async -> Bool {
+        switch CNContactStore.authorizationStatus(for: .contacts) {
+        case .authorized, .limited:
+            return true
+        case .notDetermined:
+            do {
+                return try await store.requestAccess(for: .contacts)
+            } catch {
+                return false
+            }
+        case .denied, .restricted:
+            return false
+        @unknown default:
             return false
             return false
         }
         }
     }
     }
 
 
+    @MainActor
+    static func loadContacts(forceRefresh: Bool = false) async throws -> [PrintableContact] {
+        if !forceRefresh, let cachedContacts, isAuthorized {
+            return cachedContacts
+        }
+
+        guard await ensureAccess() else {
+            throw ContactsError.accessDenied
+        }
+
+        let contacts = try fetchContacts()
+        cachedContacts = contacts
+        return contacts
+    }
+
+    static func invalidateCache() {
+        cachedContacts = nil
+    }
+
     static func fetchContacts() throws -> [PrintableContact] {
     static func fetchContacts() throws -> [PrintableContact] {
-        let store = CNContactStore()
         let keys: [CNKeyDescriptor] = [
         let keys: [CNKeyDescriptor] = [
             CNContactIdentifierKey as CNKeyDescriptor,
             CNContactIdentifierKey as CNKeyDescriptor,
             CNContactGivenNameKey as CNKeyDescriptor,
             CNContactGivenNameKey as CNKeyDescriptor,
@@ -296,16 +339,12 @@ final class PrintContactsOverlayView: NSView, AppearanceRefreshable {
 
 
     private func loadContacts() {
     private func loadContacts() {
         Task { @MainActor in
         Task { @MainActor in
-            let granted = await ContactsService.requestAccess()
-            guard granted else {
-                showAccessDeniedAlert()
-                return
-            }
-
             do {
             do {
-                allContacts = try ContactsService.fetchContacts()
+                allContacts = try await ContactsService.loadContacts()
                 selectedIDs = Set(allContacts.map(\.identifier))
                 selectedIDs = Set(allContacts.map(\.identifier))
                 reloadList()
                 reloadList()
+            } catch ContactsError.accessDenied {
+                showAccessDeniedAlert()
             } catch {
             } catch {
                 showLoadFailedAlert()
                 showLoadFailedAlert()
             }
             }

+ 50 - 13
smart_printer/PrintService.swift

@@ -116,7 +116,7 @@ enum PrintService {
         guard let operation = document.printOperation(
         guard let operation = document.printOperation(
             for: printInfo,
             for: printInfo,
             scalingMode: .pageScaleToFit,
             scalingMode: .pageScaleToFit,
-            autoRotate: true
+            autoRotate: false
         ) else {
         ) else {
             showPrintFailedAlert()
             showPrintFailedAlert()
             return
             return
@@ -178,7 +178,7 @@ enum PrintService {
         printInfo.horizontalPagination = .fit
         printInfo.horizontalPagination = .fit
         printInfo.verticalPagination = .fit
         printInfo.verticalPagination = .fit
         printInfo.isHorizontallyCentered = true
         printInfo.isHorizontallyCentered = true
-        printInfo.isVerticallyCentered = true
+        printInfo.isVerticallyCentered = false
         printInfo.topMargin = 36
         printInfo.topMargin = 36
         printInfo.bottomMargin = 36
         printInfo.bottomMargin = 36
         printInfo.leftMargin = 36
         printInfo.leftMargin = 36
@@ -192,9 +192,14 @@ enum PrintService {
         let pageSize = printInfo.paperSize
         let pageSize = printInfo.paperSize
         guard pageSize.width > 0, pageSize.height > 0 else { return nil }
         guard pageSize.width > 0, pageSize.height > 0 else { return nil }
 
 
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.alignment = .left
+        paragraphStyle.lineBreakMode = .byWordWrapping
+
         let attributes: [NSAttributedString.Key: Any] = [
         let attributes: [NSAttributedString.Key: Any] = [
             .font: NSFont.systemFont(ofSize: 12),
             .font: NSFont.systemFont(ofSize: 12),
             .foregroundColor: NSColor.black,
             .foregroundColor: NSColor.black,
+            .paragraphStyle: paragraphStyle,
         ]
         ]
         let attributed = NSAttributedString(string: text, attributes: attributes)
         let attributed = NSAttributedString(string: text, attributes: attributes)
 
 
@@ -214,8 +219,7 @@ enum PrintService {
             return nil
             return nil
         }
         }
 
 
-        var glyphIndex = 0
-        while glyphIndex < layoutManager.numberOfGlyphs {
+        while true {
             let textContainer = NSTextContainer(size: containerSize)
             let textContainer = NSTextContainer(size: containerSize)
             textContainer.lineFragmentPadding = 0
             textContainer.lineFragmentPadding = 0
             layoutManager.addTextContainer(textContainer)
             layoutManager.addTextContainer(textContainer)
@@ -223,24 +227,57 @@ enum PrintService {
             let glyphRange = layoutManager.glyphRange(for: textContainer)
             let glyphRange = layoutManager.glyphRange(for: textContainer)
             guard glyphRange.length > 0 else { break }
             guard glyphRange.length > 0 else { break }
 
 
-            context.beginPDFPage(nil)
+            let charRange = layoutManager.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil)
+            let pageText = attributed.attributedSubstring(from: charRange)
 
 
-            NSGraphicsContext.saveGraphicsState()
-            NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: false)
-            layoutManager.drawGlyphs(
-                forGlyphRange: glyphRange,
-                at: NSPoint(x: printInfo.leftMargin, y: printInfo.bottomMargin)
+            context.beginPDFPage(nil)
+            drawTextPage(
+                pageText,
+                in: context,
+                pageSize: pageSize,
+                printInfo: printInfo,
+                textWidth: textWidth,
+                textHeight: textHeight
             )
             )
-            NSGraphicsContext.restoreGraphicsState()
-
             context.endPDFPage()
             context.endPDFPage()
-            glyphIndex = NSMaxRange(glyphRange)
+
+            if NSMaxRange(glyphRange) >= layoutManager.numberOfGlyphs { break }
         }
         }
 
 
         context.closePDF()
         context.closePDF()
         return PDFDocument(data: pdfData as Data)
         return PDFDocument(data: pdfData as Data)
     }
     }
 
 
+    private static func drawTextPage(
+        _ text: NSAttributedString,
+        in context: CGContext,
+        pageSize: NSSize,
+        printInfo: NSPrintInfo,
+        textWidth: CGFloat,
+        textHeight: CGFloat
+    ) {
+        context.saveGState()
+        context.translateBy(x: 0, y: pageSize.height)
+        context.scaleBy(x: 1, y: -1)
+
+        let drawRect = CGRect(
+            x: printInfo.leftMargin,
+            y: printInfo.topMargin,
+            width: textWidth,
+            height: textHeight
+        )
+
+        NSGraphicsContext.saveGraphicsState()
+        NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true)
+        text.draw(
+            with: drawRect,
+            options: [.usesLineFragmentOrigin, .usesFontLeading]
+        )
+        NSGraphicsContext.restoreGraphicsState()
+
+        context.restoreGState()
+    }
+
     private static func pdfDocument(from image: NSImage) -> PDFDocument? {
     private static func pdfDocument(from image: NSImage) -> PDFDocument? {
         let printInfo = configuredPrintInfo()
         let printInfo = configuredPrintInfo()
         let pageSize = printInfo.paperSize
         let pageSize = printInfo.paperSize

+ 5 - 3
smart_printer/ViewController.swift

@@ -108,10 +108,12 @@ class ViewController: NSViewController {
         printTextOverlay?.dismiss(animated: false)
         printTextOverlay?.dismiss(animated: false)
         drawPrintOverlay?.dismiss(animated: false)
         drawPrintOverlay?.dismiss(animated: false)
 
 
-        let overlay = PrintContactsOverlayView()
-        overlay.onDismiss = { [weak self] in
-            self?.printContactsOverlay = nil
+        if let overlay = printContactsOverlay {
+            overlay.present(in: mainContentView)
+            return
         }
         }
+
+        let overlay = PrintContactsOverlayView()
         printContactsOverlay = overlay
         printContactsOverlay = overlay
         overlay.present(in: mainContentView)
         overlay.present(in: mainContentView)
     }
     }